diff --git a/src/App.vue b/src/App.vue index 8124308..5a88f6e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -23,11 +23,16 @@ - + + - - - + + + + + @@ -54,20 +62,21 @@ + > + + - {{ authorName }} - {{ bookTitle }} - {{ currentChapter }} + {{ $store.getters.book.author }} + {{ $store.getters.book.title }} - {{ $store.getters.chapters[currentChapter].title }} @@ -110,7 +119,9 @@ :class="{ 'mr-3': mdAndUp, 'ml-3': mdAndDown }" :min="0" :max="10" + thumb-label hide-details + color="blue" v-model="volume" prepend-icon="mdi-volume-low" append-icon="mdi-volume-high" @@ -138,6 +149,7 @@ --> + @@ -146,11 +158,13 @@ import { ref } from "vue"; import { useDisplay } from "vuetify"; import MessageBox from "./components/MessageBox"; + import SettingsDialog from "./components/SettingsDialog"; export default { name: "App", components: { MessageBox, + SettingsDialog, }, setup () { @@ -180,15 +194,19 @@ appData: { name: "", net: "" }, backgroundImage: this.defaultBackground, - authors: [], + authors: [], + autoBookmark: true, - audioElement: null, - status: 0, - currentChapter: null, - volume: 5, + audioElement: null, + currentChapter: null, + currentProgress: 0, + currentTime: "00:00", + playbackRate: 1, + status: 0, + volume: 5, - scrolled: false, - useWikipedia: true, + scrolled: false, + useWikipedia: true, } }, @@ -203,7 +221,26 @@ } }, - volume () { this.updateVolume(); } + "$store.getters.darkTheme" (val) { + this.setTheme(val == true ? "dark" : "light"); + localStorage.darkTheme = val; + }, + + "$store.getters.playbackRate" (val) { + this.playbackRate = val; + localStorage.playbackRate = val; + }, + + playbackRate (val) { + if (this.audioElement !== null) { + this.audioElement.playbackRate = val; + } + }, + + volume (val) { + this.updateVolume(); + localStorage.volume = val; + } }, computed: { @@ -223,12 +260,35 @@ return (this.currentChapter !== null) && this.audioElement; }, + progress: { + get () { + return this.currentProgress; + }, + + set (val) { + if (this.audioElement !== null && val !== Math.round(this.currentProgress)) { + //console.log(this.audioElement); + //console.log(this.audioElement.duration); + this.currentProgress = val; + this.audioElement.currentTime = this.audioElement.duration * val / 100; + } + } + }, + showProgress () { return false; } }, mounted () { + let darkTheme = localStorage.darkTheme ? JSON.parse(localStorage.darkTheme) : false; + this.$store.commit("setDarkTheme", darkTheme); + + let playbackRate = localStorage.playbackRate ? JSON.parse(localStorage.playbackRate) : 1; + this.$store.commit("setPlaybackRate", playbackRate); + + this.volume = localStorage.volume ? JSON.parse(localStorage.volume) : 5; + this.appData.name = this.$t("appName"); this.appData.net = this.appConfig.appNet; @@ -352,6 +412,12 @@ console.log(bmKey, bookmark); + if (bookmark !== null) { + this.loadChapter(bookmark.chapter, bookmark.progress); + } else { + this.loadChapter(); + } + if (this.useWikipedia) { this.fillWikiInfo(bookData.title, false); } @@ -412,16 +478,135 @@ }) }, + loadChapter (index = 0, progress = 0, autoplay = false) { + if (this.audioElement) { this.audioElement.pause(); } + if (index >= this.$store.getters.chapters.length) { return false; } // show message? + + this.currentChapter = index; + this.audioElement = new Audio(encodeURI(this.$store.getters.chapters[index].url)); + this.audioElement.playbackRate = this.playbackRate; + + this.$store.commit("setCurChapter", index); + + this.updateVolume(); + + this.status = this.statuses.stopped; + + this.audioElement.addEventListener("ended", this.loadNextChapter); + this.audioElement.ontimeupdate = this.updateProgress; + + let component = this; + + this.audioElement.addEventListener("durationchange", function () { + let min = parseInt(component.audioElement.duration / 60) < 10 + ? "0" + parseInt(component.audioElement.duration / 60) + : parseInt(component.audioElement.duration / 60); + let sec = parseInt(component.audioElement.duration % 60) < 10 + ? "0" + parseInt(component.audioElement.duration % 60) + : parseInt(component.audioElement.duration % 60); + component.duration = min + ":" + sec; + }); + + this.audioElement.addEventListener("canplay", function _listener () { + component.audioElement.currentTime = component.audioElement.duration * progress / 100; + component.audioElement.removeEventListener("canplay", _listener, true); + }, true); + + if (autoplay) this.play(); + }, + + loadNextChapter (autoplay = true) { + this.currentChapter++; + if (this.currentChapter >= this.$store.getters.chapters.length) { + this.currentChapter--; + return false; + } + this.loadChapter(this.currentChapter, 0, autoplay); + }, + + nextChapter () { + if (this.currentChapter < this.$store.getters.chapters.length - 1) { + this.currentChapter++; + this.loadChapter(this.currentChapter, 0, true); + } + }, + onScroll () { this.scrolled = window.scrollY > 0; }, + pause () { + this.status = this.statuses.paused; + this.audioElement.pause(); + }, + + play () { + this.status = this.statuses.playing; + this.audioElement.play(); + }, + + prevChapter () { + if (this.currentChapter > 0) { + this.currentChapter--; + this.loadChapter(this.currentChapter, 0, true); + } + }, + + setBookmark (silent = true) { + if (this.$store.getters.chapters.length == 0) { return; } + + let data = { + chapter: this.$store.getters.curChapter, + progress: this.currentProgress + }; + + localStorage.setItem( + this.getHash(JSON.stringify(this.$store.getters.chapters)), + JSON.stringify(data) + ); + + if (!silent) { this.showMessage(this.$t("information"), this.$t("bookmarkSaved")); } + }, + showMessage (title, text) { this.$refs.MessageBox.title = title; this.$refs.MessageBox.text = text; this.$refs.MessageBox.dialog = true; }, + showSettings () { + this.$refs.SettingsDialog.dialog = true; + }, + + toggleStatus () { + if (!this.isChapterLoaded) { + this.loadChapter(this.currentChapter || 0); + } + + if (!this.isPlaying) { + this.play(); + } else { + this.pause(); + this.setBookmark(); + } + }, + + updateProgress () { + if (!this.audioElement || !this.audioElement.currentTime) { + return this.currentProgress = 0; + } + + this.currentProgress = (this.audioElement.currentTime / this.audioElement.duration) * 100; + + let min = parseInt(this.audioElement.currentTime / 60) < 10 + ? "0" + parseInt(this.audioElement.currentTime / 60) + : parseInt(this.audioElement.currentTime / 60); + let sec = parseInt(this.audioElement.currentTime % 60) < 10 + ? "0" + parseInt(this.audioElement.currentTime % 60) + : parseInt(this.audioElement.currentTime % 60); + this.currentTime = min + ":" + sec; + }, + updateVolume () { this.audioElement ? this.audioElement.volume = (this.volume / 10) : null; } diff --git a/src/components/BookReader.vue b/src/components/BookReader.vue index 6071549..92cee8a 100644 --- a/src/components/BookReader.vue +++ b/src/components/BookReader.vue @@ -123,8 +123,19 @@ data: () => ({ illustrated: false, - currentChapter: 0 - }) + }), + + computed: { + currentChapter: function () { + return this.$store.getters.curChapter; + } + }, + + methods: { + loadChapter (index = 0, progress = 0, autoplay = false) { + this.$emit("loadChapter", index, progress, autoplay); + } + } } @@ -146,5 +157,5 @@ background: transparent .v-list-item.no-selection-pl - padding-left: 74px + padding-left: 73px diff --git a/src/components/SettingsDialog.vue b/src/components/SettingsDialog.vue new file mode 100644 index 0000000..962b250 --- /dev/null +++ b/src/components/SettingsDialog.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/plugins/locales/en.js b/src/plugins/locales/en.js index 27a52be..6fbaf70 100644 --- a/src/plugins/locales/en.js +++ b/src/plugins/locales/en.js @@ -2,8 +2,13 @@ const messages = { appName: "AudioLib", authors: "Authors", book: "Books", + bookmarkSaved: "Bookmark saved", books: "Book", - cycle: "Cycle" + cycle: "Cycle", + darkTheme: "DarkTheme", + information: "Information", + playbackRate: "Playback rate", + settings: "Settings" } export default messages; diff --git a/src/plugins/locales/ru.js b/src/plugins/locales/ru.js index c905e1f..e692d08 100644 --- a/src/plugins/locales/ru.js +++ b/src/plugins/locales/ru.js @@ -2,8 +2,13 @@ const messages = { appName: "Аудиотека", authors: "Авторы", book: "Книга", + bookmarkSaved: "Закладка сохранена", books: "Книги", - cycle: "Цикл" + cycle: "Цикл", + darkTheme: "Тёмная тема", + information: "Информация", + playbackRate: "Скорость воспроизведения", + settings: "Настройки" } export default messages; diff --git a/src/store.js b/src/store.js index ef6e0cf..2c7767c 100644 --- a/src/store.js +++ b/src/store.js @@ -10,7 +10,6 @@ const store = createStore({ wiki: "" }, authors: [], - books: [], book: { author: "", photo: "", @@ -18,24 +17,34 @@ const store = createStore({ cover: "", wiki: "" }, - chapters: [] + books: [], + chapters: [], + curChapter: 0, + darkTheme: false, + playbackRate: 1, } }, getters: { - author: state => { return state.author; }, - authors: state => { return state.authors; }, - books: state => { return state.books; }, - book: state => { return state.book; }, - chapters: state => { return state.chapters } + author: state => { return state.author; }, + authors: state => { return state.authors; }, + book: state => { return state.book; }, + books: state => { return state.books; }, + chapters: state => { return state.chapters }, + curChapter: state => { return state.curChapter }, + darkTheme: state => { return state.darkTheme }, + playbackRate: state => { return state.playbackRate }, }, mutations: { - setAuthors (state, payload) { state.authors = payload; }, - setAuthor (state, payload) { state.author = payload; }, - setBooks (state, payload) { state.books = payload; }, - setBook (state, payload) { state.book = payload; }, - setChapters (state, payload) { state.chapters = payload; } + setAuthor (state, payload) { state.author = payload; }, + setAuthors (state, payload) { state.authors = payload; }, + setBook (state, payload) { state.book = payload; }, + setBooks (state, payload) { state.books = payload; }, + setChapters (state, payload) { state.chapters = payload; }, + setCurChapter (state, payload) { state.curChapter = payload; }, + setDarkTheme (state, payload) { state.darkTheme = payload; }, + setPlaybackRate (state, payload) { state.playbackRate = payload; }, } }); diff --git a/yarn.lock b/yarn.lock index 9336a1f..6bf6602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5942,9 +5942,9 @@ vuetify-loader@^2.0.0-alpha.0: upath "^2.0.1" "vuetify@npm:@vuetify/nightly@next": - version "3.0.0-next-20220415.0" - resolved "https://registry.yarnpkg.com/@vuetify/nightly/-/nightly-3.0.0-next-20220415.0.tgz#dbcc0ae803a6efbb4eb6dbdb1c9133fcad7012ab" - integrity sha512-+KByttalgyzdE8JQmbj5sQiKCLcRJgjrdUJL2aY3c8AbLEZEwFa4ryAimkY73W/JFcUTpuw29oGiiN6BV5AFww== + version "3.0.0-next-20220416.0" + resolved "https://registry.yarnpkg.com/@vuetify/nightly/-/nightly-3.0.0-next-20220416.0.tgz#d3be97e6130900647b51ca924ae60b0812a9b5d2" + integrity sha512-3jgtwcZXmrCeR1gcLyL2jvUnZftc9RyaXHdQpXQhmlFazYEtSymsRoD8RAEv2P2GeVGKVKkR7KfBrVhUBEgaSg== vuex@^4.0.2: version "4.0.2"