Compare commits

...

16 Commits

Author SHA1 Message Date
b1005c05d4 Merge pull request 'v2' (#1) from v2 into master
Reviewed-on: https://www.cainet.info/git/cai/AudioLib/pulls/1
2022-12-26 02:11:37 +03:00
c25a2c16dd Bugfixes, minor changes in settings interface.
Bugfixes, minor changes in settings interface, node modules updated
2022-12-26 02:05:56 +03:00
8367d4c883 NodeJS packages updated
NodeJS packages updated
2022-10-30 23:10:50 +03:00
Alexander I. Chebykin
0f344e56c3 Node packages updated
Node packages updated
2022-08-23 14:58:21 +03:00
ff2ac709d2 Node.js packages updated
Node.js packages updated
2022-07-12 00:25:07 +03:00
52fa300d0c Node modules updated. Minor ui changes.
Node modules updated. Minor ui changes.
2022-06-24 00:16:33 +03:00
5ed9e61644 Node modules updated
Node modules updated
2022-05-01 20:14:59 +03:00
d3f9f32188 Yarn upgraded to v.2
Yarn upgraded to v.2
2022-05-01 20:11:10 +03:00
7df8646bc4 Node modules updated
Node modules updated
2022-05-01 20:03:06 +03:00
0e737a81bd List selection fix
List selection fix
2022-04-20 01:52:06 +03:00
7efae6315c Last played books saving added
Last played books saving added
2022-04-20 01:22:10 +03:00
67dff194d2 Some styling
Some styling
2022-04-19 02:02:20 +03:00
b07aaf38b9 bugfix: auto-bookmark setting was not saved
bugfix: auto-bookmark setting was not saved
2022-04-19 01:22:27 +03:00
3341c5df68 Migration from v1 completed
Migration from v1 completed
2022-04-19 01:07:35 +03:00
04f0ccc13c Audio part is added
Audio part is added
2022-04-18 01:38:16 +03:00
4f27a90e30 Started migration to Vue3 + Vuetify3
Started migration to Vue3 + Vuetify3
2022-04-16 02:45:35 +03:00
37 changed files with 12579 additions and 10017 deletions

View File

@ -1,2 +0,0 @@
> 1%
last 2 versions

View File

@ -1,17 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/essential',
'eslint:recommended'
],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
},
parserOptions: {
parser: 'babel-eslint'
}
}

13
.gitignore vendored
View File

@ -1,7 +1,8 @@
.DS_Store
node_modules
/dist
/public/lib/*
/public/lib
/out/
# local env files
.env.local
@ -11,6 +12,7 @@ node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
@ -20,3 +22,12 @@ yarn-error.log*
*.njsproj
*.sln
*.sw?
# Yarn 2
.yarn/*
#!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

File diff suppressed because one or more lines are too long

785
.yarn/releases/yarn-3.2.0.cjs vendored Normal file

File diff suppressed because one or more lines are too long

7
.yarnrc.yml Normal file
View File

@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools"
yarnPath: .yarn/releases/yarn-3.2.0.cjs

View File

@ -1,4 +1,4 @@
# audiobookslibrary
# audiolib.vue3
## Project setup
```
@ -7,22 +7,17 @@ yarn install
### Compiles and hot-reloads for development
```
yarn run serve
yarn serve
```
### Compiles and minifies for production
```
yarn run build
```
### Run your tests
```
yarn run test
yarn build
```
### Lints and fixes files
```
yarn run lint
yarn lint
```
### Customize configuration
@ -54,12 +49,14 @@ file format:
```
[
{
"cycle": "Books cycle",
"title": "Book 1 title",
"image": "lib/Author name/Book 1 title/cover.jpg",
"book": "lib/Author name/Book 1 title/book.json"
},
...
{
"cycle": "Books cycle",
"title": "Book N title",
"image": "lib/Author name/Book N title/cover.jpg",
"book": "lib/Author name/Book N title/book.json"
@ -82,4 +79,4 @@ file format:
"title": "Chapter N title"
}
]
```
```

View File

@ -1,5 +1,5 @@
module.exports = {
presets: [
'@vue/app'
'@vue/cli-plugin-babel/preset'
]
}

19
jsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

View File

@ -1,6 +1,6 @@
{
"name": "audiobookslibrary",
"version": "0.2.0",
"name": "audiolib.vue3",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@ -8,30 +8,52 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"@mdi/font": "^3.6.95",
"axios": "^0.19.0",
"core-js": "^2.6.5",
"crypto-js": "^3.1.9-1",
"register-service-worker": "^1.6.2",
"sass": "^1.49.11",
"sass-loader": "^10",
"vue": "^2.6.10",
"vue-axios": "^2.1.4",
"vue-i18n": "^8.11.2",
"vuetify": "^1.5.5"
"@mdi/font": "7.1.96",
"axios": "^1.2.1",
"core-js": "^3.27.0",
"crypto-js": "^4.1.1",
"roboto-fontface": "^0.10.0",
"vue": "^3.2.45",
"vue-axios": "^3.5.2",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.6",
"vuetify": "^3.0.6",
"vuex": "^4.1.0",
"webfontloader": "^1.6.28",
"webpack-plugin-vuetify": "^2.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.8.0",
"@vue/cli-plugin-eslint": "^3.8.0",
"@vue/cli-plugin-pwa": "^3.8.0",
"@vue/cli-service": "^3.8.0",
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"vue-cli-plugin-vuetify": "^0.5.0",
"vue-template-compiler": "^2.6.10",
"vuetify-loader": "^1.0.5"
}
"@babel/core": "^7.20.7",
"@babel/eslint-parser": "^7.19.1",
"@vue/cli-plugin-babel": "~5.0.8",
"@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-service": "~5.0.8",
"eslint": "^8.30.0",
"eslint-plugin-vue": "^9.8.0",
"sass": "^1.57.1",
"sass-loader": "^13.2.0",
"vue-cli-plugin-vuetify": "~2.5.8",
"vuetify-loader": "^2.0.0-alpha.9"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "@babel/eslint-parser"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"not ie 11"
],
"packageManager": "yarn@3.2.0"
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}

View File

@ -1,4 +0,0 @@
{
"AppNet": "CAINet",
"AppName": "Аудиотека"
}

3
public/data/config.json Normal file
View File

@ -0,0 +1,3 @@
{
"appNet": "CAINet"
}

BIN
public/img/library1080.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

View File

@ -1,18 +1,17 @@
<!DOCTYPE html>
<html lang="ru">
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>img/favicon.ico">
<title>CAINet: Аудиотека</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>Извините, данное приложение не работает без JavaScript.</strong><br>
<strong>You need JavaScript to run this application.</strong>
<hr>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->

View File

@ -1,2 +0,0 @@
User-agent: *
Disallow:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
<template>
<v-layout row justify-center>
<v-dialog
v-model="dialog"
persistent
scrollable
max-width="600px"
>
<v-card>
<v-card-title class="headline">{{ $t("about") }}</v-card-title>
<v-card-text>
<template v-if="locale == 'ru'">
<p class="text-center">Аудиотека</p>
<p class="text-center">&copy; Александр Чебыкин, 2019-2022</p>
<p class="text-center">Опубликовано под лицензией <a href="https://opensource.org/licenses/MIT" target="_blank">MIT license</a></p>
<p class="text-center">Git: <a href="https://www.cainet.info/git/cai/AudioLib" target="_blank">https://home.cainet.info/git/cai/AudioLib</a></p>
</template>
<template v-else>
<p class="text-center">Audiobooks library</p>
<p class="text-center">&copy; Alexander I Chebykin, 2019-2022</p>
<p class="text-center">Published under <a href="https://opensource.org/licenses/MIT" target="_blank">MIT license</a></p>
<p class="text-center">Git: <a href="https://www.cainet.info/git/cai/AudioLib" target="_blank">https://www.cainet.info/git/cai/AudioLib</a></p>
</template>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn flat variant="outlined" color="info" @click="dialog = false">Ok</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-layout>
</template>
<script>
export default {
name: "AboutDialog",
data () {
return {
dialog: false,
locale: "en",
}
},
watch: {
dialog (val) {
if (val) {
let userLocale = navigator.language || navigator.userLanguage;
userLocale = userLocale.indexOf("-") > 0
? userLocale.substring(0, userLocale.indexOf("-")).toLowerCase()
: userLocale.toLowerCase();
this.locale = userLocale;
}
},
darkTheme (val) { this.$store.commit("setDarkTheme", val); },
}
}
</script>

View File

@ -1,71 +0,0 @@
<template>
<div>
<v-card flat>
<v-container
fluid
grid-list-lg
pb-5
class="authors-container"
>
<v-layout row wrap pb-5>
<v-flex
v-for="(item, index) in items"
v-bind:key="index"
xs12 sm5 md3 lg2
my-1
>
<v-card
hover
ripple
@click="authorClick(item)"
>
<v-layout>
<v-flex xs5>
<v-img
:src="item.image"
height="95px"
width="95px"
contain
class="author-image ml-2"
>
</v-img>
</v-flex>
<v-flex xs7>
<v-card-title>
<div>
<div>{{ item.author }}</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-card>
</div>
</template>
<script>
export default {
data () {
return {
items: []
}
},
methods: {
authorClick (val) { this.$emit("author_selected", val); }
}
}
</script>
<style lang="sass" scoped>
.author-image
border-radius: 125px
border: solid #fff 2px
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)
.authors-container
min-height: calc(100vh - 190px)
</style>

View File

@ -0,0 +1,90 @@
<template>
<v-card
flat
tile
color="rgba(0, 0, 0, 0)"
class="d-flex align-content-start flex-wrap mx-0 pt-2 px-2"
>
<v-row no-gutters>
<v-col
v-for="(item, index) in $store.getters.authors"
:key="index"
cols="12"
sm="6"
md="4"
lg="3"
xl="2"
>
<v-card
hover
ripple
outlined
tile
height="127"
class="pa-2 ma-2 semi-transparent"
@click="authorClick(item)"
>
<v-row
no-gutters
style="flex-wrap: nowrap;"
>
<v-col
cols="5"
style="min-width: 127px; max-width: 127px;"
class="flex-grow-0 flex-shrink-0"
>
<v-img
:src="item.image"
height="95px"
width="95px"
contain
class="author-image ma-2"
>
</v-img>
</v-col>
<v-col
cols="1"
class="flex-grow-1 flex-shrink-0"
style="min-width: 80px; max-width: 100%"
>
<div class="author-name">
<span class=" text-center font-weight-medium">{{ item.author }}</span>
</div>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
</v-card>
</template>
<script>
export default {
name: "AuthorsLib",
data: () => ({
}),
methods: {
authorClick (author) {
this.$emit("authorSelected", author, true);
}
}
}
</script>
<style lang="sass" scoped>
.author
&-image
border-radius: 125px
border: solid #fff 2px
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)
&-name
height: 100%
line-height: 100px
text-align: center
> span
display: inline-block
vertical-align: middle
line-height: normal
</style>

View File

@ -1,228 +0,0 @@
<template>
<v-card flat>
<v-container bg fill-height grid-list-lg pb-5>
<v-layout row wrap pb-5 class="book-layout">
<v-flex xs12 sm6 lg4>
<v-card flat class="text-xs-center">
<v-layout d-inline-block mb-2>
<div class="d-inline-block f-left">
<v-img
v-if="authorImg !== ''"
:src="authorImg"
height="55px"
width="55px"
contain
class="author-image ml-2"
>
</v-img>
</div>
<div class="d-inline-block">
<v-card-title>
<div>{{ authorName }}</div>
</v-card-title>
</div>
</v-layout>
</v-card>
<v-flex xs12 text-xs-center>
<v-img
:src="bookImg"
height="200px"
width="200px"
contain
class="d-inline-block ml-2"
>
</v-img>
</v-flex>
<v-flex xs12 text-xs-center>{{ bookTitle }}</v-flex>
<v-flex xs12 v-if="wikiText !== ''">
<v-expansion-panel>
<v-expansion-panel-content>
<template v-slot:header>
<div>Wikipedia</div>
</template>
<v-card>
<v-card-text>{{ wikiText }}</v-card-text>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</v-flex>
</v-flex>
<v-flex xs12 sm6>
<v-list>
<v-list-tile
v-for="(item, index) in items"
v-bind:key="index"
@click="loadChapter(index, 0, true)"
>
<v-list-tile-action>
<v-icon
v-if="index == currentChapter"
>mdi-play</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-text="item.title"></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-flex>
</v-layout>
</v-container>
</v-card>
</template>
<script>
export default {
data () {
return {
audioElement: null,
authorName: "",
authorImg: "",
bookImg: "",
bookTitle: "",
currentChapter: 0,
currentTime: "00:00",
duration: "00:00",
items: [],
progress: 0,
status: 0, //this.statuses.stopped,
volume: 5,
wikiText: ""
}
},
computed: {
isOff () {
return this.audioElement == null;
},
isPaused () {
return this.status === this.statuses.paused;
},
isPlaying () {
return this.status === this.statuses.playing;
},
isChapterLoaded () {
return (this.currentChapter !== null) && this.audioElement;
}
},
watch: {
//volume (val) { this.updateVolume(); }
volume () { this.updateVolume(); }
},
methods: {
loadChapter (index = 0, progress = 0, autoplay = false) {
if (this.audioElement) { this.audioElement.pause(); }
if (index >= this.items.length) { return false; } // show message?
this.currentChapter = index;
this.audioElement = new Audio(encodeURI(this.items[index].url));
this.updateVolume();
this.status = this.statuses.stopped;
this.audioElement.addEventListener("ended", this.loadNextChapter);
this.audioElement.ontimeupdate = this.updateProgress;
let bookInstance = this;
this.audioElement.addEventListener("durationchange", function () {
let min = parseInt(bookInstance.audioElement.duration / 60) < 10
? "0" + parseInt(bookInstance.audioElement.duration / 60)
: parseInt(bookInstance.audioElement.duration / 60);
let sec = parseInt(bookInstance.audioElement.duration % 60) < 10
? "0" + parseInt(bookInstance.audioElement.duration % 60)
: parseInt(bookInstance.audioElement.duration % 60);
bookInstance.duration = min + ":" + sec;
});
this.audioElement.addEventListener("canplay", function _listener () {
bookInstance.audioElement.currentTime = bookInstance.audioElement.duration * progress / 100;
bookInstance.audioElement.removeEventListener("canplay", _listener, true);
}, true);
if (autoplay) this.play();
},
loadNextChapter (autoplay = true) {
this.currentChapter++;
if (this.currentChapter >= this.items.length) {
this.currentChapter--;
return false;
}
this.loadChapter(this.currentChapter, 0, autoplay);
},
nextChapter () {
if (this.currentChapter < this.items.length - 1) {
this.currentChapter++;
this.loadChapter(this.currentChapter, 0, true);
}
},
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);
}
},
toggleStatus () {
if (!this.isChapterLoaded) {
this.loadChapter(this.currentChapter || 0);
}
if (!this.isPlaying) {
this.play();
} else {
this.pause();
}
},
updateProgress () {
if (!this.audioElement || !this.audioElement.currentTime) {
return this.progress = 0;
}
this.progress = (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;
}
}
}
</script>
<style lang="sass" scoped>
.author-image
display: inline-block
border-radius: 55px
.book-layout
min-height: calc(100vh - 190px)
.f-left
float: left
</style>

View File

@ -0,0 +1,178 @@
<template>
<v-card
flat
tile
color="rgba(0, 0, 0, 0)"
class="d-flex align-content-start flex-wrap mx-0 pa-2"
>
<v-row
no-gutters
class="justify-center"
>
<v-col
style="min-width: 316px; max-width: 316px;"
cols="12"
sm="6"
md="5"
lg="4"
>
<v-card
flat
tile
width="298"
height="127"
color="rgba(0, 0, 0, 0)"
class="pa-2 ma-2 d-flex"
v-if="$store.getters.book.author.trim() !== ''"
>
<v-row
no-gutters
style="flex-wrap: nowrap;"
>
<v-col cols="5">
<v-img
:src="$store.getters.book.photo"
height="95px"
width="95px"
contain
class="author-image ma-2"
>
</v-img>
</v-col>
<v-col cols="7" class="px-2">
<div class="author-name">
<span class="font-weight-medium">{{ $store.getters.book.author }}</span>
</div>
</v-col>
<v-divider class="mt-4"></v-divider>
</v-row>
</v-card>
<v-row no-gutters>
<v-col cols="12" class="d-flex justify-center" v-if="$store.getters.book.cover.trim() !== ''">
<v-img
:src="$store.getters.book.cover"
height="200px"
width="200px"
contain
class="d-inline-block ml-2"
>
</v-img>
</v-col>
<v-col
cols="12"
class="d-flex justify-center mt-2"
v-if="$store.getters.book.cycle !== undefined && $store.getters.book.cycle.trim() !== ''"
>{{ $t("cycle") }} &laquo;{{ $store.getters.book.cycle }}&raquo;</v-col>
<v-col cols="12" class="d-flex justify-center mt-2" v-if="$store.getters.book.title.trim() !== ''">{{ $store.getters.book.title }}</v-col>
<v-col
cols="12"
class="d-flex justify-center mt-2"
v-if="$store.getters.book.reader !== undefined &&$store.getters.book.reader.trim() !== ''"
>{{ $t("reader") }} {{ $store.getters.book.reader }}</v-col>
<v-col cols="12" class="d-flex justify-center mt-4" v-if="$store.getters.book.wiki.trim() !== ''">
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>
Wikipedia
</v-expansion-panel-title>
<v-expansion-panel-text>
{{ $store.getters.book.wiki }}
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
</v-row>
</v-col>
<v-col
cols="12"
sm="8"
md="7"
lg="6"
class="flex-grow-1 flex-shrink-0 pa-2"
v-if="illustrated"
>
2
</v-col>
<v-col
cols="12"
sm="7"
:md="illustrated ? 3 : 6"
:lg="illustrated ? 2 : 5"
class="flex-grow-1 flex-shrink-0 pa-2"
>
<v-list
class="transparent-bg"
:class="{ 'limit-max-width': xs }"
:selected="[currentChapter]"
>
<v-list-item
v-for="(item, index) in $store.getters.chapters"
:key="index"
:prepend-icon="index == currentChapter ? 'mdi-play' : ''"
:title='item.title'
:class="index !== currentChapter ? 'no-selection-pl' : ''"
@click="loadChapter(index, 0, true)"
></v-list-item>
</v-list>
</v-col>
</v-row>
</v-card>
</template>
<script>
import { useDisplay } from "vuetify";
export default {
name: "BookReader",
setup () {
const { smAndDown, smAndUp, xs, md, lg, xl } = useDisplay();
return {
smAndDown, smAndUp, xs, md, lg, xl
}
},
data: () => ({
illustrated: false,
}),
computed: {
currentChapter: function () {
return this.$store.getters.curChapter;
}
},
methods: {
loadChapter (index = 0, progress = 0, autoplay = false) {
this.$emit("loadChapter", index, progress, autoplay);
}
}
}
</script>
<style lang="sass" scoped>
.author
&-image
border-radius: 125px
border: solid #fff 2px
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)
&-name
height: 100%
line-height: 100px
> span
display: inline-block
vertical-align: middle
line-height: normal
.transparent-bg
background: transparent
.v-list-item.no-selection-pl
padding-left: 73px
.v-list.limit-max-width
.v-list-item
max-width: calc(100vw - 50px)
</style>

View File

@ -1,111 +0,0 @@
<template>
<v-card flat>
<v-container bg grid-list-md>
<v-layout row wrap align-top>
<v-flex>
<v-img
v-if="authorImg !== ''"
:src="authorImg"
height="55px"
width="55px"
contain
class="author-image ml-2"
>
</v-img>
<div
class="author-name font-weight-black"
>
{{ authorName }}
</div>
</v-flex>
<v-flex xs12 sm6 v-if="wikiText !== ''">
<v-expansion-panel>
<v-expansion-panel-content>
<template v-slot:header>
<div>Wikipedia</div>
</template>
<v-card>
<v-card-text>{{ wikiText }}</v-card-text>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</v-flex>
</v-layout>
</v-container>
<v-container
fluid
fill-height
grid-list-lg
pb-5
>
<v-layout row wrap pb-5 class="books-layout">
<v-flex
v-for="(item, index) in items"
v-bind:key="index"
xs12 sm6 md4 lg3
my-1
>
<v-card
hover
ripple
@click="bookClick(item)"
>
<v-layout>
<v-flex xs5>
<v-img
:src="item.image"
height="95px"
width="95px"
contain
class="ml-2"
>
</v-img>
</v-flex>
<v-flex xs7>
<v-card-title>
<div>
<div>{{ item.title }}</div>
</div>
</v-card-title>
</v-flex>
</v-layout>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-card>
</template>
<script>
export default {
data () {
return {
authorName: "",
authorImg: "",
items: [],
wikiText: ""
}
},
methods: {
bookClick (val) {
this.$emit("book_selected", val);
}
}
}
</script>
<style lang="sass" scoped>
.author-image
display: inline-block
border-radius: 55px
.author-name
display: inline-block
position: absolute
margin: 15px
.books-layout
min-height: calc(100vh - 250px)
</style>

190
src/components/BooksLib.vue Normal file
View File

@ -0,0 +1,190 @@
<template>
<v-card
flat
color="rgba(0, 0, 0, 0)"
class="mx-0 pt-2 px-2"
>
<v-row
no-gutters
v-if="$store.getters.author.name.trim() !== ''"
>
<v-col
style="min-width: 316px; max-width: 316px;"
cols="12"
sm="6"
md="4"
lg="3"
xl="2"
>
<v-card
flat
tile
width="298"
height="127"
color="rgba(0, 0, 0, 0)"
class="pa-2 ma-2 d-flex"
>
<v-row
no-gutters
style="flex-wrap: nowrap;"
>
<v-col cols="5">
<v-img
:src="$store.getters.author.image"
height="95px"
width="95px"
contain
class="author-image ma-2"
>
</v-img>
</v-col>
<v-col cols="7" class="px-2">
<div class="author-name">
<span class="font-weight-medium">{{ $store.getters.author.name }}</span>
</div>
</v-col>
</v-row>
</v-card>
</v-col>
<v-col
cols="12"
sm="1"
style="min-width: 100px; max-width: 100%;"
class="flex-grow-1 flex-shrink-0 pa-2"
v-if="$store.getters.author.wiki.trim() !== ''"
>
<v-card class="pa-0 ma-0">
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>
Wikipedia
</v-expansion-panel-title>
<v-expansion-panel-text>
{{ $store.getters.author.wiki }}
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card>
</v-col>
</v-row>
<v-divider class="my-4"/>
<v-card
v-for="(cycle, cycleName) in $store.getters.books"
:key="cycleName"
flat
tile
color="rgba(0, 0, 0, 0)"
class="d-flex align-content-start flex-wrap"
>
<v-card-title class="w-100" v-if="cycleName !== 'no_cycle'">{{ $t("cycle") }} &laquo;{{ cycleName }}&raquo;</v-card-title>
<v-card-text class="d-flex align-content-start flex-wrap pa-0 ma-0">
<v-row no-gutters>
<v-col
v-for="(item, index) in cycle"
:key="index"
cols="12"
sm="6"
md="4"
lg="3"
xl="2"
>
<v-card
hover
ripple
outlined
tile
height="127"
class="pa-2 ma-2 semi-transparent"
@click="bookClick(item)"
>
<v-row
no-gutters
style="flex-wrap: nowrap;"
>
<v-col
cols="5"
style="min-width: 127px; max-width: 127px;"
class="flex-grow-0 flex-shrink-0"
>
<v-img
:src="item.image"
height="95px"
width="95px"
contain
class="book-image ma-2"
>
</v-img>
</v-col>
<v-col
cols="1"
class="flex-grow-1 flex-shrink-0"
style="min-width: 80px; max-width: 100%"
>
<div class="book-title">
<span class=" text-center font-weight-medium">{{ item.title }}</span>
</div>
</v-col>
</v-row>
</v-card>
</v-col>
</v-row>
</v-card-text>
<v-divider class="my-4"/>
</v-card>
</v-card>
</template>
<script>
import { useDisplay } from "vuetify";
export default {
name: "BooksLib",
setup () {
const { smAndDown, smAndUp, xs, md, lg, xl } = useDisplay();
return {
smAndDown, smAndUp, xs, md, lg, xl
}
},
data: () => ({
}),
methods: {
bookClick (book) {
this.$emit("bookSelected", book, true);
}
}
}
</script>
<style lang="sass" scoped>
.book
&-image
border-radius: 5px
border: none
&-title
height: 100%
line-height: 100px
text-align: center
> span
display: inline-block
vertical-align: middle
line-height: normal
.author
&-image
border-radius: 125px
border: solid #fff 2px
box-shadow: 0px 3px 1px -2px rgba(0,0,0,0.2), 0px 2px 2px 0px rgba(0,0,0,0.14), 0px 1px 5px 0px rgba(0,0,0,0.12)
&-name
height: 100%
line-height: 100px
> span
display: inline-block
vertical-align: middle
line-height: normal
</style>

View File

@ -7,7 +7,7 @@
>
<v-card>
<v-card-title class="headline">{{ title }}</v-card-title>
<v-card-text v-html="text"></v-card-text>
<v-card-text>{{ text }}</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" flat @click="dialog = false">Ok</v-btn>
@ -19,11 +19,13 @@
<script>
export default {
name: "MessageDialog",
data () {
return {
dialog: false,
title: "",
text: ""
title: "",
text: ""
}
}
}

View File

@ -0,0 +1,89 @@
<template>
<v-layout row justify-center>
<v-dialog
v-model="dialog"
persistent
scrollable
max-width="600px"
>
<v-card>
<v-card-title class="headline">{{ $t("settings") }}</v-card-title>
<v-card-text>
<v-switch
v-model="darkTheme"
color="blue"
:label="$t('darkTheme')"
class="ml-3"
></v-switch>
<v-switch
v-model="autoBookmark"
color="blue"
:label="$t('autoBookmarks')"
class="ml-3"
></v-switch>
<v-switch
v-model="autoLoadBooks"
color="blue"
:label="$t('autoLoadBooks')"
class="ml-3"
></v-switch>
<div class="mb-7 text-body-1">{{ $t("playbackRate") }}</div>
<v-slider
v-model="playbackRate"
color="blue"
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 flat variant="outlined" color="info" @click="dialog = false">Ok</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-layout>
</template>
<script>
export default {
name: "SettingsDialog",
data () {
return {
dialog: false,
autoBookmark: false,
autoLoadBooks: false,
darkTheme: false,
locale: "en",
playbackRate: 1,
}
},
watch: {
dialog (val) {
if (val) {
this.autoBookmark = this.$store.getters.autoBookmark;
this.autoLoadBooks = this.$store.getters.autoLoadBooks;
this.darkTheme = this.$store.getters.darkTheme;
this.playbackRate = this.$store.getters.playbackRate;
let userLocale = navigator.language || navigator.userLanguage;
userLocale = userLocale.indexOf("-") > 0
? userLocale.substring(0, userLocale.indexOf("-")).toLowerCase()
: userLocale.toLowerCase();
this.locale = userLocale;
}
},
autoBookmark (val) { this.$store.commit("setAutoBookmark", val); },
autoLoadBooks (val) { this.$store.commit("setAutoLoadBooks", val); },
darkTheme (val) { this.$store.commit("setDarkTheme", val); },
playbackRate (val) { this.$store.commit("setPlaybackRate", val); }
}
}
</script>

View File

@ -1,35 +1,54 @@
/**
* Audiobooks library
*
* @author Alexander I. Chebykin <alex.chebykin@gmail.com>
* @copyright 2018-2019 Alexander I. Chebykin
* @license https://opensource.org/licenses/MIT MIT
* @version 0.9
*/
import { createApp } from "vue";
import App from "./App.vue";
import axios from "axios";
import vueAxios from "vue-axios";
import { createRouter, createWebHashHistory } from "vue-router";
import routes from "./routes";
import store from "./store";
import vuetify from "./plugins/vuetify";
import { loadFonts } from "./plugins/webfontloader";
import i18n from "./plugins/i18n";
import "@mdi/font/css/materialdesignicons.css"
import Vue from "vue"
import Axios from "axios"
import VueAxios from "vue-axios"
import "./plugins/vuetify"
import i18n from "@/plugins/i18n";
import App from "./App.vue"
import "./registerServiceWorker"
loadFonts();
Vue.config.productionTip = false
const router = createRouter({
history: createWebHashHistory(), //createWebHistory(),
routes
});
Vue.use(VueAxios, Axios)
const app = createApp(App);
/**
* Audioplayer statuses
* Audio player statuses
*/
Vue.prototype.statuses = {
app.config.globalProperties.statuses = {
"stopped": 0,
"paused": 1,
"paused": 1,
"playing": 2
};
new Vue({
i18n,
render: h => h(App),
}).$mount("#app")
/**
* App tabs
*/
app.config.globalProperties.tabs = {
"authors": 0,
"books": 1,
"book": 2
};
app.config.globalProperties.defaultBackground = "img/library1080.webp";
fetch(process.env.BASE_URL + "data/config.json")
.then((response) => response.json())
.then((config) => {
app.config.globalProperties.appConfig = config;
app
.use(vueAxios, axios)
.use(vuetify)
.use(i18n)
.use(router)
.use(routes)
.use(store)
.mount("#app")
});

View File

@ -1,45 +1,16 @@
import Vue from "vue"
import VueI18n from "vue-i18n"
import { createI18n } from "vue-i18n";
Vue.use(VueI18n)
import en from "./locales/en.js";
import ru from "./locales/ru.js";
const messages = {
"en": {
about: "About",
authors: "Authors",
autoBookmarks: "Autobookmarks",
book: "Book",
bookmarkSaved: "Bookmark saved",
bookmarkRemoved: "Bookmark removed",
books: "Books",
darkTheme: "Dark theme",
error: "Error",
information: "Information",
removeBookmark: "Remove bookmark",
setBookmark: "Set bookmark",
useWikipedia: "Use Wikipedia"
},
"ru": {
about: "О приложении",
authors: "Авторы",
autoBookmarks: "Автозакладки",
book: "Книга",
bookmarkSaved: "Закладка сохранена",
bookmarkRemoved: "Закладка удалена",
books: "Книги",
darkTheme: "Тёмная тема",
error: "Ошибка",
information: "Информация",
removeBookmark: "Удалить закладку",
setBookmark: "Установить закладку",
useWikipedia: "Использовать Wikipedia"
}
en: en,
ru: ru
}
const i18n = new VueI18n({
locale: "ru", // set locale
fallbackLocale: "en", // set fallback locale
messages, // set locale messages
})
export default i18n
export default new createI18n({
legacy: false, // Vuetify does not support the legacy mode of vue-i18n
locale: "ru",
fallbackLocale: "en",
messages,
});

18
src/plugins/locales/en.js Normal file
View File

@ -0,0 +1,18 @@
const messages = {
appName: "AudioLib",
about: "About",
authors: "Authors",
autoBookmarks: "Auto-bookmarks",
autoLoadBooks: "Auto-load last opened books",
book: "Books",
bookmarkSaved: "Bookmark saved",
books: "Book",
cycle: "Cycle",
darkTheme: "DarkTheme",
information: "Information",
playbackRate: "Playback rate",
reader: "Reader",
settings: "Settings"
}
export default messages;

18
src/plugins/locales/ru.js Normal file
View File

@ -0,0 +1,18 @@
const messages = {
appName: "Аудиотека",
about: "О программе",
authors: "Авторы",
autoBookmarks: "Автозакладки",
autoLoadBooks: "Автоматически загружать последние открытые книги",
book: "Книга",
bookmarkSaved: "Закладка сохранена",
books: "Книги",
cycle: "Цикл",
darkTheme: "Тёмная тема",
information: "Информация",
playbackRate: "Скорость воспроизведения",
reader: "Читает",
settings: "Настройки"
}
export default messages;

View File

@ -1,7 +1,17 @@
import Vue from "vue";
import Vuetify from "vuetify/lib";
import "vuetify/src/stylus/app.styl";
// Styles
import "@mdi/font/css/materialdesignicons.css";
import "vuetify/styles";
import { createVueI18nAdapter } from "vuetify/locale/adapters/vue-i18n";
import { useI18n } from "vue-i18n";
import i18n from "./i18n.js";
Vue.use(Vuetify, {
iconfont: "md",
// Vuetify
import { createVuetify } from "vuetify";
export default createVuetify({
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
locale: createVueI18nAdapter({
i18n,
useI18n,
})
});

View File

@ -0,0 +1,15 @@
/**
* plugins/webfontloader.js
*
* webfontloader documentation: https://github.com/typekit/webfontloader
*/
export async function loadFonts () {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */"webfontloader");
webFontLoader.load({
google: {
families: ["Roboto:100,300,400,500,700,900&display=swap"],
},
});
}

View File

@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { register } from "register-service-worker"
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
@ -8,25 +8,31 @@ if (process.env.NODE_ENV === "production") {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB"
)
);
},
registered () {
console.log("Service worker has been registered.")
console.log("Service worker has been registered.");
},
cached () {
console.log("Content has been cached for offline use.")
console.log("Content has been cached for offline use.");
},
updatefound () {
console.log("New content is downloading.")
console.log("New content is downloading.");
},
updated () {
console.log("New content is available; please refresh.")
console.log("New content is available; please refresh.");
},
offline () {
console.log("No internet connection found. App is running in offline mode.")
console.log("No internet connection found. App is running in offline mode.");
},
error (error) {
console.error("Error during service worker registration:", error)
console.error("Error during service worker registration:", error);
}
})
});
}

12
src/routes.js Normal file
View File

@ -0,0 +1,12 @@
import AuthorsLib from "./components/AuthorsLib";
import BookReader from "./components/BookReader";
import BooksLib from "./components/BooksLib";
const routes = [
{ path: '', redirect: '/authors' },
{ path: '/authors', component: AuthorsLib },
{ path: '/books', component: BooksLib },
{ path: '/book', component: BookReader },
];
export default routes;

56
src/store.js Normal file
View File

@ -0,0 +1,56 @@
import { createStore } from "vuex";
const store = createStore({
state () {
return {
author: {
name: "",
image: "",
wiki: ""
},
authors: [],
autoBookmark: false,
autoLoadBooks: false,
book: {
author: "",
photo: "",
title: "",
cover: "",
wiki: ""
},
books: [],
chapters: [],
curChapter: 0,
darkTheme: false,
playbackRate: 1,
}
},
getters: {
author: state => { return state.author; },
authors: state => { return state.authors; },
autoBookmark: state => { return state.autoBookmark; },
autoLoadBooks: state => { return state.autoLoadBooks; },
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: {
setAuthor (state, payload) { state.author = payload; },
setAuthors (state, payload) { state.authors = payload; },
setAutoBookmark (state, payload) { state.autoBookmark = payload; },
setAutoLoadBooks (state, payload) { state.autoLoadBooks = 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; },
}
});
export default store;

View File

@ -1,5 +1,15 @@
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/audiobooks/'
: '/'
}
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
publicPath: process.env.NODE_ENV === "production"
? "/audiobooks.next/"
: "/",
pluginOptions: {
vuetify: {
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vuetify-loader
}
}
});

18685
yarn.lock

File diff suppressed because it is too large Load Diff