Initial commit
This commit is contained in:
parent
ef016cf61b
commit
c24762811c
2
.browserslistrc
Normal file
2
.browserslistrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
17
.eslintrc.js
Normal file
17
.eslintrc.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
/public/lib/*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) <year> <copyright holders>
|
Copyright (c) 2019 Alexander I. Chebykin
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
77
README.md
77
README.md
@ -1,3 +1,76 @@
|
|||||||
# AudioLib
|
# audiobookslibrary
|
||||||
|
|
||||||
Self-hosted audiobooks library
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
yarn run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
yarn run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run your tests
|
||||||
|
```
|
||||||
|
yarn run test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
yarn run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||||
|
|
||||||
|
## Library configuration
|
||||||
|
lib/authors.json - Authors info
|
||||||
|
file format:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"author": "Author 1 name",
|
||||||
|
"books": "lib/Author 1 name/books.json",
|
||||||
|
"image": "lib/Author 1 name/photo.png"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"author": "Author N name",
|
||||||
|
"books": "lib/Author N name/books.json",
|
||||||
|
"image": "lib/Author N name/photo.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
lib/Author name/books.json - Books info
|
||||||
|
file format:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"title": "Book 1 title",
|
||||||
|
"image": "lib/Author name/Book 1 title/cover.jpg",
|
||||||
|
"book": "lib/Author name/Book 1 title/book.json"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"title": "Book N title",
|
||||||
|
"image": "lib/Author name/Book N title/cover.jpg",
|
||||||
|
"book": "lib/Author name/Book N title/book.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
lib/Author name/Book title/book.json - Book info
|
||||||
|
file format:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"book": "lib/Author name/Book title/chapter1.mp3",
|
||||||
|
"title": "Chapter 1 title"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
{
|
||||||
|
"book": "lib/Author name/Book title/chapterN.mp3",
|
||||||
|
"title": "Chapter N title"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
5
babel.config.js
Normal file
5
babel.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/app'
|
||||||
|
]
|
||||||
|
}
|
||||||
34
package.json
Normal file
34
package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "audiobookslibrary",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"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",
|
||||||
|
"vue": "^2.6.10",
|
||||||
|
"vue-axios": "^2.1.4",
|
||||||
|
"vuetify": "^1.5.5"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
autoprefixer: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
public/config.json
Normal file
4
public/config.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"AppNet": "CAINet",
|
||||||
|
"AppName": "Аудиотека"
|
||||||
|
}
|
||||||
BIN
public/img/book.png
Normal file
BIN
public/img/book.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
BIN
public/img/favicon.ico
Normal file
BIN
public/img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
19
public/index.html
Normal file
19
public/index.html
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<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">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>Извините, данное приложение не работает без JavaScript.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
public/locales/en.json
Normal file
14
public/locales/en.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
37
public/locales/locales.json
Normal file
37
public/locales/locales.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"code": "en",
|
||||||
|
"language": "Английский",
|
||||||
|
"file": "en.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en-au",
|
||||||
|
"language": "Английский",
|
||||||
|
"file": "en.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en-gb",
|
||||||
|
"language": "Английский",
|
||||||
|
"file": "en.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "en-us",
|
||||||
|
"language": "Английский",
|
||||||
|
"file": "en.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru",
|
||||||
|
"language": "Русский",
|
||||||
|
"file": "ru.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru-ru",
|
||||||
|
"language": "Русский",
|
||||||
|
"file": "ru.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "ru-md",
|
||||||
|
"language": "Русский",
|
||||||
|
"file": "ru.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
14
public/locales/ru.json
Normal file
14
public/locales/ru.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"About": "О приложении",
|
||||||
|
"Authors": "Авторы",
|
||||||
|
"AutoBookmarks": "Автозакладки",
|
||||||
|
"Book": "Книга",
|
||||||
|
"BookmarkSaved": "Закладка сохранена",
|
||||||
|
"BookmarkRemoved": "Закладка удалена",
|
||||||
|
"Books": "Книги",
|
||||||
|
"DarkTheme": "Тёмная тема",
|
||||||
|
"Error": "Ошибка",
|
||||||
|
"Information": "Информация",
|
||||||
|
"RemoveBookmark": "Удалить закладку",
|
||||||
|
"SetBookmark": "Установить закладку"
|
||||||
|
}
|
||||||
20
public/manifest.json
Normal file
20
public/manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "audiolib",
|
||||||
|
"short_name": "audiolib",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "./img/icons/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "./img/icons/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "./index.html",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"theme_color": "#4DBA87"
|
||||||
|
}
|
||||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
457
src/App.vue
Normal file
457
src/App.vue
Normal file
@ -0,0 +1,457 @@
|
|||||||
|
<template>
|
||||||
|
<v-app :dark="darkTheme">
|
||||||
|
<v-toolbar app>
|
||||||
|
<v-toolbar-title class="headline text-uppercase">
|
||||||
|
<span v-if="appNet !== ''">{{ appNet }}:</span>
|
||||||
|
<span class="font-weight-light">{{ appName }}</span>
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
v-if="isLoading"
|
||||||
|
class="loading-progress"
|
||||||
|
></v-progress-circular>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-menu bottom left>
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
icon
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-bookmark</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-tile
|
||||||
|
@click="setBookmark(false)"
|
||||||
|
>
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-icon>mdi-bookmark-plus</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
<v-list-tile-title>{{ locale.SetBookmark }}</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
<v-list-tile
|
||||||
|
@click="removeBookmark(false)"
|
||||||
|
>
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-icon>mdi-bookmark-minus</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
<v-list-tile-title>{{ locale.RemoveBookmark }}</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
|
<v-menu bottom left>
|
||||||
|
<template v-slot:activator="{ on }">
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
icon
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-icon>more_vert</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-tile>
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-switch v-model="darkTheme" color="blue"></v-switch>
|
||||||
|
</v-list-tile-action>
|
||||||
|
<v-list-tile-title>{{ locale.DarkTheme }}</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
<v-list-tile>
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-switch v-model="autoBookmark" color="blue"></v-switch>
|
||||||
|
</v-list-tile-action>
|
||||||
|
<v-list-tile-title>{{ locale.AutoBookmarks }}</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-list-tile @click="showAboutDialog">
|
||||||
|
<v-list-tile-action>
|
||||||
|
<v-icon>mdi-information-variant</v-icon>
|
||||||
|
</v-list-tile-action>
|
||||||
|
<v-list-tile-title>{{ locale.About }}</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</v-toolbar>
|
||||||
|
|
||||||
|
<v-content>
|
||||||
|
<v-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
align-with-title
|
||||||
|
centered
|
||||||
|
fixed-tabs
|
||||||
|
>
|
||||||
|
<v-tab ripple>{{ locale.Authors }}</v-tab>
|
||||||
|
<v-tab-item>
|
||||||
|
<Authors ref="Authors" v-on:author_selected="openBooks" />
|
||||||
|
</v-tab-item>
|
||||||
|
<v-tab ripple>{{ locale.Books }}</v-tab>
|
||||||
|
<v-tab-item>
|
||||||
|
<Books ref="Books" v-on:book_selected="openBook" />
|
||||||
|
</v-tab-item>
|
||||||
|
<v-tab ripple>{{ locale.Book }}</v-tab>
|
||||||
|
<v-tab-item>
|
||||||
|
<Book ref="Book" v-on:progress_updated="updateProgress" />
|
||||||
|
</v-tab-item>
|
||||||
|
</v-tabs>
|
||||||
|
</v-content>
|
||||||
|
|
||||||
|
<v-card tile class="player-interface">
|
||||||
|
<v-flex xs12>
|
||||||
|
<v-slider
|
||||||
|
v-model="progress"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
height="30px"
|
||||||
|
:disabled="isOff"
|
||||||
|
:inverse-label="true"
|
||||||
|
:label="`${currentTime}/${duration}`"
|
||||||
|
class="time-slider px-2"
|
||||||
|
></v-slider>
|
||||||
|
</v-flex>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-tile>
|
||||||
|
<v-list-tile-content v-if="$vuetify.breakpoint.smAndUp">
|
||||||
|
<v-list-tile-title>{{ authorName }}</v-list-tile-title>
|
||||||
|
<v-list-tile-sub-title>{{ bookTitle }}<span v-if="currentChapter !== ''"> - {{ currentChapter }}</span></v-list-tile-sub-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
|
||||||
|
<v-list-tile-action @click="prevChapter">
|
||||||
|
<v-btn icon>
|
||||||
|
<v-icon>mdi-skip-backward</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-tile-action>
|
||||||
|
|
||||||
|
<v-list-tile-action :class="{ 'mx-5': $vuetify.breakpoint.mdAndUp }">
|
||||||
|
<v-btn icon @click="toggleStatus">
|
||||||
|
<v-icon>mdi-{{ isPlaying ? "pause" : "play" }}</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-tile-action>
|
||||||
|
|
||||||
|
<v-list-tile-action
|
||||||
|
@click="nextChapter"
|
||||||
|
>
|
||||||
|
<v-btn icon>
|
||||||
|
<v-icon>mdi-skip-forward</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-list-tile-action>
|
||||||
|
<v-flex xs6 sm3 md2>
|
||||||
|
<v-slider
|
||||||
|
:class="{ 'mr-3 ml-5': $vuetify.breakpoint.mdAndUp, 'ml-3' : $vuetify.breakpoint.mdAndDown }"
|
||||||
|
:min="0"
|
||||||
|
:max="10"
|
||||||
|
v-model="volume"
|
||||||
|
append-icon="volume_up"
|
||||||
|
prepend-icon="volume_down"
|
||||||
|
></v-slider>
|
||||||
|
</v-flex>
|
||||||
|
</v-list-tile>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
<MessageDialog ref="MessageDialog" />
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Authors from './components/Authors'
|
||||||
|
import Books from './components/Books'
|
||||||
|
import Book from './components/Book'
|
||||||
|
import MessageDialog from './components/MessageDialog'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
Authors,
|
||||||
|
Books,
|
||||||
|
Book,
|
||||||
|
MessageDialog
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
appNet: "CAINet",
|
||||||
|
appName: "Аудиотека",
|
||||||
|
activeTab: null,
|
||||||
|
autoBookmark: false,
|
||||||
|
darkTheme: false,
|
||||||
|
isLoading: false,
|
||||||
|
isMounted: false,
|
||||||
|
volume: 5,
|
||||||
|
locale: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.isMounted = true;
|
||||||
|
this.darkTheme = localStorage.darkTheme ? JSON.parse(localStorage.darkTheme) : false;
|
||||||
|
this.autoBookmark = localStorage.autoBookmark ? JSON.parse(localStorage.autoBookmark) : false;
|
||||||
|
this.volume = localStorage.volume ? JSON.parse(localStorage.volume) : 5;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
let userLocale = navigator.language || navigator.userLanguage;
|
||||||
|
|
||||||
|
this.axios.get("locales/locales.json")
|
||||||
|
.then(response => {
|
||||||
|
let localeLoaded = false;
|
||||||
|
response.data.forEach(element => {
|
||||||
|
if (element.code == userLocale.toLowerCase()) {
|
||||||
|
this.loadLocale(element.file);
|
||||||
|
localeLoaded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!localeLoaded) { this.loadLocale("en"); }
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
this.showMessage("Error", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.axios.get("config.json")
|
||||||
|
.then(response => {
|
||||||
|
this.appNet = response.data.AppNet;
|
||||||
|
this.appName = response.data.AppName;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
this.showMessage(this.locale.Error, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.axios.get("lib/authors.json")
|
||||||
|
.then(response => {
|
||||||
|
this.$refs.Authors.items = response.data;
|
||||||
|
this.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
this.showMessage(this.locale.Error, error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
authorName () {
|
||||||
|
return !this.isMounted ? "" : this.$refs.Book.authorName;
|
||||||
|
},
|
||||||
|
|
||||||
|
bookOpened () {
|
||||||
|
return !this.isMounted ? false : this.$refs.Book.items.length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
bookTitle () {
|
||||||
|
return !this.isMounted ? "" : this.$refs.Book.bookTitle;
|
||||||
|
},
|
||||||
|
|
||||||
|
currentChapter () {
|
||||||
|
let curChapter = !this.isMounted
|
||||||
|
? ""
|
||||||
|
: this.$refs.Book.items[this.$refs.Book.currentChapter];
|
||||||
|
return curChapter ? curChapter.title : "";
|
||||||
|
},
|
||||||
|
|
||||||
|
currentTime () {
|
||||||
|
return !this.isMounted ? "00:00" : this.$refs.Book.currentTime;
|
||||||
|
},
|
||||||
|
|
||||||
|
duration () {
|
||||||
|
return !this.isMounted ? "00:00" : this.$refs.Book.duration;
|
||||||
|
},
|
||||||
|
|
||||||
|
isOff () {
|
||||||
|
return !this.isMounted ? true : this.$refs.Book.isOff;
|
||||||
|
},
|
||||||
|
|
||||||
|
isPlaying () {
|
||||||
|
return !this.isMounted ? false : this.$refs.Book.isPlaying;
|
||||||
|
},
|
||||||
|
|
||||||
|
progress: {
|
||||||
|
get () {
|
||||||
|
return !this.isMounted ? "" : this.$refs.Book.progress;
|
||||||
|
},
|
||||||
|
|
||||||
|
set (newVal) {
|
||||||
|
if (newVal !== Math.round(this.$refs.Book.progress)) {
|
||||||
|
this.$refs.Book.audioElement.currentTime = this.$refs.Book.audioElement.duration * newVal / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
autoBookmark (val) { localStorage.autoBookmark = val; },
|
||||||
|
darkTheme (val) { localStorage.darkTheme = val; },
|
||||||
|
volume (val) {
|
||||||
|
localStorage.volume = val;
|
||||||
|
this.$refs.Book.volume = val;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getHash (val) {
|
||||||
|
let CryptoJS = require("crypto-js");
|
||||||
|
let hash = CryptoJS.MD5(val);
|
||||||
|
return hash.toString(CryptoJS.enc.Hex);
|
||||||
|
},
|
||||||
|
|
||||||
|
loadLocale (langFile) {
|
||||||
|
this.axios.get("locales/" + langFile)
|
||||||
|
.then(response => {
|
||||||
|
this.locale = response.data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.log(error);
|
||||||
|
this.showMessage("Error", error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
nextChapter () { this.$refs.Book.nextChapter(); },
|
||||||
|
|
||||||
|
openBook (val) {
|
||||||
|
this.$refs.Book.authorName = this.$refs.Books.authorName;
|
||||||
|
this.$refs.Book.authorImg = this.$refs.Books.authorImg;
|
||||||
|
this.$refs.Book.bookImg = val.image;
|
||||||
|
this.$refs.Book.bookTitle = val.title;
|
||||||
|
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.axios.get(val.book)
|
||||||
|
.then(response => {
|
||||||
|
this.$refs.Book.items = response.data;
|
||||||
|
|
||||||
|
let bmKey = this.getHash(JSON.stringify(this.$refs.Book.items));
|
||||||
|
let bookmark = localStorage.getItem(bmKey) ? JSON.parse(localStorage.getItem(bmKey)) : null;
|
||||||
|
|
||||||
|
if (bookmark !== null) {
|
||||||
|
this.$refs.Book.loadChapter(bookmark.chapter, bookmark.progress);
|
||||||
|
} else {
|
||||||
|
this.$refs.Book.loadChapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeTab = 2;
|
||||||
|
this.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.isLoading = false;
|
||||||
|
console.log(error);
|
||||||
|
this.showMessage(this.locale.Error, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeTab = 2;
|
||||||
|
},
|
||||||
|
|
||||||
|
openBooks (val) {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
this.axios.get(val.books)
|
||||||
|
.then(response => {
|
||||||
|
this.$refs.Books.authorName = val.author;
|
||||||
|
this.$refs.Books.authorImg = val.image;
|
||||||
|
this.$refs.Books.items = response.data;
|
||||||
|
this.activeTab = 1;
|
||||||
|
this.isLoading = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.isLoading = false;
|
||||||
|
console.log(error);
|
||||||
|
this.showMessage(this.locale.Error, error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
prevChapter () { this.$refs.Book.prevChapter(); },
|
||||||
|
|
||||||
|
progressClick (event) {
|
||||||
|
if (!this.isMounted) { return; }
|
||||||
|
|
||||||
|
if (this.$refs.Book.audioElement !== null) {
|
||||||
|
let elLeft = this.$refs.playProgress.$el.getBoundingClientRect().left;
|
||||||
|
let elWidth = this.$refs.playProgress.$el.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
this.$refs.Book.audioElement.currentTime = this.$refs.Book.audioElement.duration * (event.clientX - elLeft) / elWidth;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeBookmark (silent = true) {
|
||||||
|
if (this.$refs.Book.items.length == 0) { return; }
|
||||||
|
localStorage.removeItem(this.getHash(this.$refs.Book.items));
|
||||||
|
if (!silent) { this.showMessage(this.locale.Information, this.locale.BookmarkRemoved); }
|
||||||
|
},
|
||||||
|
|
||||||
|
setBookmark (silent = true) {
|
||||||
|
if (this.$refs.Book.items.length == 0) { return; }
|
||||||
|
|
||||||
|
let data = {
|
||||||
|
chapter: this.$refs.Book.currentChapter,
|
||||||
|
progress: this.$refs.Book.progress
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem(
|
||||||
|
this.getHash(JSON.stringify(this.$refs.Book.items)),
|
||||||
|
JSON.stringify(data)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!silent) { this.showMessage(this.locale.Information, this.locale.BookmarkSaved); }
|
||||||
|
},
|
||||||
|
|
||||||
|
showAboutDialog () {
|
||||||
|
let userLocale = navigator.language || navigator.userLanguage;
|
||||||
|
userLocale = userLocale.indexOf("-") > 0
|
||||||
|
? userLocale.substring(0, userLocale.indexOf("-")).toLowerCase()
|
||||||
|
: userLocale.toLowerCase();
|
||||||
|
|
||||||
|
let aboutText = "";
|
||||||
|
if (userLocale == "ru") {
|
||||||
|
aboutText = "<p><center>Аудиотека</center></p>" +
|
||||||
|
"<p><center>© Александр Чебыкин, 2019</center></p>" +
|
||||||
|
"<p><center>Опубликовано под лицензией <a href=\"https://opensource.org/licenses/MIT\" target=\"_blank\">MIT license</a></center></p>" +
|
||||||
|
"<p><center>Git: <a href=\"https://home.cainet.info:3000/cai/AudioLib\" target=\"_blank\">https://home.cainet.info:3000/cai/AudioLib</a></center></p>";
|
||||||
|
} else {
|
||||||
|
aboutText = "<p><center>Audiobooks library</center></p>" +
|
||||||
|
"<p><center>© Alexander I Chebykin, 2019</center></p>" +
|
||||||
|
"<p><center>Published under <a href=\"https://opensource.org/licenses/MIT\" target=\"_blank\">MIT license</a></center></p>" +
|
||||||
|
"<p><center>Git: <a href=\"https://home.cainet.info:3000/cai/AudioLib\" target=\"_blank\">https://home.cainet.info:3000/cai/AudioLib</a></center></p>";
|
||||||
|
}
|
||||||
|
this.showMessage(this.locale.About, aboutText);
|
||||||
|
},
|
||||||
|
|
||||||
|
showMessage (title, text) {
|
||||||
|
this.$refs.MessageDialog.title = title;
|
||||||
|
this.$refs.MessageDialog.text = text;
|
||||||
|
this.$refs.MessageDialog.dialog = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleStatus () {
|
||||||
|
this.$refs.Book.toggleStatus();
|
||||||
|
if (this.autoBookmark && this.$refs.Book.isPaused) { this.setBookmark(); }
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProgress (val) { this.progress = val; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.loading-progress { margin: auto 15px; }
|
||||||
|
|
||||||
|
.player-interface {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.player-interface .time-slider {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 0 -25px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
src/components/Authors.vue
Normal file
69
src/components/Authors.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<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: () => ({
|
||||||
|
items: []
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
authorClick (val) { this.$emit("author_selected", val); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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>
|
||||||
210
src/components/Book.vue
Normal file
210
src/components/Book.vue
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<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>
|
||||||
|
<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: () => ({
|
||||||
|
audioElement: null,
|
||||||
|
authorName: "",
|
||||||
|
authorImg: "",
|
||||||
|
bookImg: "",
|
||||||
|
bookTitle: "",
|
||||||
|
currentChapter: 0,
|
||||||
|
currentTime: "00:00",
|
||||||
|
duration: "00:00",
|
||||||
|
items: [],
|
||||||
|
progress: 0,
|
||||||
|
status: 0, //this.statuses.stopped,
|
||||||
|
volume: 5
|
||||||
|
}),
|
||||||
|
|
||||||
|
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(); }
|
||||||
|
},
|
||||||
|
|
||||||
|
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>
|
||||||
|
.author-image {
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-layout { min-height: calc(100vh - 190px); }
|
||||||
|
|
||||||
|
.f-left { float: left; }
|
||||||
|
</style>
|
||||||
97
src/components/Books.vue
Normal file
97
src/components/Books.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<v-card flat>
|
||||||
|
<v-container bg grid-list-md>
|
||||||
|
<v-layout row wrap align-center>
|
||||||
|
<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-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: () => ({
|
||||||
|
authorName: "",
|
||||||
|
authorImg: "",
|
||||||
|
items: []
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
bookClick (val) {
|
||||||
|
this.$emit("book_selected", val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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>
|
||||||
30
src/components/MessageDialog.vue
Normal file
30
src/components/MessageDialog.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<v-layout row justify-center>
|
||||||
|
<v-dialog
|
||||||
|
v-model="dialog"
|
||||||
|
persistent
|
||||||
|
max-width="290"
|
||||||
|
>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="headline">{{ title }}</v-card-title>
|
||||||
|
<v-card-text v-html="text"></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,
|
||||||
|
title: "",
|
||||||
|
text: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
33
src/main.js
Normal file
33
src/main.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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 '@mdi/font/css/materialdesignicons.css'
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Axios from 'axios'
|
||||||
|
import VueAxios from 'vue-axios'
|
||||||
|
import './plugins/vuetify'
|
||||||
|
import App from './App.vue'
|
||||||
|
import './registerServiceWorker'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
Vue.use(VueAxios, Axios)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audioplayer statuses
|
||||||
|
*/
|
||||||
|
Vue.prototype.statuses = {
|
||||||
|
'stopped': 0,
|
||||||
|
'paused': 1,
|
||||||
|
'playing': 2
|
||||||
|
};
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
render: h => h(App),
|
||||||
|
}).$mount('#app')
|
||||||
7
src/plugins/vuetify.js
Normal file
7
src/plugins/vuetify.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Vue from 'vue'
|
||||||
|
import Vuetify from 'vuetify/lib'
|
||||||
|
import 'vuetify/src/stylus/app.styl'
|
||||||
|
|
||||||
|
Vue.use(Vuetify, {
|
||||||
|
iconfont: 'md',
|
||||||
|
})
|
||||||
32
src/registerServiceWorker.js
Normal file
32
src/registerServiceWorker.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
import { register } from 'register-service-worker'
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||||
|
ready () {
|
||||||
|
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.')
|
||||||
|
},
|
||||||
|
cached () {
|
||||||
|
console.log('Content has been cached for offline use.')
|
||||||
|
},
|
||||||
|
updatefound () {
|
||||||
|
console.log('New content is downloading.')
|
||||||
|
},
|
||||||
|
updated () {
|
||||||
|
console.log('New content is available; please refresh.')
|
||||||
|
},
|
||||||
|
offline () {
|
||||||
|
console.log('No internet connection found. App is running in offline mode.')
|
||||||
|
},
|
||||||
|
error (error) {
|
||||||
|
console.error('Error during service worker registration:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
5
vue.config.js
Normal file
5
vue.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
publicPath: process.env.NODE_ENV === 'production'
|
||||||
|
? '/audiobooks/'
|
||||||
|
: '/'
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user