Merge pull request 'v2' (#1) from v2 into master
Reviewed-on: https://www.cainet.info/git/cai/AudioLib/pulls/1
This commit is contained in:
commit
b1005c05d4
@ -1,2 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
17
.eslintrc.js
17
.eslintrc.js
@ -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
13
.gitignore
vendored
@ -1,7 +1,8 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
/dist
|
/dist
|
||||||
/public/lib/*
|
/public/lib
|
||||||
|
/out/
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
@ -11,6 +12,7 @@ node_modules
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
@ -20,3 +22,12 @@ yarn-error.log*
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Yarn 2
|
||||||
|
.yarn/*
|
||||||
|
#!.yarn/cache
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|||||||
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
546
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
785
.yarn/releases/yarn-3.2.0.cjs
vendored
Normal file
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
7
.yarnrc.yml
Normal 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
|
||||||
17
README.md
17
README.md
@ -1,4 +1,4 @@
|
|||||||
# audiobookslibrary
|
# audiolib.vue3
|
||||||
|
|
||||||
## Project setup
|
## Project setup
|
||||||
```
|
```
|
||||||
@ -7,22 +7,17 @@ yarn install
|
|||||||
|
|
||||||
### Compiles and hot-reloads for development
|
### Compiles and hot-reloads for development
|
||||||
```
|
```
|
||||||
yarn run serve
|
yarn serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compiles and minifies for production
|
### Compiles and minifies for production
|
||||||
```
|
```
|
||||||
yarn run build
|
yarn build
|
||||||
```
|
|
||||||
|
|
||||||
### Run your tests
|
|
||||||
```
|
|
||||||
yarn run test
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lints and fixes files
|
### Lints and fixes files
|
||||||
```
|
```
|
||||||
yarn run lint
|
yarn lint
|
||||||
```
|
```
|
||||||
|
|
||||||
### Customize configuration
|
### Customize configuration
|
||||||
@ -54,12 +49,14 @@ file format:
|
|||||||
```
|
```
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
"cycle": "Books cycle",
|
||||||
"title": "Book 1 title",
|
"title": "Book 1 title",
|
||||||
"image": "lib/Author name/Book 1 title/cover.jpg",
|
"image": "lib/Author name/Book 1 title/cover.jpg",
|
||||||
"book": "lib/Author name/Book 1 title/book.json"
|
"book": "lib/Author name/Book 1 title/book.json"
|
||||||
},
|
},
|
||||||
...
|
...
|
||||||
{
|
{
|
||||||
|
"cycle": "Books cycle",
|
||||||
"title": "Book N title",
|
"title": "Book N title",
|
||||||
"image": "lib/Author name/Book N title/cover.jpg",
|
"image": "lib/Author name/Book N title/cover.jpg",
|
||||||
"book": "lib/Author name/Book N title/book.json"
|
"book": "lib/Author name/Book N title/book.json"
|
||||||
@ -82,4 +79,4 @@ file format:
|
|||||||
"title": "Chapter N title"
|
"title": "Chapter N title"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/app'
|
'@vue/cli-plugin-babel/preset'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
19
jsconfig.json
Normal file
19
jsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "esnext",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
74
package.json
74
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookslibrary",
|
"name": "audiolib.vue3",
|
||||||
"version": "0.2.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
@ -8,30 +8,52 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^3.6.95",
|
"@mdi/font": "7.1.96",
|
||||||
"axios": "^0.19.0",
|
"axios": "^1.2.1",
|
||||||
"core-js": "^2.6.5",
|
"core-js": "^3.27.0",
|
||||||
"crypto-js": "^3.1.9-1",
|
"crypto-js": "^4.1.1",
|
||||||
"register-service-worker": "^1.6.2",
|
"roboto-fontface": "^0.10.0",
|
||||||
"sass": "^1.49.11",
|
"vue": "^3.2.45",
|
||||||
"sass-loader": "^10",
|
"vue-axios": "^3.5.2",
|
||||||
"vue": "^2.6.10",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-axios": "^2.1.4",
|
"vue-router": "^4.1.6",
|
||||||
"vue-i18n": "^8.11.2",
|
"vuetify": "^3.0.6",
|
||||||
"vuetify": "^1.5.5"
|
"vuex": "^4.1.0",
|
||||||
|
"webfontloader": "^1.6.28",
|
||||||
|
"webpack-plugin-vuetify": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^3.8.0",
|
"@babel/core": "^7.20.7",
|
||||||
"@vue/cli-plugin-eslint": "^3.8.0",
|
"@babel/eslint-parser": "^7.19.1",
|
||||||
"@vue/cli-plugin-pwa": "^3.8.0",
|
"@vue/cli-plugin-babel": "~5.0.8",
|
||||||
"@vue/cli-service": "^3.8.0",
|
"@vue/cli-plugin-eslint": "~5.0.8",
|
||||||
"babel-eslint": "^10.0.1",
|
"@vue/cli-service": "~5.0.8",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^8.30.0",
|
||||||
"eslint-plugin-vue": "^5.0.0",
|
"eslint-plugin-vue": "^9.8.0",
|
||||||
"stylus": "^0.54.5",
|
"sass": "^1.57.1",
|
||||||
"stylus-loader": "^3.0.1",
|
"sass-loader": "^13.2.0",
|
||||||
"vue-cli-plugin-vuetify": "^0.5.0",
|
"vue-cli-plugin-vuetify": "~2.5.8",
|
||||||
"vue-template-compiler": "^2.6.10",
|
"vuetify-loader": "^2.0.0-alpha.9"
|
||||||
"vuetify-loader": "^1.0.5"
|
},
|
||||||
}
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
autoprefixer: {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"AppNet": "CAINet",
|
|
||||||
"AppName": "Аудиотека"
|
|
||||||
}
|
|
||||||
3
public/data/config.json
Normal file
3
public/data/config.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"appNet": "CAINet"
|
||||||
|
}
|
||||||
BIN
public/img/library1080.webp
Normal file
BIN
public/img/library1080.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 697 KiB |
@ -1,18 +1,17 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>img/favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>img/favicon.ico">
|
||||||
<title>CAINet: Аудиотека</title>
|
<title><%= htmlWebpackPlugin.options.title %></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">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>Извините, данное приложение не работает без JavaScript.</strong><br>
|
<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>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
|
|||||||
@ -1,2 +0,0 @@
|
|||||||
User-agent: *
|
|
||||||
Disallow:
|
|
||||||
1077
src/App.vue
1077
src/App.vue
File diff suppressed because it is too large
Load Diff
61
src/components/AboutDialog.vue
Normal file
61
src/components/AboutDialog.vue
Normal 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">© Александр Чебыкин, 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">© 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>
|
||||||
@ -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>
|
|
||||||
90
src/components/AuthorsLib.vue
Normal file
90
src/components/AuthorsLib.vue
Normal 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>
|
||||||
@ -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>
|
|
||||||
178
src/components/BookReader.vue
Normal file
178
src/components/BookReader.vue
Normal 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") }} «{{ $store.getters.book.cycle }}»</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>
|
||||||
@ -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
190
src/components/BooksLib.vue
Normal 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") }} «{{ cycleName }}»</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>
|
||||||
@ -7,7 +7,7 @@
|
|||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="headline">{{ title }}</v-card-title>
|
<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-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="primary" flat @click="dialog = false">Ok</v-btn>
|
<v-btn color="primary" flat @click="dialog = false">Ok</v-btn>
|
||||||
@ -19,11 +19,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
name: "MessageDialog",
|
||||||
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
dialog: false,
|
dialog: false,
|
||||||
title: "",
|
title: "",
|
||||||
text: ""
|
text: ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
89
src/components/SettingsDialog.vue
Normal file
89
src/components/SettingsDialog.vue
Normal 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>
|
||||||
69
src/main.js
69
src/main.js
@ -1,35 +1,54 @@
|
|||||||
/**
|
import { createApp } from "vue";
|
||||||
* Audiobooks library
|
import App from "./App.vue";
|
||||||
*
|
import axios from "axios";
|
||||||
* @author Alexander I. Chebykin <alex.chebykin@gmail.com>
|
import vueAxios from "vue-axios";
|
||||||
* @copyright 2018-2019 Alexander I. Chebykin
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
* @license https://opensource.org/licenses/MIT MIT
|
import routes from "./routes";
|
||||||
* @version 0.9
|
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"
|
loadFonts();
|
||||||
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"
|
|
||||||
|
|
||||||
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,
|
"stopped": 0,
|
||||||
"paused": 1,
|
"paused": 1,
|
||||||
"playing": 2
|
"playing": 2
|
||||||
};
|
};
|
||||||
|
|
||||||
new Vue({
|
/**
|
||||||
i18n,
|
* App tabs
|
||||||
render: h => h(App),
|
*/
|
||||||
}).$mount("#app")
|
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")
|
||||||
|
});
|
||||||
|
|||||||
@ -1,45 +1,16 @@
|
|||||||
import Vue from "vue"
|
import { createI18n } from "vue-i18n";
|
||||||
import VueI18n from "vue-i18n"
|
|
||||||
|
|
||||||
Vue.use(VueI18n)
|
import en from "./locales/en.js";
|
||||||
|
import ru from "./locales/ru.js";
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
"en": {
|
en: en,
|
||||||
about: "About",
|
ru: ru
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = new VueI18n({
|
export default new createI18n({
|
||||||
locale: "ru", // set locale
|
legacy: false, // Vuetify does not support the legacy mode of vue-i18n
|
||||||
fallbackLocale: "en", // set fallback locale
|
locale: "ru",
|
||||||
messages, // set locale messages
|
fallbackLocale: "en",
|
||||||
})
|
messages,
|
||||||
|
});
|
||||||
export default i18n
|
|
||||||
|
|||||||
18
src/plugins/locales/en.js
Normal file
18
src/plugins/locales/en.js
Normal 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
18
src/plugins/locales/ru.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const messages = {
|
||||||
|
appName: "Аудиотека",
|
||||||
|
about: "О программе",
|
||||||
|
authors: "Авторы",
|
||||||
|
autoBookmarks: "Автозакладки",
|
||||||
|
autoLoadBooks: "Автоматически загружать последние открытые книги",
|
||||||
|
book: "Книга",
|
||||||
|
bookmarkSaved: "Закладка сохранена",
|
||||||
|
books: "Книги",
|
||||||
|
cycle: "Цикл",
|
||||||
|
darkTheme: "Тёмная тема",
|
||||||
|
information: "Информация",
|
||||||
|
playbackRate: "Скорость воспроизведения",
|
||||||
|
reader: "Читает",
|
||||||
|
settings: "Настройки"
|
||||||
|
}
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@ -1,7 +1,17 @@
|
|||||||
import Vue from "vue";
|
// Styles
|
||||||
import Vuetify from "vuetify/lib";
|
import "@mdi/font/css/materialdesignicons.css";
|
||||||
import "vuetify/src/stylus/app.styl";
|
import "vuetify/styles";
|
||||||
|
import { createVueI18nAdapter } from "vuetify/locale/adapters/vue-i18n";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import i18n from "./i18n.js";
|
||||||
|
|
||||||
Vue.use(Vuetify, {
|
// Vuetify
|
||||||
iconfont: "md",
|
import { createVuetify } from "vuetify";
|
||||||
|
|
||||||
|
export default createVuetify({
|
||||||
|
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
|
||||||
|
locale: createVueI18nAdapter({
|
||||||
|
i18n,
|
||||||
|
useI18n,
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
15
src/plugins/webfontloader.js
Normal file
15
src/plugins/webfontloader.js
Normal 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"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { register } from "register-service-worker"
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||||
@ -8,25 +8,31 @@ if (process.env.NODE_ENV === "production") {
|
|||||||
console.log(
|
console.log(
|
||||||
"App is being served from cache by a service worker.\n" +
|
"App is being served from cache by a service worker.\n" +
|
||||||
"For more details, visit https://goo.gl/AFskqB"
|
"For more details, visit https://goo.gl/AFskqB"
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
registered () {
|
registered () {
|
||||||
console.log("Service worker has been registered.")
|
console.log("Service worker has been registered.");
|
||||||
},
|
},
|
||||||
|
|
||||||
cached () {
|
cached () {
|
||||||
console.log("Content has been cached for offline use.")
|
console.log("Content has been cached for offline use.");
|
||||||
},
|
},
|
||||||
|
|
||||||
updatefound () {
|
updatefound () {
|
||||||
console.log("New content is downloading.")
|
console.log("New content is downloading.");
|
||||||
},
|
},
|
||||||
|
|
||||||
updated () {
|
updated () {
|
||||||
console.log("New content is available; please refresh.")
|
console.log("New content is available; please refresh.");
|
||||||
},
|
},
|
||||||
|
|
||||||
offline () {
|
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) {
|
error (error) {
|
||||||
console.error("Error during service worker registration:", error)
|
console.error("Error during service worker registration:", error);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/routes.js
Normal file
12
src/routes.js
Normal 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
56
src/store.js
Normal 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;
|
||||||
@ -1,5 +1,15 @@
|
|||||||
module.exports = {
|
const { defineConfig } = require("@vue/cli-service");
|
||||||
publicPath: process.env.NODE_ENV === 'production'
|
|
||||||
? '/audiobooks/'
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user