Audio part is added
Audio part is added
This commit is contained in:
parent
4f27a90e30
commit
04f0ccc13c
227
src/App.vue
227
src/App.vue
@ -23,11 +23,16 @@
|
||||
</v-tabs>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<!--
|
||||
<v-btn
|
||||
:icon="theme == 'light' ? 'mdi-weather-night' : 'mdi-weather-sunny'"
|
||||
@click="toggleTheme"
|
||||
></v-btn>
|
||||
-->
|
||||
<v-btn
|
||||
icon="mdi-cog-outline"
|
||||
@click="showSettings"
|
||||
></v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<v-progress-linear
|
||||
@ -38,13 +43,16 @@
|
||||
|
||||
<v-main>
|
||||
<v-container fluid class="ma-0 pa-0">
|
||||
<transition name="fade">
|
||||
<router-view
|
||||
ref="view"
|
||||
@authorSelected="loadBooks"
|
||||
@bookSelected="loadBook"
|
||||
/>
|
||||
</transition>
|
||||
<router-view v-slot="{ Component }"
|
||||
ref="view"
|
||||
@authorSelected="loadBooks"
|
||||
@bookSelected="loadBook"
|
||||
@loadChapter="loadChapter"
|
||||
>
|
||||
<transition name="fade">
|
||||
<component :is="Component"/>
|
||||
</transition>
|
||||
</router-view>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
@ -54,20 +62,21 @@
|
||||
<v-list-item height="32" min-height="32" class="pb-0 pt-8">
|
||||
<v-slider
|
||||
v-model="progress"
|
||||
color="blue"
|
||||
min="0"
|
||||
max="100"
|
||||
dense
|
||||
hide-details
|
||||
:disabled="isOff"
|
||||
:inverse-label="true"
|
||||
:label="`${currentTime ? currentTime : '00:00:00'}/${duration ? duration : '00:00:00'}`"
|
||||
class="time-slider w-100 px-2 py-0 m-0"
|
||||
></v-slider>
|
||||
>
|
||||
<template v-slot:append>{{ currentTime ? currentTime : "00:00"}}/{{duration ? duration : "00:00"}}</template>
|
||||
</v-slider>
|
||||
</v-list-item>
|
||||
<v-list-item class="py-0">
|
||||
<v-list-item-content v-if="smAndUp">
|
||||
<v-list-item-title>{{ authorName }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ bookTitle }}<span v-if="currentChapter !== ''"> - {{ currentChapter }}</span></v-list-item-subtitle>
|
||||
<v-list-item-title>{{ $store.getters.book.author }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ $store.getters.book.title }}<span v-if="$store.getters.chapters.length > 0 && currentChapter !== null && $store.getters.chapters[currentChapter].title !== ''"> - {{ $store.getters.chapters[currentChapter].title }}</span></v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
@ -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 @@
|
||||
</v-fab-transition>
|
||||
-->
|
||||
<message-box ref="MessageBox"/>
|
||||
<settings-dialog ref="SettingsDialog"/>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -146,5 +157,5 @@
|
||||
background: transparent
|
||||
|
||||
.v-list-item.no-selection-pl
|
||||
padding-left: 74px
|
||||
padding-left: 73px
|
||||
</style>
|
||||
|
||||
60
src/components/SettingsDialog.vue
Normal file
60
src/components/SettingsDialog.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<v-layout row justify-center>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
persistent
|
||||
width="350"
|
||||
>
|
||||
<v-card style="width: 350px">
|
||||
<v-card-title class="headline">{{ $t("settings") }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-switch
|
||||
v-model="darkTheme"
|
||||
color="blue"
|
||||
:label="$t('darkTheme')"
|
||||
></v-switch>
|
||||
<div class="mb-7 text-caption">{{ $t("playbackRate") }}</div>
|
||||
<v-slider
|
||||
v-model="playbackRate"
|
||||
color="blue"
|
||||
label="color"
|
||||
min="0.5"
|
||||
max="2"
|
||||
step="0.5"
|
||||
show-ticks="always"
|
||||
thumb-label="always"
|
||||
tick-size="4"
|
||||
></v-slider>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" flat @click="dialog = false">Ok</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
dialog: false,
|
||||
darkTheme: false,
|
||||
playbackRate: 1
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialog (val) {
|
||||
if (val) {
|
||||
this.darkTheme = this.$store.getters.darkTheme;
|
||||
this.playbackRate = this.$store.getters.playbackRate;
|
||||
}
|
||||
},
|
||||
|
||||
darkTheme (val) { this.$store.commit("setDarkTheme", val); },
|
||||
playbackRate (val) { this.$store.commit("setPlaybackRate", val); }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -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;
|
||||
|
||||
@ -2,8 +2,13 @@ const messages = {
|
||||
appName: "Аудиотека",
|
||||
authors: "Авторы",
|
||||
book: "Книга",
|
||||
bookmarkSaved: "Закладка сохранена",
|
||||
books: "Книги",
|
||||
cycle: "Цикл"
|
||||
cycle: "Цикл",
|
||||
darkTheme: "Тёмная тема",
|
||||
information: "Информация",
|
||||
playbackRate: "Скорость воспроизведения",
|
||||
settings: "Настройки"
|
||||
}
|
||||
|
||||
export default messages;
|
||||
|
||||
33
src/store.js
33
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; },
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user