541 lines
20 KiB
Vue
541 lines
20 KiB
Vue
<template>
|
|
<v-app :dark="darkTheme">
|
|
<v-toolbar app>
|
|
<v-toolbar-title class="headline text-uppercase">
|
|
<span v-if="appNet !== ''">{{ appNet }}:</span>
|
|
<span class="font-weight-light">{{ appName }}</span>
|
|
</v-toolbar-title>
|
|
|
|
<v-progress-circular
|
|
indeterminate
|
|
v-if="isLoading"
|
|
class="loading-progress"
|
|
></v-progress-circular>
|
|
|
|
<v-spacer></v-spacer>
|
|
|
|
<v-menu bottom left>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn
|
|
flat
|
|
icon
|
|
v-on="on"
|
|
>
|
|
<v-icon>mdi-bookmark</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
|
|
<v-list>
|
|
<v-list-tile @click="setBookmark(false)">
|
|
<v-list-tile-action>
|
|
<v-icon>mdi-bookmark-plus</v-icon>
|
|
</v-list-tile-action>
|
|
<v-list-tile-title>{{ $t("setBookmark") }}</v-list-tile-title>
|
|
</v-list-tile>
|
|
<v-list-tile @click="removeBookmark(false)">
|
|
<v-list-tile-action>
|
|
<v-icon>mdi-bookmark-minus</v-icon>
|
|
</v-list-tile-action>
|
|
<v-list-tile-title>{{ $t("removeBookmark") }}</v-list-tile-title>
|
|
</v-list-tile>
|
|
</v-list>
|
|
</v-menu>
|
|
|
|
<v-menu bottom left>
|
|
<template v-slot:activator="{ on }">
|
|
<v-btn
|
|
flat
|
|
icon
|
|
v-on="on"
|
|
>
|
|
<v-icon>more_vert</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
|
|
<v-list>
|
|
<v-list-tile>
|
|
<v-list-tile-action>
|
|
<v-switch v-model="darkTheme" color="blue"></v-switch>
|
|
</v-list-tile-action>
|
|
<v-list-tile-title>{{ $t("darkTheme") }}</v-list-tile-title>
|
|
</v-list-tile>
|
|
<v-list-tile>
|
|
<v-list-tile-action>
|
|
<v-switch v-model="autoBookmark" color="blue"></v-switch>
|
|
</v-list-tile-action>
|
|
<v-list-tile-title>{{ $t("autoBookmarks") }}</v-list-tile-title>
|
|
</v-list-tile>
|
|
<v-list-tile>
|
|
<v-list-tile-action>
|
|
<v-switch v-model="useWikipedia" color="blue"></v-switch>
|
|
</v-list-tile-action>
|
|
<v-list-tile-title>{{ $t("useWikipedia") }}</v-list-tile-title>
|
|
</v-list-tile>
|
|
|
|
<v-divider></v-divider>
|
|
|
|
<v-list-tile @click="showAboutDialog">
|
|
<v-list-tile-action>
|
|
<v-icon>mdi-information-variant</v-icon>
|
|
</v-list-tile-action>
|
|
<v-list-tile-title>{{ $t("about") }}</v-list-tile-title>
|
|
</v-list-tile>
|
|
|
|
</v-list>
|
|
</v-menu>
|
|
</v-toolbar>
|
|
|
|
<v-content>
|
|
<v-tabs
|
|
v-model="activeTab"
|
|
centered
|
|
fixed-tabs
|
|
>
|
|
<v-tab ripple>{{ $t("authors") }}</v-tab>
|
|
<v-tab-item>
|
|
<Authors ref="Authors" v-on:author_selected="openBooks" />
|
|
</v-tab-item>
|
|
<v-tab ripple>{{ $t("books") }}</v-tab>
|
|
<v-tab-item>
|
|
<Books ref="Books" v-on:book_selected="openBook" />
|
|
</v-tab-item>
|
|
<v-tab ripple>{{ $t("book") }}</v-tab>
|
|
<v-tab-item>
|
|
<Book ref="Book" v-on:progress_updated="updateProgress" />
|
|
</v-tab-item>
|
|
</v-tabs>
|
|
</v-content>
|
|
|
|
<v-card tile class="player-interface">
|
|
<v-flex xs12>
|
|
<v-slider
|
|
v-model="progress"
|
|
min="0"
|
|
max="100"
|
|
height="30px"
|
|
:disabled="isOff"
|
|
:inverse-label="true"
|
|
:label="`${currentTime}/${duration}`"
|
|
class="time-slider px-2"
|
|
></v-slider>
|
|
</v-flex>
|
|
|
|
<v-list>
|
|
<v-list-tile>
|
|
<v-list-tile-content v-if="$vuetify.breakpoint.smAndUp">
|
|
<v-list-tile-title>{{ authorName }}</v-list-tile-title>
|
|
<v-list-tile-sub-title>{{ bookTitle }}<span v-if="currentChapter !== ''"> - {{ currentChapter }}</span></v-list-tile-sub-title>
|
|
</v-list-tile-content>
|
|
|
|
<v-spacer></v-spacer>
|
|
|
|
<v-list-tile-action @click="prevChapter">
|
|
<v-btn icon>
|
|
<v-icon>mdi-skip-backward</v-icon>
|
|
</v-btn>
|
|
</v-list-tile-action>
|
|
|
|
<v-list-tile-action :class="{ 'mx-5': $vuetify.breakpoint.mdAndUp }">
|
|
<v-btn icon @click="toggleStatus">
|
|
<v-icon>mdi-{{ isPlaying ? "pause" : "play" }}</v-icon>
|
|
</v-btn>
|
|
</v-list-tile-action>
|
|
|
|
<v-list-tile-action
|
|
@click="nextChapter"
|
|
>
|
|
<v-btn icon>
|
|
<v-icon>mdi-skip-forward</v-icon>
|
|
</v-btn>
|
|
</v-list-tile-action>
|
|
<v-flex xs6 sm3 md2>
|
|
<v-slider
|
|
:class="{ 'mr-3 ml-5': $vuetify.breakpoint.mdAndUp, 'ml-3' : $vuetify.breakpoint.mdAndDown }"
|
|
:min="0"
|
|
:max="10"
|
|
v-model="volume"
|
|
append-icon="volume_up"
|
|
prepend-icon="volume_down"
|
|
></v-slider>
|
|
</v-flex>
|
|
</v-list-tile>
|
|
</v-list>
|
|
</v-card>
|
|
<v-fab-transition>
|
|
<v-btn
|
|
class="to-top-button"
|
|
v-show="scrolled"
|
|
color="blue"
|
|
@click="scrollToTop"
|
|
dark
|
|
fixed
|
|
bottom
|
|
right
|
|
fab
|
|
>
|
|
<v-icon>keyboard_arrow_up</v-icon>
|
|
</v-btn>
|
|
</v-fab-transition>
|
|
<MessageDialog ref="MessageDialog" />
|
|
</v-app>
|
|
</template>
|
|
|
|
<script>
|
|
import Authors from "./components/Authors"
|
|
import Books from "./components/Books"
|
|
import Book from "./components/Book"
|
|
import MessageDialog from "./components/MessageDialog"
|
|
|
|
export default {
|
|
name: "App",
|
|
|
|
components: {
|
|
Authors,
|
|
Books,
|
|
Book,
|
|
MessageDialog
|
|
},
|
|
|
|
data () {
|
|
return {
|
|
appNet: "CAINet",
|
|
appName: "Аудиотека",
|
|
|
|
activeTab: null,
|
|
autoBookmark: false,
|
|
darkTheme: false,
|
|
isLoading: false,
|
|
isMounted: false,
|
|
locale: { flag: "ru", language: "ru", title: "Русский" },
|
|
locales: [
|
|
{ flag: "us", language: "en", title: "English" },
|
|
{ flag: "ru", language: "ru", title: "Русский" }
|
|
],
|
|
scrolled: false,
|
|
useWikipedia: false,
|
|
volume: 5//,
|
|
//locale: []
|
|
}
|
|
},
|
|
|
|
created () {
|
|
window.addEventListener("scroll", this.onScroll);
|
|
},
|
|
|
|
mounted () {
|
|
this.isMounted = true;
|
|
this.darkTheme = localStorage.darkTheme
|
|
? JSON.parse(localStorage.darkTheme)
|
|
: false;
|
|
this.autoBookmark = localStorage.autoBookmark
|
|
? JSON.parse(localStorage.autoBookmark)
|
|
: false;
|
|
this.useWikipedia = localStorage.useWikipedia
|
|
? JSON.parse(localStorage.useWikipedia)
|
|
: false;
|
|
this.volume = localStorage.volume
|
|
? JSON.parse(localStorage.volume)
|
|
: 5;
|
|
|
|
this.isLoading = true;
|
|
|
|
this.axios.get(`config.json?r=${Math.random()}`)
|
|
.then(response => {
|
|
this.appNet = response.data.AppNet;
|
|
this.appName = response.data.AppName;
|
|
})
|
|
.catch(error => {
|
|
this.showMessage(this.$t("error"), error);
|
|
console.error(error);
|
|
});
|
|
|
|
this.axios.get(`lib/authors.json?r=${Math.random()}`)
|
|
.then(response => {
|
|
this.$refs.Authors.items = response.data;
|
|
this.isLoading = false;
|
|
})
|
|
.catch(error => {
|
|
this.showMessage(this.$t("error"), error);
|
|
console.error(error);
|
|
});
|
|
},
|
|
|
|
destroyed () {
|
|
window.removeEventListener("scroll", this.onScroll);
|
|
},
|
|
|
|
computed: {
|
|
authorName () {
|
|
return this.isMounted ? this.$refs.Book.authorName : "";
|
|
},
|
|
|
|
bookOpened () {
|
|
return this.isMounted ? this.$refs.Book.items.length > 0 : false;
|
|
},
|
|
|
|
bookTitle () {
|
|
return this.isMounted ? this.$refs.Book.bookTitle : "";
|
|
},
|
|
|
|
currentChapter () {
|
|
let curChapter = this.isMounted ? this.$refs.Book.items[this.$refs.Book.currentChapter] : "";
|
|
return curChapter ? curChapter.title : "";
|
|
},
|
|
|
|
currentTime () {
|
|
return this.isMounted ? this.$refs.Book.currentTime : "00:00";
|
|
},
|
|
|
|
duration () {
|
|
return this.isMounted ? this.$refs.Book.duration : "00:00";
|
|
},
|
|
|
|
isOff () {
|
|
return this.isMounted ? this.$refs.Book.isOff : true;
|
|
},
|
|
|
|
isPlaying () {
|
|
return this.isMounted ? this.$refs.Book.isPlaying : false;
|
|
},
|
|
|
|
progress: {
|
|
get () {
|
|
return this.isMounted ? this.$refs.Book.progress : "";
|
|
},
|
|
|
|
set (newVal) {
|
|
if (newVal !== Math.round(this.$refs.Book.progress)) {
|
|
this.$refs.Book.audioElement.currentTime = this.$refs.Book.audioElement.duration * newVal / 100;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
watch: {
|
|
autoBookmark (val) { localStorage.autoBookmark = val; },
|
|
darkTheme (val) { localStorage.darkTheme = val; },
|
|
useWikipedia (val) { localStorage.useWikipedia = val; },
|
|
volume (val) {
|
|
localStorage.volume = val;
|
|
this.$refs.Book.volume = val;
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
fillWikiInfo (searchText, fillAuthorInfo = true) {
|
|
this.axios.get(`https://${this.locale.language}.wikipedia.org/w/api.php`, {
|
|
params: {
|
|
format: "json",
|
|
action: "query",
|
|
prop: "extracts",
|
|
exintro: 1,
|
|
explaintext: 1,
|
|
redirects: 1,
|
|
titles: searchText,
|
|
origin: "*"
|
|
}
|
|
}).then(response => {
|
|
if (Object.getOwnPropertyNames(response.data.query.pages)[0] > 0) {
|
|
let result = response.data.query.pages[
|
|
Object.getOwnPropertyNames(response.data.query.pages)[0]
|
|
].extract;
|
|
|
|
if (fillAuthorInfo) {
|
|
this.$refs.Books.wikiText = result;
|
|
} else {
|
|
this.$refs.Book.wikiText = result;
|
|
}
|
|
} else {
|
|
if (fillAuthorInfo) {
|
|
this.$refs.Books.wikiText = "";
|
|
} else {
|
|
this.$refs.Book.wikiText = "";
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
this.isLoading = false;
|
|
if (fillAuthorInfo) {
|
|
this.$refs.Books.wikiText = "";
|
|
} else {
|
|
this.$refs.Book.wikiText = "";
|
|
}
|
|
console.error(error);
|
|
});
|
|
},
|
|
|
|
getHash (val) {
|
|
let CryptoJS = require("crypto-js");
|
|
let hash = CryptoJS.MD5(val);
|
|
return hash.toString(CryptoJS.enc.Hex);
|
|
},
|
|
|
|
nextChapter () {
|
|
this.$refs.Book.nextChapter();
|
|
},
|
|
|
|
onScroll () {
|
|
this.scrolled = window.scrollY > 0;
|
|
},
|
|
|
|
openBook (val) {
|
|
this.$refs.Book.authorName = this.$refs.Books.authorName;
|
|
this.$refs.Book.authorImg = this.$refs.Books.authorImg;
|
|
this.$refs.Book.bookImg = val.image;
|
|
this.$refs.Book.bookTitle = val.title;
|
|
|
|
this.isLoading = true;
|
|
|
|
this.axios.get(`${val.book}?r=${Math.random()}`)
|
|
.then(response => {
|
|
this.$refs.Book.items = response.data;
|
|
|
|
let bmKey = this.getHash(JSON.stringify(this.$refs.Book.items));
|
|
let bookmark = localStorage.getItem(bmKey) ? JSON.parse(localStorage.getItem(bmKey)) : null;
|
|
|
|
if (bookmark !== null) {
|
|
this.$refs.Book.loadChapter(bookmark.chapter, bookmark.progress);
|
|
} else {
|
|
this.$refs.Book.loadChapter();
|
|
}
|
|
|
|
if (this.useWikipedia) {
|
|
this.fillWikiInfo(val.title, false);
|
|
}
|
|
this.activeTab = 2;
|
|
this.isLoading = false;
|
|
})
|
|
.catch(error => {
|
|
this.isLoading = false;
|
|
this.showMessage(this.$t("error"), error);
|
|
console.error(error);
|
|
});
|
|
},
|
|
|
|
openBooks (val) {
|
|
this.isLoading = true;
|
|
|
|
this.axios.get(`${val.books}?r=${Math.random()}`)
|
|
.then(response => {
|
|
this.$refs.Books.authorName = val.author;
|
|
this.$refs.Books.authorImg = val.image;
|
|
this.$refs.Books.items = response.data;
|
|
|
|
if (this.useWikipedia) {
|
|
this.fillWikiInfo(val.author);
|
|
}
|
|
|
|
this.activeTab = 1;
|
|
this.isLoading = false;
|
|
})
|
|
.catch(error => {
|
|
this.isLoading = false;
|
|
this.showMessage(this.$t("error"), error);
|
|
console.error(error);
|
|
});
|
|
},
|
|
|
|
prevChapter () { this.$refs.Book.prevChapter(); },
|
|
|
|
progressClick (event) {
|
|
if (!this.isMounted) { return; }
|
|
|
|
if (this.$refs.Book.audioElement !== null) {
|
|
let elLeft = this.$refs.playProgress.$el.getBoundingClientRect().left;
|
|
let elWidth = this.$refs.playProgress.$el.getBoundingClientRect().width;
|
|
|
|
this.$refs.Book.audioElement.currentTime = this.$refs.Book.audioElement.duration * (event.clientX - elLeft) / elWidth;
|
|
}
|
|
},
|
|
|
|
removeBookmark (silent = true) {
|
|
if (this.$refs.Book.items.length == 0) { return; }
|
|
localStorage.removeItem(this.getHash(this.$refs.Book.items));
|
|
if (!silent) { this.showMessage(this.$t("information"), this.$t("bookmarkRemoved")); }
|
|
},
|
|
|
|
scrollToTop () {
|
|
window.scrollTo({
|
|
top: 0,
|
|
left: 0,
|
|
behavior: "smooth"
|
|
});
|
|
},
|
|
|
|
setBookmark (silent = true) {
|
|
if (this.$refs.Book.items.length == 0) { return; }
|
|
|
|
let data = {
|
|
chapter: this.$refs.Book.currentChapter,
|
|
progress: this.$refs.Book.progress
|
|
};
|
|
|
|
localStorage.setItem(
|
|
this.getHash(JSON.stringify(this.$refs.Book.items)),
|
|
JSON.stringify(data)
|
|
);
|
|
|
|
if (!silent) { this.showMessage(this.$t("information"), this.$t("bookmarkSaved")); }
|
|
},
|
|
|
|
setLocale (locale) {
|
|
this.locales.forEach(element => {
|
|
if (element.language == locale) {
|
|
this.locale = element;
|
|
}
|
|
});
|
|
this.$i18n.locale = locale;
|
|
},
|
|
|
|
showAboutDialog () {
|
|
let userLocale = navigator.language || navigator.userLanguage;
|
|
userLocale = userLocale.indexOf("-") > 0
|
|
? userLocale.substring(0, userLocale.indexOf("-")).toLowerCase()
|
|
: userLocale.toLowerCase();
|
|
|
|
let aboutText = "";
|
|
if (userLocale == "ru") {
|
|
aboutText = "<p><center>Аудиотека</center></p>" +
|
|
"<p><center>© Александр Чебыкин, 2019</center></p>" +
|
|
"<p><center>Опубликовано под лицензией <a href=\"https://opensource.org/licenses/MIT\" target=\"_blank\">MIT license</a></center></p>" +
|
|
"<p><center>Git: <a href=\"https://home.cainet.info:3000/cai/AudioLib\" target=\"_blank\">https://home.cainet.info:3000/cai/AudioLib</a></center></p>";
|
|
} else {
|
|
aboutText = "<p><center>Audiobooks library</center></p>" +
|
|
"<p><center>© Alexander I Chebykin, 2019</center></p>" +
|
|
"<p><center>Published under <a href=\"https://opensource.org/licenses/MIT\" target=\"_blank\">MIT license</a></center></p>" +
|
|
"<p><center>Git: <a href=\"https://home.cainet.info:3000/cai/AudioLib\" target=\"_blank\">https://home.cainet.info:3000/cai/AudioLib</a></center></p>";
|
|
}
|
|
this.showMessage(this.$t("about"), aboutText);
|
|
},
|
|
|
|
showMessage (title, text) {
|
|
this.$refs.MessageDialog.title = title;
|
|
this.$refs.MessageDialog.text = text;
|
|
this.$refs.MessageDialog.dialog = true;
|
|
},
|
|
|
|
toggleStatus () {
|
|
this.$refs.Book.toggleStatus();
|
|
if (this.autoBookmark && this.$refs.Book.isPaused) { this.setBookmark(); }
|
|
},
|
|
|
|
updateProgress (val) { this.progress = val; }
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.loading-progress { margin: auto 15px; }
|
|
|
|
.player-interface {
|
|
position: fixed;
|
|
bottom: 0;
|
|
width: 100%;
|
|
}
|
|
.player-interface .time-slider {
|
|
width: 100%;
|
|
margin: 0 0 -25px 0;
|
|
}
|
|
.to-top-button { margin-bottom: 80px; }
|
|
</style>
|