mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48f232790a | |||
| 3c55aa5f43 | |||
| 8c1edb30a6 | |||
| 5e64af4448 | |||
| 9f60017cfe | |||
| b6a86d11d2 | |||
| db86bfd63d | |||
| 7ff72a8920 | |||
| 2c4f86d148 | |||
| 1a9f26e804 | |||
| 42f8194bde | |||
| 8634b7058c | |||
| fc276b330a | |||
| 5b22d7430a | |||
| 8883debc74 | |||
| c92cb08f6f | |||
| 1254b668de | |||
| 48b703bf9f | |||
| 064679c057 | |||
| ba23d258e7 | |||
| 98cd19d440 | |||
| 4c8b91e9d9 | |||
| ba742563c2 | |||
| f0e70ed27b | |||
| acc4bdbc5e | |||
| c45c82306e | |||
| fd827b2214 | |||
| df1c157994 | |||
| a92e417581 | |||
| 6ad0719880 | |||
| 5383d0b5f7 | |||
| b3cefc075d | |||
| ac62d18007 | |||
| fe14c26782 | |||
| b33a3cabf9 | |||
| 6224163ecd | |||
| 05aabb2843 | |||
| 7d2d5f6bf4 | |||
| c938685679 | |||
| e6ecc28001 | |||
| 93fa6ba466 | |||
| a8f459e4fa | |||
| 2441bb1cec | |||
| 25cc24fca5 | |||
| ff4cbc6d5f | |||
| f79bfae95d | |||
| 2f99efcc60 | |||
| 45b13571a5 | |||
| 04da8812df | |||
| 840304ee04 | |||
| 41bd9a9358 | |||
| 1e0a9918fd | |||
| 799acf5db8 | |||
| 1326d29fad | |||
| 9b35530956 | |||
| 0ae054c5d7 | |||
| c72eac9987 | |||
| 159ccd807f | |||
| 5d13faef33 | |||
| e0de59a4b6 | |||
| 519a1b0eaf | |||
| 4d8e1b7cef | |||
| 6d3e096e08 | |||
| 38edcdca4b | |||
| 8774e6be71 | |||
| ec197b2e13 | |||
| 1c0d6e9c67 | |||
| 7d711da381 | |||
| f66cea9829 | |||
| 5f572face5 | |||
| 88a4cf9f12 | |||
| 0b860e0d40 | |||
| 149bb3e5b2 | |||
| 7a7a779824 | |||
| 20a3657063 | |||
| 9c87c3a095 | |||
| 4de65b4369 | |||
| 996c78d760 | |||
| ccdc3d60c4 | |||
| 8be08882d8 | |||
| 26d2c5a8f0 | |||
| bae39e3a2d | |||
| bb1a72269a | |||
| 9674cfd258 | |||
| 627ddd2f70 | |||
| 27b3a44147 | |||
| 5308fd8b46 | |||
| 1b914d5d4f | |||
| 9e0f17f7c6 | |||
| 1320b6d785 | |||
| f1ddbeadaf | |||
| f9f89e1e51 | |||
| bbf214fa4c | |||
| f1582177e1 | |||
| d5712a564c | |||
| 1c274862d8 | |||
| 663c9e0fa9 | |||
| bcb0bc75c9 | |||
| 603823d6ea | |||
| 20c04d3ed3 | |||
| 02e5d608d0 | |||
| e53ac6566b | |||
| 2472b86284 | |||
| 29a15858f4 | |||
| afc16358ca | |||
| 9facf77ff1 | |||
| 1923854202 | |||
| 9cd92c7b7f | |||
| 8e0b723207 | |||
| 68ef3a07a7 | |||
| 202ceb02b5 | |||
| 59370cae81 | |||
| 52a3bc224a | |||
| 54d67e5216 | |||
| b55d8250cc | |||
| 3a1e9abd68 | |||
| c5ba40a178 | |||
| f0c6dccadb | |||
| e701d1ab6a | |||
| e10c8093c9 | |||
| e81b3461b2 | |||
| 9345cb3934 | |||
| eb36a0b3dd | |||
| 7e442ecb3d | |||
| f07c5eb725 | |||
| a486be92cb | |||
| 4d84060036 | |||
| fc503691fe | |||
| c80dd43a3e | |||
| a4a62e0c18 | |||
| 2f98cb9b6d | |||
| 91dc6eebb0 | |||
| d72e0a4418 | |||
| 2c8ebd43cc | |||
| 9f561aa296 | |||
| 930bacd45d | |||
| ef2d736b20 | |||
| f3a453be20 | |||
| 45c97a778d | |||
| 6ebc64f73b | |||
| 52807d0d49 | |||
| a5e18e99bc | |||
| f545b3e745 | |||
| e0877803e3 | |||
| 4916887c8d | |||
| 20eb573897 | |||
| 8ff7b6b6e6 | |||
| 06eaee8909 | |||
| 8f9487ba70 | |||
| eca51457b7 | |||
| 15c6fce648 | |||
| 6c872263c6 | |||
| 4d3b3d1740 | |||
| bba8920855 | |||
| f56b9487ff | |||
| 1946d8296b | |||
| 41e5d7f820 | |||
| 2507568103 | |||
| 19733798fa | |||
| 427d6da360 | |||
| 2b67d3d1c5 | |||
| 6926a40ad6 | |||
| 7a8da5bf3a | |||
| fc8fa17c6f | |||
| 0a88659a9f | |||
| 9967858c44 | |||
| e2ce388f90 | |||
| f31649f1d2 | |||
| a55c167dde | |||
| 642cf232ba | |||
| 164b4525c4 | |||
| 39c26d2bee | |||
| 2a69955cc1 | |||
| 4a5345dd5d | |||
| 1e6dd0e3e0 | |||
| 91cca2e358 | |||
| 816a9be618 | |||
| 9eb0ec76fe | |||
| 49054d5239 | |||
| 787c4e45a8 | |||
| 34cb7a4d02 | |||
| 006241163b | |||
| 03818fadee | |||
| 897c3ea625 | |||
| 73e4293f04 | |||
| 6f5ffcb1f8 | |||
| ed70f3af83 | |||
| 73196f9be8 | |||
| a77f4e9d77 | |||
| 294490f814 | |||
| 6183001fca | |||
| 3ac604c665 | |||
| e342b07cd0 | |||
| b524cbd1b3 | |||
| 88693d73bd | |||
| 2c453a34ee | |||
| 3d2b2e43b1 | |||
| c3f3fca896 | |||
| dedf6e5d4b | |||
| 6c379fc3a7 | |||
| 329e9c9eb2 | |||
| ee53086444 | |||
| 43d6c6678f | |||
| 82f136ba79 | |||
| e40d3dd64d | |||
| a5897fd64b | |||
| e786e3c057 | |||
| d347645475 | |||
| 215b78c162 | |||
| ee271519f9 | |||
| b350277bbc | |||
| 604ae080ac | |||
| a191dab359 | |||
| 3223011b13 | |||
| f746e246e4 | |||
| 0476b68585 | |||
| ec395bed72 | |||
| bff56220c2 | |||
| 3006405a52 | |||
| 9cd0ac80b1 | |||
| da51d38ba2 | |||
| 5ba6459069 | |||
| 75899242fd | |||
| 7faf42d892 | |||
| 10f5f331d7 | |||
| b1414388e1 | |||
| eb0f5b2e1b | |||
| 7af02ad2e2 | |||
| 8330dabc46 | |||
| dbc7ad0b3b | |||
| c0fd24770e | |||
| 4289fe4990 | |||
| e925e9b23f | |||
| 71cd86fdd5 | |||
| 03be947ad6 | |||
| 96f9084f2e | |||
| bbccfcbd12 | |||
| 9a697f48db | |||
| 37ad1cced2 | |||
| 26db20f63d | |||
| ff788e3591 | |||
| 4b482488de | |||
| e230b6640f | |||
| 2bc949fae3 | |||
| b1bc472205 | |||
| 5c7a38c292 | |||
| bbd6c51eb6 | |||
| d17f9b0687 | |||
| 4d2bdb6eee | |||
| b6a1014c72 | |||
| b99885c806 | |||
| f422c9b820 | |||
| 0befe91360 | |||
| da671e3fd5 | |||
| fec94c18aa | |||
| 11c6fc7d90 | |||
| 7ea5e7dc95 | |||
| 2a98e2c361 | |||
| 7fb499b301 | |||
| af9aee76cf | |||
| 075ec15f02 | |||
| 1c650473f8 | |||
| 0efdf50821 | |||
| df65ef2191 | |||
| bc3b1d9565 | |||
| 2998d3ba6a | |||
| ea11153032 | |||
| 733f61075f | |||
| 618e69775c | |||
| eabfa90121 | |||
| 43b7ccd61a | |||
| b6875a44cf | |||
| c0004dd532 | |||
| 0ee3b89760 | |||
| c5e60d30e1 | |||
| acaf1ac196 | |||
| 8dc4538c95 | |||
| e224fd2595 | |||
| f0a1ea4d6d | |||
| 10cb8ebf3b | |||
| 8c4afa1866 | |||
| eb5af47bbf | |||
| 4fd93ce64c | |||
| 7ba4e9e66d | |||
| e2e5449d25 | |||
| abc76ca155 | |||
| 0fc84a8684 | |||
| a76600e53b | |||
| e55cf30705 | |||
| 2c65b8fd2b | |||
| 20b8e35132 | |||
| 8007225a41 | |||
| 63a6da6680 | |||
| 31c8cb476a | |||
| 79bd6a25d9 | |||
| 0042604e6d | |||
| 54f2bb1092 | |||
| 6b6df619f5 |
@@ -6,5 +6,5 @@ module.exports.config = {
|
|||||||
MetadataPath: Path.resolve('metadata'),
|
MetadataPath: Path.resolve('metadata'),
|
||||||
FFmpegPath: '/usr/bin/ffmpeg',
|
FFmpegPath: '/usr/bin/ffmpeg',
|
||||||
FFProbePath: '/usr/bin/ffprobe',
|
FFProbePath: '/usr/bin/ffprobe',
|
||||||
SkipBinariesCheck: true
|
SkipBinariesCheck: false
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,3 @@ contact_links:
|
|||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/HQgCbd6E75
|
url: https://discord.gg/HQgCbd6E75
|
||||||
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
||||||
- name: Matrix
|
|
||||||
url: https://matrix.to/#/#audiobookshelf:matrix.org
|
|
||||||
about: Ask questions, get help troubleshooting, and join the Abs community here.
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Add issue comments by label
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- labeled
|
||||||
|
jobs:
|
||||||
|
help-wanted:
|
||||||
|
if: github.event.label.name == 'help wanted'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Help wanted comment
|
||||||
|
run: gh issue comment "$NUMBER" --body "$BODY"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
BODY: >
|
||||||
|
This issue is not able to be completed due to limited bandwidth or access to the required test hardware.
|
||||||
|
|
||||||
|
This issue is available for anyone to work on.
|
||||||
|
|
||||||
|
|
||||||
|
config-issue:
|
||||||
|
if: github.event.label.name == 'config-issue'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Config issue comment
|
||||||
|
run: gh issue close "$NUMBER" --reason "not planned" --comment "$BODY"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GH_REPO: ${{ github.repository }}
|
||||||
|
NUMBER: ${{ github.event.issue.number }}
|
||||||
|
BODY: >
|
||||||
|
After reviewing this issue, this appears to be a problem with your setup and not Audiobookshelf. This issue is being closed to keep the issue tracker focused on Audiobookshelf itself. Please reach out on the Audiobookshelf Discord for community support.
|
||||||
|
|
||||||
|
Some common search terms to help you find the solution to your problem:
|
||||||
|
- Reverse proxy
|
||||||
|
- Enabling websockets
|
||||||
|
- SSL (https vs http)
|
||||||
|
- Configuring a static IP
|
||||||
|
- `localhost` versus IP address
|
||||||
|
- hairpin NAT
|
||||||
|
- VPN
|
||||||
|
- firewall ports
|
||||||
|
- public versus private network
|
||||||
|
- bridge versus host mode
|
||||||
|
- Docker networking
|
||||||
|
- DNS (such as EAI_AGAIN errors)
|
||||||
|
|
||||||
|
After you have followed these steps, please post the solution or steps you followed to fix the problem to help others in the future, or show that it is a problem with Audiobookshelf so we can reopen the issue.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: Close fixed issues on release.
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Close issues marked as fixed upon a release.
|
||||||
|
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
|
||||||
|
with:
|
||||||
|
label: 'awaiting release'
|
||||||
|
removeLabel: true
|
||||||
|
applyToAll: true
|
||||||
|
message: Fixed in [${releaseTag}](${releaseUrl}).
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
name: API linting
|
name: API linting
|
||||||
|
|
||||||
# Run on pull requests or pushes when there is a change to the OpenAPI file
|
# Run on pull requests or pushes when there is a change to any OpenAPI files in docs/
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- docs/
|
- 'docs/**'
|
||||||
pull_request:
|
|
||||||
paths:
|
# This action only needs read permissions
|
||||||
- docs/
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
/.nyc_output/
|
/.nyc_output/
|
||||||
/ffmpeg*
|
/ffmpeg*
|
||||||
/ffprobe*
|
/ffprobe*
|
||||||
|
/unicode*
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ RUN apk update && \
|
|||||||
tzdata \
|
tzdata \
|
||||||
ffmpeg \
|
ffmpeg \
|
||||||
make \
|
make \
|
||||||
|
gcompat \
|
||||||
python3 \
|
python3 \
|
||||||
g++ \
|
g++ \
|
||||||
tini
|
tini
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
set -e
|
set -e
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
FFMPEG_INSTALL_DIR="/usr/lib/audiobookshelf-ffmpeg"
|
|
||||||
DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
|
DEFAULT_DATA_DIR="/usr/share/audiobookshelf"
|
||||||
CONFIG_PATH="/etc/default/audiobookshelf"
|
CONFIG_PATH="/etc/default/audiobookshelf"
|
||||||
DEFAULT_PORT=13378
|
DEFAULT_PORT=13378
|
||||||
@@ -46,25 +45,6 @@ add_group() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_ffmpeg() {
|
|
||||||
echo "Starting FFMPEG Install"
|
|
||||||
|
|
||||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz"
|
|
||||||
|
|
||||||
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
|
||||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
|
||||||
mkdir "$FFMPEG_INSTALL_DIR"
|
|
||||||
chown -R 'audiobookshelf:audiobookshelf' "$FFMPEG_INSTALL_DIR"
|
|
||||||
cd "$FFMPEG_INSTALL_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
$WGET
|
|
||||||
tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 --no-same-owner
|
|
||||||
rm ffmpeg-git-amd64-static.tar.xz
|
|
||||||
|
|
||||||
echo "Good to go on Ffmpeg... hopefully"
|
|
||||||
}
|
|
||||||
|
|
||||||
setup_config() {
|
setup_config() {
|
||||||
if [ -f "$CONFIG_PATH" ]; then
|
if [ -f "$CONFIG_PATH" ]; then
|
||||||
echo "Existing config found."
|
echo "Existing config found."
|
||||||
@@ -83,8 +63,6 @@ setup_config() {
|
|||||||
|
|
||||||
config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata
|
config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata
|
||||||
CONFIG_PATH=$DEFAULT_DATA_DIR/config
|
CONFIG_PATH=$DEFAULT_DATA_DIR/config
|
||||||
FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg
|
|
||||||
FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe
|
|
||||||
PORT=$DEFAULT_PORT
|
PORT=$DEFAULT_PORT
|
||||||
HOST=$DEFAULT_HOST"
|
HOST=$DEFAULT_HOST"
|
||||||
|
|
||||||
@@ -101,5 +79,3 @@ add_group 'audiobookshelf' ''
|
|||||||
add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false'
|
add_user 'audiobookshelf' '' 'audiobookshelf' 'audiobookshelf user-daemon' '/bin/false'
|
||||||
|
|
||||||
setup_config
|
setup_config
|
||||||
|
|
||||||
install_ffmpeg
|
|
||||||
|
|||||||
+7
-31
@@ -1,19 +1,12 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Symbols Rounded';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(~static/fonts/MaterialIcons.woff2) format('woff2');
|
src: url(~static/fonts/MaterialSymbolsRounded.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
.material-symbols {
|
||||||
font-family: 'Material Icons Outlined';
|
font-family: 'Material Symbols Rounded';
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url(~static/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons {
|
|
||||||
font-family: 'Material Icons';
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -27,26 +20,9 @@
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons:not([class*="text-"]) {
|
.material-symbols.fill {
|
||||||
font-size: 1.5rem;
|
font-variation-settings:
|
||||||
}
|
'FILL' 1
|
||||||
|
|
||||||
.material-icons-outlined {
|
|
||||||
font-family: 'Material Icons Outlined';
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: normal;
|
|
||||||
text-transform: none;
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
word-wrap: normal;
|
|
||||||
direction: ltr;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons-outlined:not([class*="text-"]) {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||||
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
<span class="material-symbols text-2xl text-warning text-opacity-50"> cast </span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
<div v-if="isChromecastInitialized" class="w-6 min-w-6 h-6 ml-2 mr-1 sm:mx-2 cursor-pointer">
|
||||||
<google-cast-launcher></google-cast-launcher>
|
<google-cast-launcher></google-cast-launcher>
|
||||||
@@ -26,19 +26,19 @@
|
|||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
<span class="material-symbols text-2xl" aria-label="User Stats" role="button"></span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
<span class="material-symbols text-2xl" aria-label="Upload Media" role="button"></span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
<span class="material-symbols text-2xl" aria-label="System Settings" role="button"></span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<span class="block truncate">{{ username }}</span>
|
<span class="block truncate">{{ username }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
<span class="h-full md:ml-3 md:absolute inset-y-0 md:right-0 flex items-center justify-center md:pr-2 pointer-events-none">
|
||||||
<span class="material-icons text-xl text-gray-100">person</span>
|
<span class="material-symbols text-xl text-gray-100"></span>
|
||||||
</span>
|
</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
|
<h1 class="text-lg md:text-2xl px-4">{{ $getString('MessageItemsSelected', [numMediaItemsSelected]) }}</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
<ui-btn v-if="!isPodcastLibrary && selectedMediaItemsArePlayable" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playSelectedItems">
|
||||||
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
<span class="material-symbols fill text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ $strings.ButtonPlay }}
|
{{ $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
<ui-context-menu-dropdown v-if="contextMenuItems.length && !processingBatch" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
|
|
||||||
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.LabelDeselectAll" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
<span class="material-symbols text-3xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatch ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,13 +170,13 @@ export default {
|
|||||||
|
|
||||||
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
|
if (!this.isPodcastLibrary && this.selectedMediaItemsArePlayable) {
|
||||||
options.push({
|
options.push({
|
||||||
text: 'Quick Embed Metadata',
|
text: this.$strings.ButtonQuickEmbedMetadata,
|
||||||
action: 'quick-embed'
|
action: 'quick-embed'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
options.push({
|
options.push({
|
||||||
text: 'Re-Scan',
|
text: this.$strings.ButtonReScan,
|
||||||
action: 'rescan'
|
action: 'rescan'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -332,13 +332,13 @@ export default {
|
|||||||
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Batch delete success')
|
this.$toast.success(this.$strings.ToastBatchDeleteSuccess)
|
||||||
this.$store.commit('globals/resetSelectedMediaItems', [])
|
this.$store.commit('globals/resetSelectedMediaItems', [])
|
||||||
this.$eventBus.$emit('bookshelf_clear_selection')
|
this.$eventBus.$emit('bookshelf_clear_selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Batch delete failed', error)
|
console.error('Batch delete failed', error)
|
||||||
this.$toast.error('Batch delete failed')
|
this.$toast.error(this.$strings.ToastBatchDeleteFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
|
|||||||
@@ -167,8 +167,19 @@ export default {
|
|||||||
this.loaded = true
|
this.loaded = true
|
||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
|
// Sets the limit for the number of items to be displayed based on the viewport width.
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
let limit
|
||||||
|
if (viewportWidth >= 3240) {
|
||||||
|
limit = 15
|
||||||
|
} else if (viewportWidth >= 2880 && viewportWidth < 3240) {
|
||||||
|
limit = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitQuery = limit ? `&limit=${limit}` : ''
|
||||||
|
|
||||||
const categories = await this.$axios
|
const categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share${limitQuery}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -44,10 +44,10 @@
|
|||||||
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
<div class="bookshelfDividerCategorized h-6e w-full absolute top-0 left-0 right-0 z-20"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollLeft">
|
||||||
<span class="material-icons text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-40" @click="scrollRight">
|
||||||
<span class="material-icons text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
<span class="material-symbols text-white" :style="{ fontSize: 3.75 + 'em' }">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
|
||||||
<span v-else class="material-icons-outlined text-lg">queue_music</span>
|
<span v-else class="material-symbols text-lg"></span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
<span v-else class="material-symbols text-lg"></span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
<span class="font-mono">{{ numShowing }}</span>
|
<span class="font-mono">{{ numShowing }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
|
||||||
|
|
||||||
<!-- RSS feed -->
|
<!-- RSS feed -->
|
||||||
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
@@ -68,9 +67,6 @@
|
|||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
<!-- collapse series checkbox -->
|
|
||||||
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
|
||||||
|
|
||||||
<!-- library filter select -->
|
<!-- library filter select -->
|
||||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||||
|
|
||||||
@@ -93,14 +89,20 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
<p>{{ $strings.MessageSearchResultsFor }} "{{ searchQuery }}"</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- authors page -->
|
<!-- authors page -->
|
||||||
<template v-else-if="page === 'authors'">
|
<template v-else-if="page === 'authors'">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
<ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
|
||||||
|
|
||||||
<!-- author sort select -->
|
<!-- author sort select -->
|
||||||
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
<controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
|
||||||
|
</template>
|
||||||
|
<!-- home page -->
|
||||||
|
<template v-else-if="isHome">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,11 +153,14 @@ export default {
|
|||||||
|
|
||||||
if (this.isSeriesRemovedFromContinueListening) {
|
if (this.isSeriesRemovedFromContinueListening) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Re-Add Series to Continue Listening',
|
text: this.$strings.LabelReAddSeriesToContinueListening,
|
||||||
action: 're-add-to-continue-listening'
|
action: 're-add-to-continue-listening'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addSubtitlesMenuItem(items)
|
||||||
|
this.addCollapseSubSeriesMenuItem(items)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
seriesSortItems() {
|
seriesSortItems() {
|
||||||
@@ -183,6 +188,10 @@ export default {
|
|||||||
{
|
{
|
||||||
text: this.$strings.LabelTotalDuration,
|
text: this.$strings.LabelTotalDuration,
|
||||||
value: 'totalDuration'
|
value: 'totalDuration'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelRandomly,
|
||||||
|
value: 'random'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -318,11 +327,14 @@ export default {
|
|||||||
|
|
||||||
if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) {
|
if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) {
|
||||||
items.push({
|
items.push({
|
||||||
text: 'Export OPML',
|
text: this.$strings.LabelExportOPML,
|
||||||
action: 'export-opml'
|
action: 'export-opml'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addSubtitlesMenuItem(items)
|
||||||
|
this.addCollapseSeriesMenuItem(items)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
showPlaylists() {
|
showPlaylists() {
|
||||||
@@ -330,9 +342,98 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
addSubtitlesMenuItem(items) {
|
||||||
|
if (this.isBookLibrary && (!this.page || this.page === 'search')) {
|
||||||
|
if (this.settings.showSubtitles) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelHideSubtitles,
|
||||||
|
action: 'hide-subtitles'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelShowSubtitles,
|
||||||
|
action: 'show-subtitles'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addCollapseSeriesMenuItem(items) {
|
||||||
|
if (this.isLibraryPage && this.isBookLibrary && !this.isBatchSelecting) {
|
||||||
|
if (this.settings.collapseSeries) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelExpandSeries,
|
||||||
|
action: 'expand-series'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelCollapseSeries,
|
||||||
|
action: 'collapse-series'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addCollapseSubSeriesMenuItem(items) {
|
||||||
|
if (this.selectedSeries && this.isBookLibrary && !this.isBatchSelecting) {
|
||||||
|
if (this.settings.collapseBookSeries) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelExpandSubSeries,
|
||||||
|
action: 'expand-sub-series'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelCollapseSubSeries,
|
||||||
|
action: 'collapse-sub-series'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSubtitlesAction(action) {
|
||||||
|
if (action === 'show-subtitles') {
|
||||||
|
this.settings.showSubtitles = true
|
||||||
|
this.updateShowSubtitles()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (action === 'hide-subtitles') {
|
||||||
|
this.settings.showSubtitles = false
|
||||||
|
this.updateShowSubtitles()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
handleCollapseSeriesAction(action) {
|
||||||
|
if (action === 'collapse-series') {
|
||||||
|
this.settings.collapseSeries = true
|
||||||
|
this.updateCollapseSeries()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (action === 'expand-series') {
|
||||||
|
this.settings.collapseSeries = false
|
||||||
|
this.updateCollapseSeries()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
handleCollapseSubSeriesAction(action) {
|
||||||
|
if (action === 'collapse-sub-series') {
|
||||||
|
this.settings.collapseBookSeries = true
|
||||||
|
this.updateCollapseSubSeries()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (action === 'expand-sub-series') {
|
||||||
|
this.settings.collapseBookSeries = false
|
||||||
|
this.updateCollapseSubSeries()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
contextMenuAction({ action }) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'export-opml') {
|
if (action === 'export-opml') {
|
||||||
this.exportOPML()
|
this.exportOPML()
|
||||||
|
return
|
||||||
|
} else if (this.handleSubtitlesAction(action)) {
|
||||||
|
return
|
||||||
|
} else if (this.handleCollapseSeriesAction(action)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exportOPML() {
|
exportOPML() {
|
||||||
@@ -353,6 +454,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.markSeriesFinished()
|
this.markSeriesFinished()
|
||||||
|
} else if (this.handleSubtitlesAction(action)) {
|
||||||
|
return
|
||||||
|
} else if (this.handleCollapseSubSeriesAction(action)) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showOpenSeriesRSSFeed() {
|
showOpenSeriesRSSFeed() {
|
||||||
@@ -368,11 +473,11 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
.$get(`/api/me/series/${this.seriesId}/readd-to-continue-listening`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Series re-added to continue listening')
|
this.$toast.success(this.$strings.ToastItemUpdateSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to re-add series to continue listening', error)
|
console.error('Failed to re-add series to continue listening', error)
|
||||||
this.$toast.error('Failed to re-add series to continue listening')
|
this.$toast.error(this.$strings.ToastItemUpdateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingSeries = false
|
this.processingSeries = false
|
||||||
@@ -399,7 +504,7 @@ export default {
|
|||||||
})
|
})
|
||||||
if (!response) {
|
if (!response) {
|
||||||
console.error(`Author ${author.name} not found`)
|
console.error(`Author ${author.name} not found`)
|
||||||
this.$toast.error(`Author ${author.name} not found`)
|
this.$toast.error(this.$getString('ToastAuthorNotFound', [author.name]))
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
if (response.author.imagePath) console.log(`Author ${response.author.name} was updated`)
|
||||||
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
else console.log(`Author ${response.author.name} was updated (no image found)`)
|
||||||
@@ -417,13 +522,13 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
.$delete(`/api/libraries/${this.currentLibraryId}/issues`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Removed library items with issues')
|
this.$toast.success(this.$strings.ToastRemoveItemsWithIssuesSuccess)
|
||||||
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove library items with issues', error)
|
console.error('Failed to remove library items with issues', error)
|
||||||
this.$toast.error('Failed to remove library items with issues')
|
this.$toast.error(this.$strings.ToastRemoveItemsWithIssuesFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processingIssues = false
|
this.processingIssues = false
|
||||||
@@ -479,7 +584,10 @@ export default {
|
|||||||
updateCollapseSeries() {
|
updateCollapseSeries() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateCollapseBookSeries() {
|
updateCollapseSubSeries() {
|
||||||
|
this.saveSettings()
|
||||||
|
},
|
||||||
|
updateShowSubtitles() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
updateAuthorSort() {
|
updateAuthorSort() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||||
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<span class="material-symbols text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-3 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
<modals-changelog-view-modal v-model="showChangelogModal" :versionData="versionData" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
<p class="text-xs text-gray-300 italic">{{ Source }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ $config.version }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -114,9 +114,9 @@ export default {
|
|||||||
|
|
||||||
if (this.currentLibraryId) {
|
if (this.currentLibraryId) {
|
||||||
configRoutes.push({
|
configRoutes.push({
|
||||||
id: 'config-library-stats',
|
id: 'library-stats',
|
||||||
title: this.$strings.HeaderLibraryStats,
|
title: this.$strings.HeaderLibraryStats,
|
||||||
path: '/config/library-stats'
|
path: `/library/${this.currentLibraryId}/stats`
|
||||||
})
|
})
|
||||||
configRoutes.push({
|
configRoutes.push({
|
||||||
id: 'config-stats',
|
id: 'config-stats',
|
||||||
@@ -156,15 +156,9 @@ export default {
|
|||||||
hasUpdate() {
|
hasUpdate() {
|
||||||
return !!this.versionData.hasUpdate
|
return !!this.versionData.hasUpdate
|
||||||
},
|
},
|
||||||
latestVersion() {
|
|
||||||
return this.versionData.latestVersion
|
|
||||||
},
|
|
||||||
githubTagUrl() {
|
githubTagUrl() {
|
||||||
return this.versionData.githubTagUrl
|
return this.versionData.githubTagUrl
|
||||||
},
|
},
|
||||||
currentVersionChangelog() {
|
|
||||||
return this.versionData.currentVersionChangelog || 'No Changelog Available'
|
|
||||||
},
|
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<widgets-explicit-indicator v-if="isExplicit" />
|
<widgets-explicit-indicator v-if="isExplicit" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
|
||||||
<span class="material-icons text-sm">person</span>
|
<span class="material-symbols text-sm">person</span>
|
||||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||||
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
|
||||||
@@ -23,41 +23,48 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-400 flex items-center">
|
<div class="text-gray-400 flex items-center">
|
||||||
<span class="material-icons text-xs">schedule</span>
|
<span class="material-symbols text-xs">schedule</span>
|
||||||
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
|
<p class="font-mono text-xs sm:text-sm pl-1 sm:pl-1.5 pb-px">{{ totalDurationPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||||
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
<button :aria-label="$strings.LabelClosePlayer" class="material-symbols sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<player-ui
|
<player-ui
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
:chapters="chapters"
|
:chapters="chapters"
|
||||||
|
:current-chapter="currentChapter"
|
||||||
:paused="!isPlaying"
|
:paused="!isPlaying"
|
||||||
:loading="playerLoading"
|
:loading="playerLoading"
|
||||||
:bookmarks="bookmarks"
|
:bookmarks="bookmarks"
|
||||||
:sleep-timer-set="sleepTimerSet"
|
:sleep-timer-set="sleepTimerSet"
|
||||||
:sleep-timer-remaining="sleepTimerRemaining"
|
:sleep-timer-remaining="sleepTimerRemaining"
|
||||||
|
:sleep-timer-type="sleepTimerType"
|
||||||
:is-podcast="isPodcast"
|
:is-podcast="isPodcast"
|
||||||
|
:hasNextItemInQueue="hasNextItemInQueue"
|
||||||
@playPause="playPause"
|
@playPause="playPause"
|
||||||
@jumpForward="jumpForward"
|
@jumpForward="jumpForward"
|
||||||
@jumpBackward="jumpBackward"
|
@jumpBackward="jumpBackward"
|
||||||
@setVolume="setVolume"
|
@setVolume="setVolume"
|
||||||
@setPlaybackRate="setPlaybackRate"
|
@setPlaybackRate="setPlaybackRate"
|
||||||
@seek="seek"
|
@seek="seek"
|
||||||
|
@nextItemInQueue="playNextItemInQueue"
|
||||||
@close="closePlayer"
|
@close="closePlayer"
|
||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
@showSleepTimer="showSleepTimerModal = true"
|
@showSleepTimer="showSleepTimerModal = true"
|
||||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||||
|
@showPlayerSettings="showPlayerSettingsModal = true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||||
|
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||||
|
|
||||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" />
|
||||||
|
|
||||||
|
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -76,9 +83,10 @@ export default {
|
|||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
showPlayerQueueItemsModal: false,
|
showPlayerQueueItemsModal: false,
|
||||||
|
showPlayerSettingsModal: false,
|
||||||
sleepTimerSet: false,
|
sleepTimerSet: false,
|
||||||
sleepTimerTime: 0,
|
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
|
sleepTimerType: null,
|
||||||
sleepTimer: null,
|
sleepTimer: null,
|
||||||
displayTitle: null,
|
displayTitle: null,
|
||||||
currentPlaybackRate: 1,
|
currentPlaybackRate: 1,
|
||||||
@@ -145,6 +153,9 @@ export default {
|
|||||||
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
|
currentChapter() {
|
||||||
|
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
@@ -167,6 +178,16 @@ export default {
|
|||||||
if (!this.isMusic) return null
|
if (!this.isMusic) return null
|
||||||
return this.mediaMetadata.artists.join(', ')
|
return this.mediaMetadata.artists.join(', ')
|
||||||
},
|
},
|
||||||
|
hasNextItemInQueue() {
|
||||||
|
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
|
||||||
|
},
|
||||||
|
currentPlayerQueueIndex() {
|
||||||
|
if (!this.libraryItemId) return -1
|
||||||
|
return this.playerQueueItems.findIndex((i) => {
|
||||||
|
if (this.streamEpisode?.id) return i.episodeId === this.streamEpisode.id
|
||||||
|
return i.libraryItemId === this.libraryItemId
|
||||||
|
})
|
||||||
|
},
|
||||||
playerQueueItems() {
|
playerQueueItems() {
|
||||||
return this.$store.state.playerQueueItems || []
|
return this.$store.state.playerQueueItems || []
|
||||||
}
|
}
|
||||||
@@ -204,14 +225,18 @@ export default {
|
|||||||
this.$store.commit('setIsPlaying', isPlaying)
|
this.$store.commit('setIsPlaying', isPlaying)
|
||||||
this.updateMediaSessionPlaybackState()
|
this.updateMediaSessionPlaybackState()
|
||||||
},
|
},
|
||||||
setSleepTimer(seconds) {
|
setSleepTimer(time) {
|
||||||
this.sleepTimerSet = true
|
this.sleepTimerSet = true
|
||||||
this.sleepTimerTime = seconds
|
|
||||||
this.sleepTimerRemaining = seconds
|
|
||||||
this.runSleepTimer()
|
|
||||||
this.showSleepTimerModal = false
|
this.showSleepTimerModal = false
|
||||||
|
|
||||||
|
this.sleepTimerType = time.timerType
|
||||||
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.COUNTDOWN) {
|
||||||
|
this.runSleepTimer(time)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
runSleepTimer() {
|
runSleepTimer(time) {
|
||||||
|
this.sleepTimerRemaining = time.seconds
|
||||||
|
|
||||||
var lastTick = Date.now()
|
var lastTick = Date.now()
|
||||||
clearInterval(this.sleepTimer)
|
clearInterval(this.sleepTimer)
|
||||||
this.sleepTimer = setInterval(() => {
|
this.sleepTimer = setInterval(() => {
|
||||||
@@ -220,12 +245,23 @@ export default {
|
|||||||
this.sleepTimerRemaining -= elapsed / 1000
|
this.sleepTimerRemaining -= elapsed / 1000
|
||||||
|
|
||||||
if (this.sleepTimerRemaining <= 0) {
|
if (this.sleepTimerRemaining <= 0) {
|
||||||
this.clearSleepTimer()
|
this.sleepTimerEnd()
|
||||||
this.playerHandler.pause()
|
|
||||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
|
checkChapterEnd(time) {
|
||||||
|
if (!this.currentChapter) return
|
||||||
|
const chapterEndTime = this.currentChapter.end
|
||||||
|
const tolerance = 0.75
|
||||||
|
if (time >= chapterEndTime - tolerance) {
|
||||||
|
this.sleepTimerEnd()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sleepTimerEnd() {
|
||||||
|
this.clearSleepTimer()
|
||||||
|
this.playerHandler.pause()
|
||||||
|
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
||||||
|
},
|
||||||
cancelSleepTimer() {
|
cancelSleepTimer() {
|
||||||
this.showSleepTimerModal = false
|
this.showSleepTimerModal = false
|
||||||
this.clearSleepTimer()
|
this.clearSleepTimer()
|
||||||
@@ -235,6 +271,7 @@ export default {
|
|||||||
this.sleepTimerRemaining = 0
|
this.sleepTimerRemaining = 0
|
||||||
this.sleepTimer = null
|
this.sleepTimer = null
|
||||||
this.sleepTimerSet = false
|
this.sleepTimerSet = false
|
||||||
|
this.sleepTimerType = null
|
||||||
},
|
},
|
||||||
incrementSleepTimer(amount) {
|
incrementSleepTimer(amount) {
|
||||||
if (!this.sleepTimerSet) return
|
if (!this.sleepTimerSet) return
|
||||||
@@ -275,6 +312,10 @@ export default {
|
|||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setCurrentTime(time)
|
this.$refs.audioPlayer.setCurrentTime(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||||
|
this.checkChapterEnd(time)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setDuration(duration) {
|
setDuration(duration) {
|
||||||
this.totalDuration = duration
|
this.totalDuration = duration
|
||||||
@@ -431,6 +472,30 @@ export default {
|
|||||||
this.playerHandler.switchPlayer()
|
this.playerHandler.switchPlayer()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
playNextItemInQueue() {
|
||||||
|
if (this.hasNextItemInQueue) {
|
||||||
|
this.playQueueItem({ index: this.currentPlayerQueueIndex + 1 })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {{ index: number }} payload
|
||||||
|
*/
|
||||||
|
playQueueItem(payload) {
|
||||||
|
if (payload?.index === undefined) {
|
||||||
|
console.error('playQueueItem: No index provided')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.playerQueueItems[payload.index]) {
|
||||||
|
console.error('playQueueItem: No item found at index', payload.index)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = this.playerQueueItems[payload.index]
|
||||||
|
this.playLibraryItem({
|
||||||
|
libraryItemId: item.libraryItemId,
|
||||||
|
episodeId: item.episodeId || null,
|
||||||
|
queueItems: this.playerQueueItems
|
||||||
|
})
|
||||||
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
const libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
const episodeId = payload.episodeId || null
|
const episodeId = payload.episodeId || null
|
||||||
@@ -483,6 +548,7 @@ export default {
|
|||||||
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
||||||
this.$eventBus.$on('playback-seek', this.seek)
|
this.$eventBus.$on('playback-seek', this.seek)
|
||||||
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
||||||
|
this.$eventBus.$on('play-queue-item', this.playQueueItem)
|
||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||||
},
|
},
|
||||||
@@ -490,6 +556,7 @@ export default {
|
|||||||
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
||||||
this.$eventBus.$off('playback-seek', this.seek)
|
this.$eventBus.$off('playback-seek', this.seek)
|
||||||
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
||||||
|
this.$eventBus.$off('play-queue-item', this.playQueueItem)
|
||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2.5xl">queue_music</span>
|
<span class="material-symbols text-2.5xl"></span>
|
||||||
|
|
||||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
|
||||||
@@ -72,13 +72,21 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/narrators`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2xl">record_voice_over</span>
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||||
|
|
||||||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||||
|
|
||||||
|
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="abs-icons icon-podcast text-xl"></span>
|
<span class="abs-icons icon-podcast text-xl"></span>
|
||||||
|
|
||||||
@@ -88,7 +96,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-xl">album</span>
|
<span class="material-symbols text-xl">album</span>
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||||
|
|
||||||
@@ -96,15 +104,15 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2xl">file_download</span>
|
<span class="material-symbols text-2xl"></span>
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonDownloadQueue }}</p>
|
||||||
|
|
||||||
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : 'bg-error bg-opacity-20'">
|
||||||
<span class="material-icons text-2xl">warning</span>
|
<span class="material-symbols text-2xl">warning</span>
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||||
|
|
||||||
@@ -194,6 +202,9 @@ export default {
|
|||||||
isPlaylistsPage() {
|
isPlaylistsPage() {
|
||||||
return this.paramId === 'playlists'
|
return this.paramId === 'playlists'
|
||||||
},
|
},
|
||||||
|
isStatsPage() {
|
||||||
|
return this.$route.name === 'library-library-stats'
|
||||||
|
},
|
||||||
libraryBookshelfPage() {
|
libraryBookshelfPage() {
|
||||||
return this.$route.name === 'library-library-bookshelf-id'
|
return this.$route.name === 'library-library-bookshelf-id'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
<!-- Search icon btn -->
|
<!-- Search icon btn -->
|
||||||
<div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
<div cy-id="match" v-show="!searching && isHovering && userCanUpdate" class="absolute top-0 left-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="searchAuthor">
|
||||||
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
<ui-tooltip :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">search</span>
|
<span class="material-symbols" :style="{ fontSize: 1.125 + 'em' }">search</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
<div cy-id="edit" v-show="isHovering && !searching && userCanUpdate" class="absolute top-0 right-0 p-2e cursor-pointer hover:text-white text-gray-200 transform hover:scale-125 duration-150" @click.prevent.stop="$emit('edit', author)">
|
||||||
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
<ui-tooltip :text="$strings.LabelEdit" direction="bottom">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.125 + 'em' }">edit</span>
|
<span class="material-symbols" :style="{ fontSize: 1.125 + 'em' }">edit</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
||||||
<p class="truncate text-sm">{{ name }}</p>
|
<p class="truncate text-sm">{{ name }}</p>
|
||||||
|
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -23,6 +24,9 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
name() {
|
name() {
|
||||||
return this.author.name
|
return this.author.name
|
||||||
|
},
|
||||||
|
numBooks() {
|
||||||
|
return this.author.numBooks
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
@@ -33,7 +37,7 @@ export default {
|
|||||||
<style>
|
<style>
|
||||||
.authorSearchCardContent {
|
.authorSearchCardContent {
|
||||||
width: calc(100% - 80px);
|
width: calc(100% - 80px);
|
||||||
height: 40px;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,254 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div ref="wrapper" class="relative pointer-events-none" :style="{ width: standardWidth * 0.8 * 1.1 * scale + 'px', height: standardHeight * 1.1 * scale + 'px', marginBottom: 20 + 'px', marginTop: 15 + 'px' }">
|
|
||||||
<div ref="card" class="wrap absolute origin-center transform duration-200" :style="{ transform: `scale(${scale * scaleMultiplier}) translateY(${hover2 ? '-40%' : '-50%'})` }">
|
|
||||||
<div class="perspective">
|
|
||||||
<div class="book-wrap transform duration-100 pointer-events-auto" :class="hover2 ? 'z-80' : 'rotate'" @mouseover="hover = true" @mouseout="hover = false">
|
|
||||||
<div class="book book-1 box-shadow-book3d" ref="front"></div>
|
|
||||||
<div class="title book-1 pointer-events-none" ref="left"></div>
|
|
||||||
<div class="bottom book-1 pointer-events-none" ref="bottom"></div>
|
|
||||||
<div class="book-back book-1 pointer-events-none">
|
|
||||||
<div class="text pointer-events-none">
|
|
||||||
<h3 class="mb-4">Book Back</h3>
|
|
||||||
<p>
|
|
||||||
<span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Sunt earum doloremque aliquam culpa dolor nostrum consequatur quas dicta? Molestias repellendus minima pariatur libero vel, reiciendis optio magnam rerum, labore corporis.</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
src: String,
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
default: 200
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hover: false,
|
|
||||||
hover2: false,
|
|
||||||
standardWidth: 200,
|
|
||||||
standardHeight: 320,
|
|
||||||
isAttached: true,
|
|
||||||
pageX: 0,
|
|
||||||
pageY: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
src(newVal) {
|
|
||||||
this.setCover()
|
|
||||||
},
|
|
||||||
width(newVal) {
|
|
||||||
this.init()
|
|
||||||
},
|
|
||||||
hover(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
this.unattach()
|
|
||||||
} else {
|
|
||||||
this.attach()
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
this.hover2 = newVal
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
scaleMultiplier() {
|
|
||||||
return this.hover2 ? 1.25 : 1
|
|
||||||
},
|
|
||||||
scale() {
|
|
||||||
var scale = this.width / this.standardWidth
|
|
||||||
return scale
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
unattach() {
|
|
||||||
if (this.$refs.card && this.isAttached) {
|
|
||||||
var bookshelf = document.getElementById('bookshelf')
|
|
||||||
if (bookshelf) {
|
|
||||||
var pos = this.$refs.wrapper.getBoundingClientRect()
|
|
||||||
|
|
||||||
this.pageX = pos.x
|
|
||||||
this.pageY = pos.y
|
|
||||||
document.body.appendChild(this.$refs.card)
|
|
||||||
this.$refs.card.style.left = this.pageX + 'px'
|
|
||||||
this.$refs.card.style.top = this.pageY + 'px'
|
|
||||||
this.$refs.card.style.zIndex = 50
|
|
||||||
this.isAttached = false
|
|
||||||
} else if (bookshelf) {
|
|
||||||
console.log(this.pageX, this.pageY)
|
|
||||||
this.isAttached = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
attach() {
|
|
||||||
if (this.$refs.card && !this.isAttached) {
|
|
||||||
if (this.$refs.wrapper) {
|
|
||||||
this.isAttached = true
|
|
||||||
|
|
||||||
this.$refs.wrapper.appendChild(this.$refs.card)
|
|
||||||
this.$refs.card.style.left = '0px'
|
|
||||||
this.$refs.card.style.top = '0px'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Is attached already', this.isAttached)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init() {
|
|
||||||
var standardWidth = this.standardWidth
|
|
||||||
document.documentElement.style.setProperty('--book-w', standardWidth + 'px')
|
|
||||||
document.documentElement.style.setProperty('--book-wx', standardWidth + 1 + 'px')
|
|
||||||
document.documentElement.style.setProperty('--book-h', standardWidth * 1.6 + 'px')
|
|
||||||
document.documentElement.style.setProperty('--book-d', 40 + 'px')
|
|
||||||
},
|
|
||||||
setElBg(el) {
|
|
||||||
el.style.backgroundImage = `url("${this.src}")`
|
|
||||||
el.style.backgroundSize = 'cover'
|
|
||||||
el.style.backgroundPosition = 'center center'
|
|
||||||
el.style.backgroundRepeat = 'no-repeat'
|
|
||||||
},
|
|
||||||
setCover() {
|
|
||||||
if (this.$refs.front) {
|
|
||||||
this.setElBg(this.$refs.front)
|
|
||||||
}
|
|
||||||
if (this.$refs.bottom) {
|
|
||||||
this.setElBg(this.$refs.bottom)
|
|
||||||
this.$refs.bottom.style.backgroundSize = '2000%'
|
|
||||||
this.$refs.bottom.style.filter = 'blur(1px)'
|
|
||||||
}
|
|
||||||
if (this.$refs.left) {
|
|
||||||
this.setElBg(this.$refs.left)
|
|
||||||
this.$refs.left.style.backgroundSize = '2000%'
|
|
||||||
this.$refs.left.style.filter = 'blur(1px)'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.setCover()
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* :root {
|
|
||||||
--book-w: 200px;
|
|
||||||
--book-h: 320px;
|
|
||||||
--book-d: 30px;
|
|
||||||
--book-wx: 201px;
|
|
||||||
} */
|
|
||||||
/*
|
|
||||||
.wrap {
|
|
||||||
width: calc(1.1 * var(--book-w));
|
|
||||||
height: calc(1.1 * var(--book-h));
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.perspective {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
perspective: 600px;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-wrap {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
transform-style: preserve-3d;
|
|
||||||
transition: 'all ease-out 0.6s';
|
|
||||||
}
|
|
||||||
|
|
||||||
.book {
|
|
||||||
width: var(--book-w);
|
|
||||||
height: var(--book-h);
|
|
||||||
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
|
||||||
background-size: cover;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
content: '';
|
|
||||||
height: var(--book-h);
|
|
||||||
width: var(--book-d);
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
left: calc(var(--book-wx) * -1);
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
background: #444;
|
|
||||||
transform: rotateY(-80deg) translateX(-14px);
|
|
||||||
|
|
||||||
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
|
||||||
background-size: 5000%;
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom {
|
|
||||||
content: '';
|
|
||||||
height: var(--book-d);
|
|
||||||
width: var(--book-w);
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: var(--book-h);
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
margin: auto;
|
|
||||||
background: #444;
|
|
||||||
transform: rotateY(0deg) rotateX(90deg) translateY(-15px) translateX(-2.5px) skewX(10deg);
|
|
||||||
|
|
||||||
background: url(https://covers.openlibrary.org/b/id/8303020-L.jpg) no-repeat center center;
|
|
||||||
background-size: 5000%;
|
|
||||||
filter: blur(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-back {
|
|
||||||
width: var(--book-w);
|
|
||||||
height: var(--book-h);
|
|
||||||
background-color: #444;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
transform: rotate(180deg) translateZ(-30px) translateX(5px);
|
|
||||||
}
|
|
||||||
.book-back .text {
|
|
||||||
transform: rotateX(180deg);
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0px;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.book-back .text h3 {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.book-back .text span {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.book-wrap.rotate {
|
|
||||||
transform: rotateY(30deg) rotateX(0deg);
|
|
||||||
}
|
|
||||||
.book-wrap.flip {
|
|
||||||
transform: rotateY(180deg);
|
|
||||||
} */
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
|
<span class="material-symbols text-2xl text-gray-200">category</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||||
|
<p class="truncate text-sm">{{ genre }}</p>
|
||||||
|
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
genre: String,
|
||||||
|
numItems: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tagSearchCardContent {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,15 +2,9 @@
|
|||||||
<div class="flex items-center h-full px-1 overflow-hidden">
|
<div class="flex items-center h-full px-1 overflow-hidden">
|
||||||
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="flex-grow px-2 audiobookSearchCardContent">
|
<div class="flex-grow px-2 audiobookSearchCardContent">
|
||||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
<p v-if="subtitle" class="truncate text-xs text-gray-300">{{ subtitle }}</p>
|
||||||
|
<p class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
|
||||||
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" />
|
|
||||||
|
|
||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">{{ $getString('LabelByAuthor', [authorName]) }}</p>
|
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
|
||||||
|
|
||||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -21,10 +15,7 @@ export default {
|
|||||||
libraryItem: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
}
|
||||||
search: String,
|
|
||||||
matchKey: String,
|
|
||||||
matchText: String
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -58,23 +49,6 @@ export default {
|
|||||||
authorName() {
|
authorName() {
|
||||||
if (this.isPodcast) return this.mediaMetadata.author || 'Unknown'
|
if (this.isPodcast) return this.mediaMetadata.author || 'Unknown'
|
||||||
return this.mediaMetadata.authorName || 'Unknown'
|
return this.mediaMetadata.authorName || 'Unknown'
|
||||||
},
|
|
||||||
matchHtml() {
|
|
||||||
if (!this.matchText || !this.search) return ''
|
|
||||||
|
|
||||||
// This used to highlight the part of the search found
|
|
||||||
// but with removing commas periods etc this is no longer plausible
|
|
||||||
const html = this.matchText
|
|
||||||
|
|
||||||
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
|
|
||||||
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
|
|
||||||
if (this.matchKey === 'authors') this.$getString('LabelByAuthor', [html])
|
|
||||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
|
||||||
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
|
||||||
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
|
|
||||||
if (this.matchKey === 'narrators') return `<p class="truncate">${this.$strings.LabelNarrator}: ${html}</p>`
|
|
||||||
return `${html}`
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center px-1 overflow-hidden">
|
<div class="flex items-center px-1 overflow-hidden">
|
||||||
<div class="w-8 flex items-center justify-center">
|
<div class="w-8 flex items-center justify-center">
|
||||||
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span>
|
<span v-if="isFinished" :class="taskIconStatus" class="material-symbols text-base">{{ actionIcon }}</span>
|
||||||
<widgets-loading-spinner v-else />
|
<widgets-loading-spinner v-else />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 taskRunningCardContent">
|
<div class="flex-grow px-2 taskRunningCardContent">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
|
<div v-if="!processing && !uploadFailed && !uploadSuccess" class="absolute -top-3 -right-3 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-error cursor-pointer" @click="$emit('remove')">
|
||||||
<span class="text-base text-white text-opacity-80 font-mono material-icons">close</span>
|
<span class="text-base text-white text-opacity-80 font-mono material-symbols">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="!uploadSuccess && !uploadFailed">
|
<template v-if="!uploadSuccess && !uploadFailed">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
||||||
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
||||||
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
|
<div class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer" @click="fetchMetadata">
|
||||||
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
|
<span class="text-base text-white text-opacity-80 font-mono material-symbols">sync</span>
|
||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,28 +45,28 @@
|
|||||||
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
|
<div cy-id="overlay" v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen) && !processing" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded md:block" :class="overlayWrapperClasslist">
|
||||||
<div cy-id="playButton" v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
<div cy-id="playButton" v-show="showPlayButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="play">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">play_circle_filled</span>
|
<span class="material-symbols fill" :style="{ fontSize: playIconFontSize + 'em' }">play_arrow</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="readButton" v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
|
<div cy-id="readButton" v-show="showReadButton" class="h-full flex items-center justify-center pointer-events-none">
|
||||||
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto" @click.stop.prevent="clickReadEBook">
|
||||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'em' }">auto_stories</span>
|
<span class="material-symbols" :style="{ fontSize: playIconFontSize + 'em' }">auto_stories</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="editButton" v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 + 'em' }" @click.stop.prevent="editClick">
|
<div cy-id="editButton" v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-150 top-0 right-0" :style="{ padding: 0.375 + 'em' }" @click.stop.prevent="editClick">
|
||||||
<span class="material-icons" :style="{ fontSize: 1 + 'em' }">edit</span>
|
<span class="material-symbols" :style="{ fontSize: 1 + 'em' }">edit</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Radio button -->
|
<!-- Radio button -->
|
||||||
<div cy-id="selectedRadioButton" v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 + 'em', left: 0.375 + 'em' }" @click.stop.prevent="selectBtnClick">
|
<div cy-id="selectedRadioButton" v-if="!isAuthorBookshelfView" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 + 'em', left: 0.375 + 'em' }" @click.stop.prevent="selectBtnClick">
|
||||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 + 'em' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
<span class="material-symbols" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 + 'em' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More Menu Icon -->
|
<!-- More Menu Icon -->
|
||||||
<div cy-id="moreButton" ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 + 'em', right: 0.375 + 'em' }" @click.stop.prevent="clickShowMore">
|
<div cy-id="moreButton" ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 + 'em', right: 0.375 + 'em' }" @click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 + 'em' }">more_vert</span>
|
<span class="material-symbols" :style="{ fontSize: 1.2 + 'em' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="ebookFormat" v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 + 'em', left: 0.375 + 'em' }">
|
<div cy-id="ebookFormat" v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 + 'em', left: 0.375 + 'em' }">
|
||||||
@@ -87,17 +87,17 @@
|
|||||||
<!-- Error widget -->
|
<!-- Error widget -->
|
||||||
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" class="absolute bottom-4e left-0 z-10">
|
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" class="absolute bottom-4e left-0 z-10">
|
||||||
<div :style="{ height: 1.5 + 'em', width: 2.5 + 'em' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
<div :style="{ height: 1.5 + 'em', width: 2.5 + 'em' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||||
<span class="material-icons text-red-100 pr-1e" :style="{ fontSize: 0.875 + 'em' }">priority_high</span>
|
<span class="material-symbols text-red-100 pr-1e" :style="{ fontSize: 0.875 + 'em' }">priority_high</span>
|
||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<!-- rss feed icon -->
|
<!-- rss feed icon -->
|
||||||
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
<div cy-id="rssFeed" v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 + 'em' }">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- media item shared icon -->
|
<!-- media item shared icon -->
|
||||||
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
|
<div cy-id="mediaItemShare" v-if="mediaItemShare && !isSelectionMode && !isHovering" class="absolute text-success left-0 z-10" :style="{ padding: 0.375 + 'em', top: rssFeed ? '2em' : '0px' }">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
<span class="material-symbols" :style="{ fontSize: 1.5 + 'em' }">public</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
@@ -132,6 +132,9 @@
|
|||||||
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<ui-tooltip v-if="showSubtitles" :text="displaySubtitle" :disabled="!displaySubtitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
|
||||||
|
<p cy-id="subtitle" class="truncate" ref="displaySubtitle" :style="{ fontSize: 0.6 + 'em' }">{{ displaySubtitle }}</p>
|
||||||
|
</ui-tooltip>
|
||||||
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || ' ' }}</p>
|
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || ' ' }}</p>
|
||||||
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
<p cy-id="line3" v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,6 +174,7 @@ export default {
|
|||||||
selected: false,
|
selected: false,
|
||||||
isSelectionMode: false,
|
isSelectionMode: false,
|
||||||
displayTitleTruncated: false,
|
displayTitleTruncated: false,
|
||||||
|
displaySubtitleTruncated: false,
|
||||||
showCoverBg: false
|
showCoverBg: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -197,23 +201,6 @@ export default {
|
|||||||
// This method returns immediately without waiting for the DOM to update
|
// This method returns immediately without waiting for the DOM to update
|
||||||
return this.coverWidth
|
return this.coverWidth
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
cardHeight() {
|
|
||||||
// This method returns immediately without waiting for the DOM to update
|
|
||||||
return this.coverHeight + this.detailsHeight
|
|
||||||
},
|
|
||||||
detailsHeight() {
|
|
||||||
if (!this.isAlternativeBookshelfView) return 0
|
|
||||||
const lineHeight = 1.5
|
|
||||||
const remSize = 16
|
|
||||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
|
||||||
const titleHeight = 0.9 * baseHeight
|
|
||||||
const line2Height = 0.8 * baseHeight
|
|
||||||
const line3Height = this.displaySortLine ? 0.8 * baseHeight : 0
|
|
||||||
const marginHeight = 8 * 2 * this.sizeMultiplier // py-2
|
|
||||||
return titleHeight + line2Height + line3Height + marginHeight
|
|
||||||
},
|
|
||||||
*/
|
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
return this.store.getters['user/getSizeMultiplier']
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
},
|
},
|
||||||
@@ -237,7 +224,7 @@ export default {
|
|||||||
return this._libraryItem.mediaType
|
return this._libraryItem.mediaType
|
||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
|
||||||
},
|
},
|
||||||
isMusic() {
|
isMusic() {
|
||||||
return this.mediaType === 'music'
|
return this.mediaType === 'music'
|
||||||
@@ -339,6 +326,13 @@ export default {
|
|||||||
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
||||||
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix || '\u00A0' : this.title || '\u00A0'
|
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix || '\u00A0' : this.title || '\u00A0'
|
||||||
},
|
},
|
||||||
|
displaySubtitle() {
|
||||||
|
if (!this.libraryItem) return '\u00A0'
|
||||||
|
if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books`
|
||||||
|
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
|
||||||
|
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
|
||||||
|
return ''
|
||||||
|
},
|
||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
@@ -352,14 +346,14 @@ export default {
|
|||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
if (this.collapsedSeries) return null
|
if (this.collapsedSeries) return null
|
||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
if (this.orderBy === 'mtimeMs') return this.$getString('LabelFileModifiedDate', [this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)])
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
if (this.orderBy === 'birthtimeMs') return this.$getString('LabelFileBornDate', [this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)])
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
if (this.orderBy === 'addedAt') return this.$getString('LabelAddedDate', [this.$formatDate(this._libraryItem.addedAt, this.dateFormat)])
|
||||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
if (this.orderBy === 'media.duration') return this.$strings.LabelDuration + ': ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return this.$strings.LabelSize + ': ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} ` + this.$strings.LabelEpisodes
|
||||||
if (this.orderBy === 'media.metadata.publishedYear') {
|
if (this.orderBy === 'media.metadata.publishedYear') {
|
||||||
if (this.mediaMetadata.publishedYear) return 'Published ' + this.mediaMetadata.publishedYear
|
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
|
||||||
return '\u00A0'
|
return '\u00A0'
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
@@ -644,6 +638,9 @@ export default {
|
|||||||
},
|
},
|
||||||
mediaItemShare() {
|
mediaItemShare() {
|
||||||
return this._libraryItem.mediaItemShare || null
|
return this._libraryItem.mediaItemShare || null
|
||||||
|
},
|
||||||
|
showSubtitles() {
|
||||||
|
return !this.isPodcast && this.store.getters['user/getUserSetting']('showSubtitles')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -685,6 +682,9 @@ export default {
|
|||||||
if (this.$refs.displayTitle) {
|
if (this.$refs.displayTitle) {
|
||||||
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
|
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
|
||||||
}
|
}
|
||||||
|
if (this.$refs.displaySubtitle) {
|
||||||
|
this.displaySubtitleTruncated = this.$refs.displaySubtitle.scrollWidth > this.$refs.displaySubtitle.clientWidth
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
@@ -710,7 +710,7 @@ export default {
|
|||||||
toggleFinished(confirmed = false) {
|
toggleFinished(confirmed = false) {
|
||||||
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
|
message: this.$getString('MessageConfirmMarkItemFinished', [this.displayTitle]),
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.toggleFinished(true)
|
this.toggleFinished(true)
|
||||||
@@ -755,18 +755,18 @@ export default {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
var result = data.result
|
var result = data.result
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
this.$toast.error(this.$getString('ToastRescanFailed', [this.displayTitle]))
|
||||||
} else if (result === 'UPDATED') {
|
} else if (result === 'UPDATED') {
|
||||||
this.$toast.success(`Re-Scan complete item was updated`)
|
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||||
} else if (result === 'UPTODATE') {
|
} else if (result === 'UPTODATE') {
|
||||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
this.$toast.success(this.$strings.ToastRescanUpToDate)
|
||||||
} else if (result === 'REMOVED') {
|
} else if (result === 'REMOVED') {
|
||||||
this.$toast.error(`Re-Scan complete item was removed`)
|
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to scan library item', error)
|
console.error('Failed to scan library item', error)
|
||||||
this.$toast.error('Failed to scan library item')
|
this.$toast.error(this.$strings.ToastScanFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -823,7 +823,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove series from home', error)
|
console.error('Failed to remove series from home', error)
|
||||||
this.$toast.error('Failed to update user')
|
this.$toast.error(this.$strings.ToastFailedToUpdateUser)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -841,7 +841,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to hide item from home', error)
|
console.error('Failed to hide item from home', error)
|
||||||
this.$toast.error('Failed to update user')
|
this.$toast.error(this.$strings.ToastFailedToUpdateUser)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -856,7 +856,7 @@ export default {
|
|||||||
episodeId: this.recentEpisode.id,
|
episodeId: this.recentEpisode.id,
|
||||||
title: this.recentEpisode.title,
|
title: this.recentEpisode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: this.recentEpisode.publishedAt ? `Published ${this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
caption: this.recentEpisode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(this.recentEpisode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||||
duration: this.recentEpisode.audioFile.duration || null,
|
duration: this.recentEpisode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
}
|
}
|
||||||
@@ -906,11 +906,11 @@ export default {
|
|||||||
axios
|
axios
|
||||||
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
.$delete(`/api/items/${this.libraryItemId}?hard=${hardDelete ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Item deleted')
|
this.$toast.success(this.$strings.ToastItemDeletedSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to delete item', error)
|
console.error('Failed to delete item', error)
|
||||||
this.$toast.error('Failed to delete item')
|
this.$toast.error(this.$strings.ToastItemDeletedFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -1016,7 +1016,7 @@ export default {
|
|||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
subtitle: this.mediaMetadata.title,
|
subtitle: this.mediaMetadata.title,
|
||||||
caption: episode.publishedAt ? `Published ${this.$formatDate(episode.publishedAt, this.dateFormat)}` : 'Unknown publish date',
|
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
|
||||||
duration: episode.audioFile.duration || null,
|
duration: episode.audioFile.duration || null,
|
||||||
coverPath: this.media.coverPath || null
|
coverPath: this.media.coverPath || null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||||
<span class="material-icons text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-symbols text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||||
@@ -57,23 +57,11 @@ export default {
|
|||||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
cardWidth() {
|
cardWidth() {
|
||||||
return this.width || this.coverHeight * 2
|
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
|
||||||
},
|
},
|
||||||
coverHeight() {
|
coverHeight() {
|
||||||
return this.height * this.sizeMultiplier
|
return this.height * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
cardHeight() {
|
|
||||||
return this.coverHeight + this.bottomTextHeight
|
|
||||||
},
|
|
||||||
bottomTextHeight() {
|
|
||||||
if (!this.isAlternativeBookshelfView) return 0 // bottom text appears on top of the divider
|
|
||||||
const lineHeight = 1.5
|
|
||||||
const remSize = 16
|
|
||||||
const baseHeight = this.sizeMultiplier * lineHeight * remSize
|
|
||||||
const titleHeight = this.labelFontSize * baseHeight
|
|
||||||
const paddingHeight = 4 * 2 * this.sizeMultiplier // py-1
|
|
||||||
return titleHeight + paddingHeight
|
|
||||||
},
|
|
||||||
labelFontSize() {
|
labelFontSize() {
|
||||||
if (this.width < 160) return 0.75
|
if (this.width < 160) return 0.75
|
||||||
return 0.9
|
return 0.9
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||||
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
<div class="absolute pointer-events-auto" :style="{ top: 0.5 + 'em', right: 0.5 + 'em' }" @click.stop.prevent="clickEdit">
|
||||||
<span class="material-icons text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
<span class="material-symbols text-white text-opacity-75 hover:text-opacity-100" :style="{ fontSize: 1.25 + 'em' }">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 + 'em' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
<span cy-id="rssFeedMarker" v-if="!isHovering && rssFeed" class="absolute z-10 material-symbols text-success" :style="{ top: 0.5 + 'em', left: 0.5 + 'em', fontSize: 1.5 + 'em' }">rss_feed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
<div cy-id="standardBottomText" v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6e h-6e rounded-md text-center" :style="{ width: Math.min(200, cardWidth) + 'px' }">
|
||||||
@@ -65,7 +65,7 @@ export default {
|
|||||||
return this.store.getters['libraries/getBookCoverAspectRatio']
|
return this.store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
cardWidth() {
|
cardWidth() {
|
||||||
return this.width || this.coverHeight * 2
|
return this.width || (this.coverHeight / this.bookCoverAspectRatio) * 2
|
||||||
},
|
},
|
||||||
coverHeight() {
|
coverHeight() {
|
||||||
return this.height * this.sizeMultiplier
|
return this.height * this.sizeMultiplier
|
||||||
@@ -81,22 +81,22 @@ export default {
|
|||||||
return this.store.getters['user/getSizeMultiplier']
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
return this.series ? this.series.id : ''
|
return this.series?.id || ''
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.series ? this.series.name : ''
|
return this.series?.name || ''
|
||||||
},
|
},
|
||||||
nameIgnorePrefix() {
|
nameIgnorePrefix() {
|
||||||
return this.series ? this.series.nameIgnorePrefix : ''
|
return this.series?.nameIgnorePrefix || ''
|
||||||
},
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title || '\u00A0'
|
||||||
return this.title || '\u00A0'
|
return this.title || '\u00A0'
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
switch (this.orderBy) {
|
switch (this.orderBy) {
|
||||||
case 'addedAt':
|
case 'addedAt':
|
||||||
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
|
return this.$getString('LabelAddedDate', [this.$formatDate(this.addedAt, this.dateFormat)])
|
||||||
case 'totalDuration':
|
case 'totalDuration':
|
||||||
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
||||||
case 'lastBookUpdated':
|
case 'lastBookUpdated':
|
||||||
@@ -110,13 +110,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
books() {
|
books() {
|
||||||
return this.series ? this.series.books || [] : []
|
return this.series?.books || []
|
||||||
},
|
},
|
||||||
addedAt() {
|
addedAt() {
|
||||||
return this.series ? this.series.addedAt : 0
|
return this.series?.addedAt || 0
|
||||||
},
|
},
|
||||||
totalDuration() {
|
totalDuration() {
|
||||||
return this.series ? this.series.totalDuration : 0
|
return this.series?.totalDuration || 0
|
||||||
},
|
},
|
||||||
seriesBookProgress() {
|
seriesBookProgress() {
|
||||||
return this.books
|
return this.books
|
||||||
@@ -161,7 +161,7 @@ export default {
|
|||||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
},
|
},
|
||||||
rssFeed() {
|
rssFeed() {
|
||||||
return this.series ? this.series.rssFeed : null
|
return this.series?.rssFeed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(name)}`">
|
||||||
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
<div cy-id="card" :style="{ width: cardWidth + 'px', height: cardHeight + 'px', fontSize: sizeMultiplier + 'rem' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
||||||
<span class="material-icons-outlined text-[10em]">record_voice_over</span>
|
<span class="material-symbols text-[10em]"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Narrator name & num books overlay -->
|
<!-- Narrator name & num books overlay -->
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full px-1 overflow-hidden">
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
<div class="w-10 h-10 flex items-center justify-center">
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
|
<span class="material-symbols text-2xl text-gray-200"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||||
<p class="truncate text-sm">{{ narrator }}</p>
|
<p class="truncate text-sm">{{ narrator }}</p>
|
||||||
|
<p class="text-xs text-gray-400">{{ $getString('LabelXBooks', [numBooks]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -12,7 +13,8 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
narrator: String
|
narrator: String,
|
||||||
|
numBooks: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -26,7 +28,7 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.narratorSearchCardContent {
|
.narratorSearchCardContent {
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
height: 40px;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
<p class="text-base md:text-lg font-semibold pr-4">{{ eventName }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">Fire onTest Event</ui-btn>
|
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="fireTestEventAndSucceed">{{ this.$strings.ButtonFireOnTest }}</ui-btn>
|
||||||
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">Fire & Fail</ui-btn>
|
<ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" color="red-600" @click.stop="fireTestEventAndFail">{{ this.$strings.ButtonFireAndFail }}</ui-btn>
|
||||||
<!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
|
<!-- <ui-btn v-if="eventName === 'onTest' && notification.enabled" :loading="testing" small class="mr-2" @click.stop="rapidFireTestEvents">Rapid Fire</ui-btn> -->
|
||||||
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">Test</ui-btn>
|
<ui-btn v-else-if="notification.enabled" :loading="sendingTest" small class="mr-2" @click.stop="sendTestClick">{{ this.$strings.ButtonTest }}</ui-btn>
|
||||||
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">Enable</ui-btn>
|
<ui-btn v-else :loading="enabling" small color="success" class="mr-2" @click="enableNotification">{{ this.$strings.ButtonEnable }}</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
|
<ui-icon-btn :size="7" icon-font-size="1.1rem" icon="edit" class="mr-2" @click="editNotification" />
|
||||||
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
<ui-icon-btn bg-color="error" :size="7" icon-font-size="1.2rem" icon="delete" @click="deleteNotificationClick" />
|
||||||
@@ -65,12 +65,12 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
|
.$get(`/api/notifications/test?fail=${intentionallyFail ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Triggered onTest Event')
|
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger onTest event')
|
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.testing = false
|
this.testing = false
|
||||||
@@ -91,7 +91,7 @@ export default {
|
|||||||
// End testing functions
|
// End testing functions
|
||||||
sendTestClick() {
|
sendTestClick() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Trigger this notification with test data?`,
|
message: this.$strings.MessageConfirmNotificationTestTrigger,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.sendTest()
|
this.sendTest()
|
||||||
@@ -106,12 +106,12 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/notifications/${this.notification.id}/test`)
|
.$get(`/api/notifications/${this.notification.id}/test`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Triggered test notification')
|
this.$toast.success(this.$strings.ToastNotificationTestTriggerSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
const errorMsg = error.response ? error.response.data : null
|
const errorMsg = error.response ? error.response.data : null
|
||||||
this.$toast.error(`Failed: ${errorMsg}` || 'Failed to trigger test notification')
|
this.$toast.error(`Failed: ${errorMsg}` || this.$strings.ToastNotificationTestTriggerFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.sendingTest = false
|
this.sendingTest = false
|
||||||
@@ -127,11 +127,10 @@ export default {
|
|||||||
.$patch(`/api/notifications/${this.notification.id}`, payload)
|
.$patch(`/api/notifications/${this.notification.id}`, payload)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Notification enabled')
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update notification', error)
|
console.error('Failed to update notification', error)
|
||||||
this.$toast.error('Failed to update notification')
|
this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.enabling = false
|
this.enabling = false
|
||||||
@@ -139,7 +138,7 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteNotificationClick() {
|
deleteNotificationClick() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want to delete this notification?`,
|
message: this.$strings.MessageConfirmDeleteNotification,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.deleteNotification()
|
this.deleteNotification()
|
||||||
@@ -155,11 +154,10 @@ export default {
|
|||||||
.$delete(`/api/notifications/${this.notification.id}`)
|
.$delete(`/api/notifications/${this.notification.id}`)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Deleted notification')
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error('Failed to delete notification')
|
this.$toast.error(this.$strings.ToastNotificationDeleteFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.deleting = false
|
this.deleting = false
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full px-1 overflow-hidden">
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
<div class="w-10 h-10 flex items-center justify-center">
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
<span class="material-icons text-2xl text-gray-200">local_offer</span>
|
<span class="material-symbols text-2xl text-gray-200">local_offer</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
<div class="flex-grow px-2 tagSearchCardContent h-full">
|
||||||
<p class="truncate text-sm">{{ tag }}</p>
|
<p class="truncate text-sm">{{ tag }}</p>
|
||||||
|
<p class="text-xs text-gray-400">{{ $getString('LabelXItems', [numItems]) }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -12,7 +13,8 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
tag: String
|
tag: String,
|
||||||
|
numItems: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -26,7 +28,7 @@ export default {
|
|||||||
<style>
|
<style>
|
||||||
.tagSearchCardContent {
|
.tagSearchCardContent {
|
||||||
width: calc(100% - 40px);
|
width: calc(100% - 40px);
|
||||||
height: 40px;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||||
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<!-- selected checkmark icon -->
|
<!-- selected checkmark icon -->
|
||||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||||
<span class="material-icons text-base text-yellow-400">check</span>
|
<span class="material-symbols text-base text-yellow-400">check</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||||
</form>
|
</form>
|
||||||
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
|
||||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
<span v-if="!search" class="material-symbols" style="font-size: 1.2rem"></span>
|
||||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<template v-for="item in bookResults">
|
<template v-for="item in bookResults">
|
||||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
<cards-item-search-card :library-item="item.libraryItem" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<template v-for="item in podcastResults">
|
<template v-for="item in podcastResults">
|
||||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
<cards-item-search-card :library-item="item.libraryItem" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelAuthors }}</p>
|
||||||
<template v-for="item in authorResults">
|
<template v-for="item in authorResults">
|
||||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
<nuxt-link :to="`/author/${item.id}`">
|
||||||
<cards-author-search-card :author="item" />
|
<cards-author-search-card :author="item" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
@@ -59,9 +59,18 @@
|
|||||||
|
|
||||||
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p>
|
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelTags }}</p>
|
||||||
<template v-for="item in tagResults">
|
<template v-for="item in tagResults">
|
||||||
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="`tag.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
||||||
<cards-tag-search-card :tag="item.name" />
|
<cards-tag-search-card :tag="item.name" :num-items="item.numItems" />
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<p v-if="genreResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelGenres }}</p>
|
||||||
|
<template v-for="item in genreResults">
|
||||||
|
<li :key="`genre.${item.name}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(item.name)}`">
|
||||||
|
<cards-genre-search-card :genre="item.name" :num-items="item.numItems" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,7 +79,7 @@
|
|||||||
<template v-for="narrator in narratorResults">
|
<template v-for="narrator in narratorResults">
|
||||||
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||||
<cards-narrator-search-card :narrator="narrator.name" />
|
<cards-narrator-search-card :narrator="narrator.name" :num-books="narrator.numBooks" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -95,6 +104,7 @@ export default {
|
|||||||
authorResults: [],
|
authorResults: [],
|
||||||
seriesResults: [],
|
seriesResults: [],
|
||||||
tagResults: [],
|
tagResults: [],
|
||||||
|
genreResults: [],
|
||||||
narratorResults: [],
|
narratorResults: [],
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
lastSearch: null
|
lastSearch: null
|
||||||
@@ -105,7 +115,7 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
totalResults() {
|
totalResults() {
|
||||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.podcastResults.length + this.narratorResults.length
|
return this.bookResults.length + this.seriesResults.length + this.authorResults.length + this.tagResults.length + this.genreResults.length + this.podcastResults.length + this.narratorResults.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -116,7 +126,7 @@ export default {
|
|||||||
if (!this.search) return
|
if (!this.search) return
|
||||||
var search = this.search
|
var search = this.search
|
||||||
this.clearResults()
|
this.clearResults()
|
||||||
this.$router.push(`/library/${this.currentLibraryId}/search?q=${search}`)
|
this.$router.push(`/library/${this.currentLibraryId}/search?q=${encodeURIComponent(search)}`)
|
||||||
},
|
},
|
||||||
clearResults() {
|
clearResults() {
|
||||||
this.search = null
|
this.search = null
|
||||||
@@ -126,6 +136,7 @@ export default {
|
|||||||
this.authorResults = []
|
this.authorResults = []
|
||||||
this.seriesResults = []
|
this.seriesResults = []
|
||||||
this.tagResults = []
|
this.tagResults = []
|
||||||
|
this.genreResults = []
|
||||||
this.narratorResults = []
|
this.narratorResults = []
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
@@ -155,7 +166,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isFetching = true
|
this.isFetching = true
|
||||||
|
|
||||||
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${encodeURIComponent(value)}&limit=3`).catch((error) => {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -168,6 +179,7 @@ export default {
|
|||||||
this.authorResults = searchResults.authors || []
|
this.authorResults = searchResults.authors || []
|
||||||
this.seriesResults = searchResults.series || []
|
this.seriesResults = searchResults.series || []
|
||||||
this.tagResults = searchResults.tags || []
|
this.tagResults = searchResults.tags || []
|
||||||
|
this.genreResults = searchResults.genres || []
|
||||||
this.narratorResults = searchResults.narrators || []
|
this.narratorResults = searchResults.narrators || []
|
||||||
|
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-200" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
|
||||||
<span class="material-icons" style="font-size: 1.1rem">close</span>
|
<span class="material-symbols" style="font-size: 1.1rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -22,11 +22,11 @@
|
|||||||
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons text-2xl">arrow_right</span>
|
<span class="material-symbols text-2xl">arrow_right</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- selected checkmark icon -->
|
<!-- selected checkmark icon -->
|
||||||
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
<div v-if="item.value === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||||
<span class="material-icons text-base text-yellow-400">check</span>
|
<span class="material-symbols text-base text-yellow-400">check</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
<li class="text-gray-50 select-none relative py-2 pl-9 cursor-pointer hover:bg-white/5" role="option" @click="sublist = null">
|
||||||
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
<div class="absolute left-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons text-2xl">arrow_left</span>
|
<span class="material-symbols text-2xl">arrow_left</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- selected checkmark icon -->
|
<!-- selected checkmark icon -->
|
||||||
<div v-if="`${sublist}.${item.value}` === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
<div v-if="`${sublist}.${item.value}` === selected" class="absolute inset-y-0 right-2 h-full flex items-center pointer-events-none">
|
||||||
<span class="material-icons text-base text-yellow-400">check</span>
|
<span class="material-symbols text-base text-yellow-400">check</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
<button type="button" class="relative w-full h-full bg-fg border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||||
<span class="flex items-center justify-between">
|
<span class="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,6 +88,10 @@ export default {
|
|||||||
{
|
{
|
||||||
text: this.$strings.LabelFileModified,
|
text: this.$strings.LabelFileModified,
|
||||||
value: 'mtimeMs'
|
value: 'mtimeMs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelRandomly,
|
||||||
|
value: 'random'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -128,6 +132,10 @@ export default {
|
|||||||
{
|
{
|
||||||
text: this.$strings.LabelFileModified,
|
text: this.$strings.LabelFileModified,
|
||||||
value: 'mtimeMs'
|
value: 'mtimeMs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelRandomly,
|
||||||
|
value: 'random'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu">
|
||||||
<span class="flex items-center justify-between">
|
<span class="flex items-center justify-between">
|
||||||
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
<span class="block truncate text-xs" :class="!selectedText ? 'text-gray-300' : ''">{{ selectedText }}</span>
|
||||||
<span class="material-icons text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-lg text-yellow-400">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
<span v-if="item.value === selected" class="text-yellow-400 absolute inset-y-0 right-0 flex items-center pr-4">
|
||||||
<span class="material-icons text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
<span class="material-symbols text-xl">{{ descending ? 'expand_more' : 'expand_less' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||||
</button>
|
</button>
|
||||||
<transition name="menux">
|
<transition name="menux">
|
||||||
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||||
|
|
||||||
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
||||||
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
<span class="material-symbols" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex pt-4 px-2">
|
<div class="flex pt-4 px-2">
|
||||||
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
|
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">{{ $strings.ButtonUnlinkOpenId }}</ui-btn>
|
||||||
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
@@ -212,19 +212,19 @@ export default {
|
|||||||
},
|
},
|
||||||
unlinkOpenID() {
|
unlinkOpenID() {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: 'Are you sure you want to unlink this user from OpenID?',
|
message: this.$strings.MessageConfirmUnlinkOpenId,
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.unlinkingFromOpenID = true
|
this.unlinkingFromOpenID = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
.$patch(`/api/users/${this.account.id}/openid-unlink`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('User unlinked from OpenID')
|
this.$toast.success(this.$strings.ToastUnlinkOpenIdSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to unlink user from OpenID', error)
|
console.error('Failed to unlink user from OpenID', error)
|
||||||
this.$toast.error('Failed to unlink user from OpenID')
|
this.$toast.error(this.$strings.ToastUnlinkOpenIdFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.unlinkingFromOpenID = false
|
this.unlinkingFromOpenID = false
|
||||||
@@ -265,15 +265,15 @@ export default {
|
|||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.newUser.username) {
|
if (!this.newUser.username) {
|
||||||
this.$toast.error('Enter a username')
|
this.$toast.error(this.$strings.ToastNewUserUsernameError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
|
if (!this.newUser.permissions.accessAllLibraries && !this.newUser.librariesAccessible.length) {
|
||||||
this.$toast.error('Must select at least one library')
|
this.$toast.error(this.$strings.ToastNewUserLibraryError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
||||||
this.$toast.error('Must select at least one tag')
|
this.$toast.error(this.$strings.ToastNewUserTagError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,12 +313,12 @@ export default {
|
|||||||
this.processing = false
|
this.processing = false
|
||||||
console.error('Failed to update account', error)
|
console.error('Failed to update account', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
this.$toast.error(errMsg || 'Failed to update account')
|
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdateAccount)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
submitCreateAccount() {
|
submitCreateAccount() {
|
||||||
if (!this.newUser.password) {
|
if (!this.newUser.password) {
|
||||||
this.$toast.error('Must have a password, only root user can have an empty password')
|
this.$toast.error(this.$strings.ToastNewUserPasswordError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,9 +329,9 @@ export default {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(`Failed to create account: ${data.error}`)
|
this.$toast.error(this.$strings.ToastNewUserCreatedFailed + ': ' + data.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success('New account created')
|
this.$toast.success(this.$strings.ToastNewUserCreatedSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -351,6 +351,7 @@ export default {
|
|||||||
update: type === 'admin',
|
update: type === 'admin',
|
||||||
delete: type === 'admin',
|
delete: type === 'admin',
|
||||||
upload: type === 'admin',
|
upload: type === 'admin',
|
||||||
|
accessExplicitContent: true,
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false
|
||||||
@@ -385,6 +386,7 @@ export default {
|
|||||||
upload: false,
|
upload: false,
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true,
|
accessAllTags: true,
|
||||||
|
accessExplicitContent: true,
|
||||||
selectedTagsNotAccessible: false
|
selectedTagsNotAccessible: false
|
||||||
},
|
},
|
||||||
librariesAccessible: [],
|
librariesAccessible: [],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="text-3xl text-white truncate">Add custom metadata provider</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderAddCustomMetadataProvider }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
<ui-text-input-with-label v-model="newUrl" label="URL" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full mb-2 p-1">
|
<div class="w-full mb-2 p-1">
|
||||||
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
|
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex px-1 pt-4">
|
<div class="flex px-1 pt-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -67,7 +67,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.newName || !this.newUrl) {
|
if (!this.newName || !this.newUrl) {
|
||||||
this.$toast.error('Must add name and url')
|
this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,13 +81,13 @@ export default {
|
|||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$emit('added', data.provider)
|
this.$emit('added', data.provider)
|
||||||
this.$toast.success('New provider added')
|
this.$toast.success(this.$strings.ToastProviderCreatedSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
const errorMsg = error.response?.data || 'Unknown error'
|
const errorMsg = error.response?.data || 'Unknown error'
|
||||||
console.error('Failed to add provider', error)
|
console.error('Failed to add provider', error)
|
||||||
this.$toast.error('Failed to add provider: ' + errorMsg)
|
this.$toast.error(this.$strings.ToastProviderCreatedFailed + ': ' + errorMsg)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
<p class="text-base text-gray-200 truncate">{{ metadata.filename }}</p>
|
||||||
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
|
<ui-btn v-if="ffprobeData" small class="ml-2" @click="ffprobeData = null">{{ $strings.ButtonReset }}</ui-btn>
|
||||||
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">Probe Audio File</ui-btn>
|
<ui-btn v-else-if="userIsAdminOrUp" small :loading="probingFile" class="ml-2" @click="getFFProbeData">{{ $strings.ButtonProbeAudioFile }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
|
<ui-textarea-with-label :value="prettyFfprobeData" readonly :rows="30" class="text-xs" />
|
||||||
|
|
||||||
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
|
<button class="absolute top-4 right-4" :class="copiedToClipboard ? 'text-success' : 'text-white/50 hover:text-white/80'" @click.stop="copyFfprobeData">
|
||||||
<span class="material-icons">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
|
<span class="material-symbols">{{ copiedToClipboard ? 'check' : 'content_copy' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to get ffprobe data', error)
|
console.error('Failed to get ffprobe data', error)
|
||||||
this.$toast.error('FFProbe failed')
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.probingFile = false
|
this.probingFile = false
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
<widgets-cron-expression-builder ref="expressionBuilder" v-model="newCronExpression" @input="expressionUpdated" />
|
||||||
|
|
||||||
<div class="flex items-center justify-end">
|
<div class="flex items-center justify-end">
|
||||||
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdateNecessary }}</ui-btn>
|
<ui-btn :disabled="!isUpdated" @click="submit">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
<ui-tooltip :text="$strings.LabelUpdateCoverHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelUpdateCover }}
|
{{ $strings.LabelUpdateCover }}
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
<ui-tooltip :text="$strings.LabelUpdateDetailsHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelUpdateDetails }}
|
{{ $strings.LabelUpdateDetails }}
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">add</span></ui-btn>
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@ export default {
|
|||||||
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
|
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
console.error(error)
|
console.error(error)
|
||||||
})
|
})
|
||||||
this.show = false
|
this.show = false
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="chapters" :width="600" :height="'unset'">
|
<modals-modal v-model="show" name="chapters" :width="600" :height="'unset'">
|
||||||
<div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div id="chapter-modal-wrapper" ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
<template v-for="chap in chapters">
|
<template v-for="chap in chapters">
|
||||||
<div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-yellow-400 bg-opacity-10' : chap.end / _playbackRate <= currentChapterStart ? 'bg-success bg-opacity-5' : 'bg-opacity-20'" @click="clickChapter(chap)">
|
<div :key="chap.id" :id="`chapter-row-${chap.id}`" class="flex items-center px-6 py-3 justify-start cursor-pointer relative" :class="chap.id === currentChapterId ? 'bg-yellow-400/20 hover:bg-yellow-400/10' : chap.end / _playbackRate <= currentChapterStart ? 'bg-success/10 hover:bg-success/5' : 'hover:bg-primary/10'" @click="clickChapter(chap)">
|
||||||
<p class="chapter-title truncate text-sm md:text-base">
|
<p class="chapter-title truncate text-sm md:text-base">
|
||||||
{{ chap.title }}
|
{{ chap.title }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
<div ref="wrapper" class="hidden absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 rounded-lg items-center justify-center" style="z-index: 61" @click="clickClose">
|
||||||
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
<div class="absolute top-3 right-3 md:top-5 md:right-5 h-8 w-8 md:h-12 md:w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300">
|
||||||
<span class="material-icons text-2xl md:text-4xl">close</span>
|
<span class="material-symbols text-2xl md:text-4xl">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div ref="content" class="text-white">
|
<div ref="content" class="text-white">
|
||||||
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
<form v-if="selectedSeries" @submit.prevent="submitSeriesForm">
|
||||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" />
|
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" :label="$strings.LabelSeriesName" @input="seriesNameInputHandler" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24 sm:w-28 md:w-40 p-1">
|
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||||
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" :label="$strings.LabelSequence" />
|
||||||
@@ -66,6 +66,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
seriesNameInputHandler() {
|
||||||
|
if (this.$refs.sequenceInput) {
|
||||||
|
this.$refs.sequenceInput.setFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
setInputFocus() {
|
setInputFocus() {
|
||||||
if (this.isNewSeries) {
|
if (this.isNewSeries) {
|
||||||
// Focus on series input if new series
|
// Focus on series input if new series
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
<ui-btn v-if="!isOpenSession && !isMediaItemShareSession" small color="error" @click.stop="deleteSessionClick">{{ $strings.ButtonDelete }}</ui-btn>
|
||||||
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">Close Open Session</ui-btn>
|
<ui-btn v-else-if="!isMediaItemShareSession" small color="error" @click.stop="closeSessionClick">{{ $strings.ButtonCloseSession }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -206,14 +206,13 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/session/${this._session.id}/close`)
|
.$post(`/api/session/${this._session.id}/close`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Session closed')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
this.$emit('closedSession')
|
this.$emit('closedSession')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to close session', error)
|
console.error('Failed to close session', error)
|
||||||
const errMsg = error.response?.data || ''
|
const errMsg = error.response?.data || ''
|
||||||
this.$toast.error(errMsg || 'Failed to close open session')
|
this.$toast.error(errMsg || this.$strings.ToastSessionCloseFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||||
|
|
||||||
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
<button class="absolute top-4 right-4 landscape:top-4 landscape:right-4 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 inline-flex text-gray-200 hover:text-white" aria-label="Close modal" @click="clickClose">
|
||||||
<span class="material-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
<span class="material-symbols text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||||
</button>
|
</button>
|
||||||
<slot name="outer" />
|
<slot name="outer" />
|
||||||
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
<div ref="content" style="min-width: 380px; min-height: 200px; max-width: 100vw" class="relative text-white" aria-modal="true" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="player-settings" :width="500" :height="'unset'">
|
||||||
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-4" style="max-height: 80vh; min-height: 40vh">
|
||||||
|
<h3 class="text-xl font-semibold mb-8">{{ $strings.HeaderPlayerSettings }}</h3>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<ui-toggle-switch v-model="useChapterTrack" @input="setUseChapterTrack" />
|
||||||
|
<div class="pl-4">
|
||||||
|
<span>{{ $strings.LabelUseChapterTrack }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
useChapterTrack: false,
|
||||||
|
jumpValues: [
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['10']), value: 10 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['15']), value: 15 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['30']), value: 30 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['60']), value: 60 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXMinutes', ['2']), value: 120 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
|
||||||
|
],
|
||||||
|
jumpForwardAmount: 10,
|
||||||
|
jumpBackwardAmount: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setUseChapterTrack() {
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||||
|
},
|
||||||
|
setJumpForwardAmount(val) {
|
||||||
|
this.jumpForwardAmount = val
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { jumpForwardAmount: val })
|
||||||
|
},
|
||||||
|
setJumpBackwardAmount(val) {
|
||||||
|
this.jumpBackwardAmount = val
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
||||||
|
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||||
|
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="absolute top-0 right-0 p-4">
|
<div class="absolute top-0 right-0 p-4">
|
||||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
|
||||||
<a href="https://www.audiobookshelf.org/guides/media-item-shares" target="_blank" class="inline-flex">
|
<a href="https://www.audiobookshelf.org/guides/media-item-shares" target="_blank" class="inline-flex">
|
||||||
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
|
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
|
||||||
</a>
|
</a>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,7 +165,7 @@ export default {
|
|||||||
},
|
},
|
||||||
openShare() {
|
openShare() {
|
||||||
if (!this.newShareSlug) {
|
if (!this.newShareSlug) {
|
||||||
this.$toast.error('Slug is required')
|
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -6,34 +6,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
<div v-if="!timerSet" class="w-full">
|
<div class="w-full">
|
||||||
<template v-for="time in sleepTimes">
|
<template v-for="time in sleepTimes">
|
||||||
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time.seconds)">
|
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-primary/25 relative" @click="setTime(time)">
|
||||||
<p class="text-xl text-center">{{ time.text }}</p>
|
<p class="text-lg text-center">{{ time.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
|
||||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-18 flex items-center justify-center ml-1">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full p-4">
|
<div v-if="timerSet" class="w-full p-4">
|
||||||
<div class="mb-4 flex items-center justify-center">
|
<div class="mb-4 h-px w-full bg-white/10" />
|
||||||
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
|
|
||||||
<span class="material-icons text-lg">remove</span>
|
<div v-if="timerType === $constants.SleepTimerTypes.COUNTDOWN" class="mb-4 flex items-center justify-center space-x-4">
|
||||||
<span class="pl-1 text-base font-mono">30m</span>
|
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center h-9" @click="decrement(30 * 60)">
|
||||||
|
<span class="material-symbols text-lg">remove</span>
|
||||||
|
<span class="pl-1 text-sm">30m</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" />
|
<ui-icon-btn icon="remove" class="min-w-9" @click="decrement(60 * 5)" />
|
||||||
|
|
||||||
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
<p class="text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
||||||
|
|
||||||
<ui-icon-btn icon="add" @click="increment(60 * 5)" />
|
<ui-icon-btn icon="add" class="min-w-9" @click="increment(60 * 5)" />
|
||||||
|
|
||||||
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)">
|
<ui-btn :padding-x="2" small class="flex items-center h-9" @click="increment(30 * 60)">
|
||||||
<span class="material-icons text-lg">add</span>
|
<span class="material-symbols text-lg">add</span>
|
||||||
<span class="pl-1 text-base font-mono">30m</span>
|
<span class="pl-1 text-sm">30m</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
@@ -47,52 +49,13 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
timerSet: Boolean,
|
timerSet: Boolean,
|
||||||
timerTime: Number,
|
timerType: String,
|
||||||
remaining: Number
|
remaining: Number,
|
||||||
|
hasChapters: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
customTime: null,
|
customTime: null
|
||||||
sleepTimes: [
|
|
||||||
{
|
|
||||||
seconds: 60 * 5,
|
|
||||||
text: '5 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 15,
|
|
||||||
text: '15 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 20,
|
|
||||||
text: '20 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 30,
|
|
||||||
text: '30 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 45,
|
|
||||||
text: '45 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 60,
|
|
||||||
text: '60 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 90,
|
|
||||||
text: '90 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 120,
|
|
||||||
text: '2 hours'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
show(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -103,6 +66,54 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
sleepTimes() {
|
||||||
|
const times = [
|
||||||
|
{
|
||||||
|
seconds: 60 * 5,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['5']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 15,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['15']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 20,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['20']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 30,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['30']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 45,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['45']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 60,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['60']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 90,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['90']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 120,
|
||||||
|
text: this.$getString('LabelTimeDurationXHours', ['2']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (this.hasChapters) {
|
||||||
|
times.push({ seconds: -1, text: this.$strings.LabelEndOfChapter, timerType: this.$constants.SleepTimerTypes.CHAPTER })
|
||||||
|
}
|
||||||
|
return times
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -113,10 +124,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeInSeconds = Math.round(Number(this.customTime) * 60)
|
const timeInSeconds = Math.round(Number(this.customTime) * 60)
|
||||||
this.setTime(timeInSeconds)
|
const time = {
|
||||||
|
seconds: timeInSeconds,
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
}
|
||||||
|
this.setTime(time)
|
||||||
},
|
},
|
||||||
setTime(seconds) {
|
setTime(time) {
|
||||||
this.$emit('set', seconds)
|
this.$emit('set', time)
|
||||||
},
|
},
|
||||||
increment(amount) {
|
increment(amount) {
|
||||||
this.$emit('increment', amount)
|
this.$emit('increment', amount)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
||||||
<p class="text-lg">Preview Cover</p>
|
<p class="text-lg">Preview Cover</p>
|
||||||
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
<span class="absolute top-4 right-4 material-symbols text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||||
<div class="flex justify-center py-4">
|
<div class="flex justify-center py-4">
|
||||||
<covers-preview-cover :src="previewUpload" :width="240" />
|
<covers-preview-cover :src="previewUpload" :width="240" />
|
||||||
</div>
|
</div>
|
||||||
@@ -78,14 +78,13 @@ export default {
|
|||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.$toast.error(data.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success('Cover Uploaded')
|
|
||||||
this.resetCoverPreview()
|
this.resetCoverPreview()
|
||||||
}
|
}
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
|
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
})
|
})
|
||||||
@@ -95,7 +94,7 @@ export default {
|
|||||||
|
|
||||||
var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => {
|
var success = await this.$axios.$post(`/api/${this.entity}/${this.entityId}/cover`, { url: this.imageUrl }).catch((error) => {
|
||||||
console.error('Failed to download cover from url', error)
|
console.error('Failed to download cover from url', error)
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Unknown Error'
|
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastUnknownError
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div class="w-full h-45 relative">
|
<div class="w-full h-45 relative">
|
||||||
<covers-author-image :author="authorCopy" />
|
<covers-author-image :author="authorCopy" />
|
||||||
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
||||||
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
<span class="absolute top-2 right-2 material-symbols text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,12 +116,12 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/authors/${this.authorId}`)
|
.$delete(`/api/authors/${this.authorId}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Author removed')
|
this.$toast.success(this.$strings.ToastAuthorRemoveSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove author', error)
|
console.error('Failed to remove author', error)
|
||||||
this.$toast.error('Failed to remove author')
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -141,7 +141,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!Object.keys(updatePayload).length) {
|
if (!Object.keys(updatePayload).length) {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdateNecessary)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
@@ -158,7 +158,7 @@ export default {
|
|||||||
} else if (result.merged) {
|
} else if (result.merged) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
|
this.$toast.success(this.$strings.ToastAuthorUpdateMerged)
|
||||||
this.show = false
|
this.show = false
|
||||||
} else this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
} else this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
@@ -174,7 +174,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error(this.$strings.ToastAuthorImageRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -182,7 +182,7 @@ export default {
|
|||||||
},
|
},
|
||||||
submitUploadCover() {
|
submitUploadCover() {
|
||||||
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
if (!this.imageUrl?.startsWith('http:') && !this.imageUrl?.startsWith('https:')) {
|
||||||
this.$toast.error('Invalid image url')
|
this.$toast.error(this.$strings.ToastInvalidImageUrl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,14 +194,14 @@ export default {
|
|||||||
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
.$post(`/api/authors/${this.authorId}/image`, updatePayload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
this.$toast.success('Author image updated')
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
|
|
||||||
this.authorCopy.updatedAt = data.author.updatedAt
|
this.authorCopy.updatedAt = data.author.updatedAt
|
||||||
this.authorCopy.imagePath = data.author.imagePath
|
this.authorCopy.imagePath = data.author.imagePath
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error(error.response.data || 'Failed to remove author image')
|
this.$toast.error(error.response.data || this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -209,7 +209,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async searchAuthor() {
|
async searchAuthor() {
|
||||||
if (!this.authorCopy.name && !this.authorCopy.asin) {
|
if (!this.authorCopy.name && !this.authorCopy.asin) {
|
||||||
this.$toast.error('Must enter an author name')
|
this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
@@ -228,17 +228,19 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!response) {
|
if (!response) {
|
||||||
this.$toast.error('Author not found')
|
this.$toast.error(this.$strings.ToastAuthorSearchNotFound)
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) {
|
if (response.author.imagePath) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
} else {
|
||||||
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
|
}
|
||||||
|
|
||||||
this.authorCopy = {
|
this.authorCopy = {
|
||||||
...response.author
|
...response.author
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made for Author')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
<div class="flex-grow pr-2">
|
<div class="flex-grow pr-2">
|
||||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-icons text-2xl -mt-px">forward</span></ui-btn>
|
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
|
||||||
<div class="pl-2 flex items-center">
|
<div class="pl-2 flex items-center">
|
||||||
<span class="material-icons text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
<span class="material-symbols text-3xl text-white text-opacity-70 hover:text-opacity-95 cursor-pointer" @click.stop.prevent="cancelEditing">close</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
<p v-else class="pl-2 pr-2 truncate">{{ bookmark.title }}</p>
|
<p v-else class="pl-2 pr-2 truncate">{{ bookmark.title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
<div v-if="!isEditing" class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
<span class="material-icons text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
<span class="material-symbols text-xl mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||||
<span class="material-icons text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
|
<span class="material-symbols text-xl text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -6,10 +6,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||||
<p class="text-xl font-bold pb-4">
|
<template v-for="release in releasesToShow">
|
||||||
Changelog <a :href="currentTagUrl" target="_blank" class="hover:underline">v{{ currentVersionNumber }}</a> ({{ currentVersionPubDate }})
|
<div :key="release.name">
|
||||||
</p>
|
<p class="text-xl font-bold pb-4">
|
||||||
<div class="custom-text" v-html="compiledMarkedown" />
|
Changelog <a :href="`https://github.com/advplyr/audiobookshelf/releases/tag/${release.name}`" target="_blank" class="hover:underline">{{ release.name }}</a> ({{ $formatDate(release.pubdate, dateFormat) }})
|
||||||
|
</p>
|
||||||
|
<div class="custom-text" v-html="getChangelog(release)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="release !== releasesToShow[releasesToShow.length - 1]" class="border-b border-black-300 my-8" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -37,24 +42,15 @@ export default {
|
|||||||
dateFormat() {
|
dateFormat() {
|
||||||
return this.$store.state.serverSettings.dateFormat
|
return this.$store.state.serverSettings.dateFormat
|
||||||
},
|
},
|
||||||
changelog() {
|
releasesToShow() {
|
||||||
return this.versionData?.currentVersionChangelog || 'No Changelog Available'
|
return this.versionData?.releasesToShow || []
|
||||||
},
|
}
|
||||||
compiledMarkedown() {
|
},
|
||||||
return marked.parse(this.changelog, { gfm: true, breaks: true })
|
methods: {
|
||||||
},
|
getChangelog(release) {
|
||||||
currentVersionPubDate() {
|
return marked.parse(release.changelog || 'No Changelog Available', { gfm: true, breaks: true })
|
||||||
if (!this.versionData?.currentVersionPubDate) return 'Unknown release date'
|
|
||||||
return `${this.$formatDate(this.versionData.currentVersionPubDate, this.dateFormat)}`
|
|
||||||
},
|
|
||||||
currentTagUrl() {
|
|
||||||
return this.versionData?.currentTagUrl
|
|
||||||
},
|
|
||||||
currentVersionNumber() {
|
|
||||||
return this.$config.version
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove books from collection', error)
|
console.error('Failed to remove books from collection', error)
|
||||||
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -157,7 +157,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove book from collection', error)
|
console.error('Failed to remove book from collection', error)
|
||||||
this.$toast.error(this.$strings.ToastCollectionItemsRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,12 +172,12 @@ export default {
|
|||||||
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
.$post(`/api/collections/${collection.id}/batch/add`, { books: this.selectedBookIds })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Books added to collection`, updatedCollection)
|
console.log(`Books added to collection`, updatedCollection)
|
||||||
this.$toast.success('Books added to collection')
|
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add books to collection', error)
|
console.error('Failed to add books to collection', error)
|
||||||
this.$toast.error('Failed to add books to collection')
|
this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -187,12 +187,12 @@ export default {
|
|||||||
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
.$post(`/api/collections/${collection.id}/book`, { id: this.selectedLibraryItemId })
|
||||||
.then((updatedCollection) => {
|
.then((updatedCollection) => {
|
||||||
console.log(`Book added to collection`, updatedCollection)
|
console.log(`Book added to collection`, updatedCollection)
|
||||||
this.$toast.success('Book added to collection')
|
this.$toast.success(this.$strings.ToastCollectionItemsAddSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add book to collection', error)
|
console.error('Failed to add book to collection', error)
|
||||||
this.$toast.error('Failed to add book to collection')
|
this.$toast.error(this.$strings.ToastCollectionItemsAddFailed)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create collection', error)
|
console.error('Failed to create collection', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
this.$toast.error(`Failed to create collection: ${errMsg}`)
|
this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
<nuxt-link :to="`/collection/${collection.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ collection.name }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
<ui-btn v-if="!isBookIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="flex items-center mb-3">
|
<div class="flex items-center mb-3">
|
||||||
<div class="hover:bg-white hover:bg-opacity-10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false">
|
<div class="hover:bg-white hover:bg-opacity-10 cursor-pointer h-11 w-11 flex items-center justify-center rounded-full" @click="showImageUploader = false">
|
||||||
<span class="material-icons text-4xl">arrow_back</span>
|
<span class="material-symbols text-4xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="ml-2 text-xl mb-1">Collection Cover Image</p>
|
<p class="ml-2 text-xl mb-1">Collection Cover Image</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove collection', error)
|
console.error('Failed to remove collection', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -115,7 +115,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newCollectionName) {
|
if (!this.newCollectionName) {
|
||||||
return this.$toast.error('Collection must have a name')
|
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|||||||
@@ -125,12 +125,12 @@ export default {
|
|||||||
this.$refs.ereaderEmailInput.blur()
|
this.$refs.ereaderEmailInput.blur()
|
||||||
|
|
||||||
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||||
this.$toast.error('Name and email required')
|
this.$toast.error(this.$strings.ToastNameEmailRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
|
if (this.newDevice.availabilityOption === 'specificUsers' && !this.newDevice.users.length) {
|
||||||
this.$toast.error('Must select at least one user')
|
this.$toast.error(this.$strings.ToastSelectAtLeastOneUser)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.newDevice.availabilityOption !== 'specificUsers') {
|
if (this.newDevice.availabilityOption !== 'specificUsers') {
|
||||||
@@ -142,14 +142,14 @@ export default {
|
|||||||
|
|
||||||
if (!this.ereaderDevice) {
|
if (!this.ereaderDevice) {
|
||||||
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||||
this.$toast.error('Ereader device with that name already exists')
|
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.submitCreate()
|
this.submitCreate()
|
||||||
} else {
|
} else {
|
||||||
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||||
this.$toast.error('Ereader device with that name already exists')
|
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,12 +174,11 @@ export default {
|
|||||||
.$post(`/api/emails/ereader-devices`, payload)
|
.$post(`/api/emails/ereader-devices`, payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$emit('update', data.ereaderDevices)
|
this.$emit('update', data.ereaderDevices)
|
||||||
this.$toast.success('Device updated')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update device', error)
|
console.error('Failed to update device', error)
|
||||||
this.$toast.error('Failed to update device')
|
this.$toast.error(this.$strings.ToastDeviceUpdateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -201,12 +200,11 @@ export default {
|
|||||||
.$post('/api/emails/ereader-devices', payload)
|
.$post('/api/emails/ereader-devices', payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$emit('update', data.ereaderDevices || [])
|
this.$emit('update', data.ereaderDevices || [])
|
||||||
this.$toast.success('Device added')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add device', error)
|
console.error('Failed to add device', error)
|
||||||
this.$toast.error('Failed to add device')
|
this.$toast.error(this.$strings.ToastDeviceAddFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
|
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
|
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
|
<div class="w-full h-full max-h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||||
<div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
<div v-if="userCanDelete" class="p-1 absolute top-1 right-1 text-red-500 rounded-full w-8 h-8 cursor-pointer hover:text-red-400 shadow-sm" @click="removeCover">
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
<ui-tooltip direction="top" :text="$strings.LabelRemoveCover">
|
||||||
<span class="material-icons text-2xl">delete</span>
|
<span class="material-symbols text-2xl">delete</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
|
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
|
||||||
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
||||||
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
|
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
|
||||||
<span class="material-icons text-2xl inline-block md:!hidden">upload</span>
|
<span class="material-symbols text-2xl inline-block md:!hidden">upload</span>
|
||||||
</ui-file-input>
|
</ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
|
|
||||||
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
||||||
<p class="text-lg">{{ $strings.HeaderPreviewCover }}</p>
|
<p class="text-lg">{{ $strings.HeaderPreviewCover }}</p>
|
||||||
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
<span class="absolute top-4 right-4 material-symbols text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||||
<div class="flex justify-center py-4">
|
<div class="flex justify-center py-4">
|
||||||
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
@@ -194,7 +194,6 @@ export default {
|
|||||||
if (data.error) {
|
if (data.error) {
|
||||||
this.$toast.error(data.error)
|
this.$toast.error(data.error)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success('Cover Uploaded')
|
|
||||||
this.resetCoverPreview()
|
this.resetCoverPreview()
|
||||||
}
|
}
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
@@ -204,7 +203,7 @@ export default {
|
|||||||
if (error.response && error.response.data) {
|
if (error.response && error.response.data) {
|
||||||
this.$toast.error(error.response.data)
|
this.$toast.error(error.response.data)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Oops, something went wrong...')
|
this.$toast.error(this.$strings.ToastUnknownError)
|
||||||
}
|
}
|
||||||
this.processingUpload = false
|
this.processingUpload = false
|
||||||
})
|
})
|
||||||
@@ -255,7 +254,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async updateCover(cover) {
|
async updateCover(cover) {
|
||||||
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
if (!cover.startsWith('http:') && !cover.startsWith('https:')) {
|
||||||
this.$toast.error('Invalid URL')
|
this.$toast.error(this.$strings.ToastInvalidUrl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,11 +263,10 @@ export default {
|
|||||||
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
|
.$post(`/api/items/${this.libraryItemId}/cover`, { url: cover })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
this.$toast.success('Update Successful')
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update cover', error)
|
console.error('Failed to update cover', error)
|
||||||
this.$toast.error(error.response?.data || 'Failed to update cover')
|
this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
@@ -308,12 +306,9 @@ export default {
|
|||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
|
.$patch(`/api/items/${this.libraryItemId}/cover`, { cover: coverFile.metadata.path })
|
||||||
.then(() => {
|
|
||||||
this.$toast.success('Update Successful')
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to set local cover', error)
|
console.error('Failed to set local cover', error)
|
||||||
this.$toast.error(error.response?.data || 'Failed to set cover')
|
this.$toast.error(error.response?.data || this.$strings.ToastCoverUpdateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
|
|
||||||
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
var { title, author } = this.$refs.itemDetailsEdit.getTitleAndAuthorName()
|
||||||
if (!title) {
|
if (!title) {
|
||||||
this.$toast.error('Must have a title for quick match')
|
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.quickMatching = true
|
this.quickMatching = true
|
||||||
@@ -108,9 +108,9 @@ export default {
|
|||||||
if (res.warning) {
|
if (res.warning) {
|
||||||
this.$toast.warning(res.warning)
|
this.$toast.warning(res.warning)
|
||||||
} else if (res.updated) {
|
} else if (res.updated) {
|
||||||
this.$toast.success('Item details updated')
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -128,18 +128,18 @@ export default {
|
|||||||
this.rescanning = false
|
this.rescanning = false
|
||||||
var result = data.result
|
var result = data.result
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
this.$toast.error(this.$getString('ToastRescanFailed', [this.title]))
|
||||||
} else if (result === 'UPDATED') {
|
} else if (result === 'UPDATED') {
|
||||||
this.$toast.success(`Re-Scan complete item was updated`)
|
this.$toast.success(this.$strings.ToastRescanUpdated)
|
||||||
} else if (result === 'UPTODATE') {
|
} else if (result === 'UPTODATE') {
|
||||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
this.$toast.success(this.$strings.ToastRescanUpToDate)
|
||||||
} else if (result === 'REMOVED') {
|
} else if (result === 'REMOVED') {
|
||||||
this.$toast.error(`Re-Scan complete item was removed`)
|
this.$toast.error(this.$strings.ToastRescanRemoved)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to scan library item', error)
|
console.error('Failed to scan library item', error)
|
||||||
this.$toast.error('Failed to scan library item')
|
this.$toast.error(this.$strings.ToastScanFailed)
|
||||||
this.rescanning = false
|
this.rescanning = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -156,7 +156,7 @@ export default {
|
|||||||
}
|
}
|
||||||
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
|
var updatedDetails = this.$refs.itemDetailsEdit.getDetails()
|
||||||
if (!updatedDetails.hasChanges) {
|
if (!updatedDetails.hasChanges) {
|
||||||
this.$toast.info('No changes were made')
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.updateDetails(updatedDetails)
|
return this.updateDetails(updatedDetails)
|
||||||
@@ -170,7 +170,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success('Item details updated')
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="flex -mb-0.5">
|
<div class="flex -mb-0.5">
|
||||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
|
||||||
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
|
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited.">
|
||||||
<span class="material-icons text-base">info_outlined</span>
|
<span class="material-symbols text-base">info</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</ui-text-input-with-label>
|
</ui-text-input-with-label>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
<div v-if="selectedMatchOrig" class="absolute top-0 left-0 w-full bg-bg h-full px-2 py-6 md:p-8 max-h-full overflow-y-auto overflow-x-hidden">
|
||||||
<div class="flex mb-4">
|
<div class="flex mb-4">
|
||||||
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
|
<div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="clearSelectedMatch">
|
||||||
<span class="material-icons text-3xl">arrow_back</span>
|
<span class="material-symbols text-3xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
|
<p class="text-xl pl-3">{{ $strings.HeaderUpdateDetails }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,49 +59,63 @@
|
|||||||
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.title" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
|
||||||
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.title || '' }}</p>
|
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.subtitle" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.subtitle" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
|
||||||
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.subtitle || '' }}</p>
|
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.author" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.author" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
|
||||||
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.authorName || '' }}</p>
|
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
||||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
|
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.description" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.description" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
|
||||||
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</p>
|
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.publisher" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.publisher" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
|
||||||
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publisher || '' }}</p>
|
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.publishedYear" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.publishedYear" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
|
||||||
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear || '' }}</p>
|
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,42 +123,54 @@
|
|||||||
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.series" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
|
||||||
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.seriesName || '' }}</p>
|
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.genres?.length" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
|
||||||
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
|
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||||
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
|
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.language" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.language" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.language" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
|
||||||
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.language || '' }}</p>
|
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.isbn" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.isbn" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
|
||||||
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.isbn || '' }}</p>
|
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.asin" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.asin" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
|
||||||
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.asin || '' }}</p>
|
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -152,28 +178,36 @@
|
|||||||
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.itunesId" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
|
||||||
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesId || '' }}</p>
|
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.feedUrl" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.feedUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
|
||||||
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.feedUrl || '' }}</p>
|
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.itunesPageUrl" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.itunesPageUrl" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
|
||||||
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.itunesPageUrl || '' }}</p>
|
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.releaseDate" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.releaseDate" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
|
||||||
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.releaseDate || '' }}</p>
|
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
|
||||||
|
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
|
<div v-if="selectedMatchOrig.explicit != null" class="flex items-center pb-2" :class="{ 'pt-2': mediaMetadata.explicit == null }">
|
||||||
@@ -281,7 +315,7 @@ export default {
|
|||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
filterData() {
|
filterData() {
|
||||||
return this.$store.state.libraries.filterData
|
return this.$store.state.libraries.filterData || {}
|
||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
@@ -321,6 +355,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setMatchFieldValue(field, value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
this.selectedMatch[field] = [...value]
|
||||||
|
} else {
|
||||||
|
this.selectedMatch[field] = value
|
||||||
|
}
|
||||||
|
},
|
||||||
selectAllToggled(val) {
|
selectAllToggled(val) {
|
||||||
for (const key in this.selectedMatchUsage) {
|
for (const key in this.selectedMatchUsage) {
|
||||||
this.selectedMatchUsage[key] = val
|
this.selectedMatchUsage[key] = val
|
||||||
@@ -356,7 +397,7 @@ export default {
|
|||||||
},
|
},
|
||||||
submitSearch() {
|
submitSearch() {
|
||||||
if (!this.searchTitle) {
|
if (!this.searchTitle) {
|
||||||
this.$toast.warning('Search title is required')
|
this.$toast.warning(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.persistProvider()
|
this.persistProvider()
|
||||||
@@ -577,7 +618,7 @@ export default {
|
|||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
this.$emit('selectTab', 'details')
|
this.$emit('selectTab', 'details')
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max episodes to keep
|
Max episodes to keep
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
Max new episodes to download per check
|
Max new episodes to download per check
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-symbols icon-text">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -163,7 +163,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (updateResult) {
|
if (updateResult) {
|
||||||
if (updateResult.updated) {
|
if (updateResult.updated) {
|
||||||
this.$toast.success('Item details updated')
|
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
|
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=m4b`" class="flex items-center"
|
||||||
>{{ $strings.ButtonOpenManager }}
|
>{{ $strings.ButtonOpenManager }}
|
||||||
<span class="material-icons text-lg ml-2">launch</span>
|
<span class="material-symbols text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
|
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=embed`" class="flex items-center"
|
||||||
>{{ $strings.ButtonOpenManager }}
|
>{{ $strings.ButtonOpenManager }}
|
||||||
<span class="material-icons text-lg ml-2">launch</span>
|
<span class="material-symbols text-lg ml-2">launch</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn>
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
<div class="folders-container overflow-y-auto w-full py-2 mb-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text ref="folderInput" v-model="folder.fullPath" :readonly="!!folder.id" type="text" class="w-full" @blur="existingFolderInputBlurred(folder)" />
|
<ui-editable-text ref="folderInput" v-model="folder.fullPath" :readonly="!!folder.id" type="text" class="w-full" @blur="existingFolderInputBlurred(folder)" />
|
||||||
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
<span v-show="folders.length > 1" class="material-symbols text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-1 px-2 items-center w-full">
|
<div class="flex py-1 px-2 items-center w-full">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-symbols fill bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export default {
|
|||||||
},
|
},
|
||||||
validate() {
|
validate() {
|
||||||
if (!this.libraryCopy.name) {
|
if (!this.libraryCopy.name) {
|
||||||
this.$toast.error('Library must have a name')
|
this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (!this.libraryCopy.folders.length) {
|
if (!this.libraryCopy.folders.length) {
|
||||||
@@ -205,7 +205,7 @@ export default {
|
|||||||
submitUpdateLibrary() {
|
submitUpdateLibrary() {
|
||||||
var newLibraryPayload = this.getLibraryUpdatePayload()
|
var newLibraryPayload = this.getLibraryUpdatePayload()
|
||||||
if (!Object.keys(newLibraryPayload).length) {
|
if (!Object.keys(newLibraryPayload).length) {
|
||||||
this.$toast.info('No updates are necessary')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
|
<div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10">
|
||||||
<div class="flex items-center py-1 mb-2">
|
<div class="flex items-center py-1 mb-2">
|
||||||
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
<span class="material-symbols text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
|
||||||
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
|
||||||
@@ -10,18 +10,18 @@
|
|||||||
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
|
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
|
||||||
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
|
||||||
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
|
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
|
||||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<p class="text-base font-mono px-2">..</p>
|
<p class="text-base font-mono px-2">..</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
|
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
|
||||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||||
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
|
<span v-if="dir.path === selectedPath" class="material-symbols" style="font-size: 1.1rem">arrow_right</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 h-full overflow-y-auto">
|
<div class="w-1/2 h-full overflow-y-auto">
|
||||||
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
|
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
|
||||||
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +162,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to get filesystem paths', error)
|
console.error('Failed to get filesystem paths', error)
|
||||||
this.$toast.error('Failed to get filesystem paths')
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
|
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
|
||||||
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
|
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
|
||||||
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
|
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
|
||||||
<span class="material-icons text-xl w-5">help_outline</span>
|
<span class="material-symbols text-xl w-5">help_outline</span>
|
||||||
</a>
|
</a>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="metadataSourceMapped" v-bind="dragOptions" class="list-group" draggable=".item" handle=".drag-handle" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10">
|
<li v-for="(source, index) in metadataSourceMapped" :key="source.id" :class="source.include ? 'item' : 'opacity-50'" class="w-full px-2 flex items-center relative border border-white/10">
|
||||||
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span>
|
<span class="material-symbols drag-handle text-xl text-gray-400 hover:text-gray-50 mr-2 md:mr-4">reorder</span>
|
||||||
<div class="text-center py-1 w-8 min-w-8">
|
<div class="text-center py-1 w-8 min-w-8">
|
||||||
{{ source.include ? getSourceIndex(source.id) : '' }}
|
{{ source.include ? getSourceIndex(source.id) : '' }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
{{ $strings.LabelSettingsSquareBookCovers }}
|
{{ $strings.LabelSettingsSquareBookCovers }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
{{ $strings.LabelSettingsAudiobooksOnly }}
|
{{ $strings.LabelSettingsAudiobooksOnly }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
{{ $strings.LabelSettingsHideSingleBookSeries }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
||||||
<p class="pl-4 text-base">
|
<p class="pl-4 text-base">
|
||||||
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
||||||
<span class="material-icons icon-text text-sm">info_outlined</span>
|
<span class="material-symbols icon-text text-sm">info</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default {
|
|||||||
return this.selectedEventData && this.selectedEventData.requiresLibrary
|
return this.selectedEventData && this.selectedEventData.requiresLibrary
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.isNew ? 'Create Notification' : 'Update Notification'
|
return this.isNew ? this.$strings.HeaderNotificationCreate : this.$strings.HeaderNotificationUpdate
|
||||||
},
|
},
|
||||||
availableVariables() {
|
availableVariables() {
|
||||||
return this.selectedEventData ? this.selectedEventData.variables || null : null
|
return this.selectedEventData ? this.selectedEventData.variables || null : null
|
||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
this.$refs.urlsInput?.forceBlur()
|
this.$refs.urlsInput?.forceBlur()
|
||||||
|
|
||||||
if (!this.newNotification.urls.length) {
|
if (!this.newNotification.urls.length) {
|
||||||
this.$toast.error('Must enter an Apprise URL')
|
this.$toast.error(this.$strings.ToastAppriseUrlRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +127,12 @@ export default {
|
|||||||
.$patch(`/api/notifications/${payload.id}`, payload)
|
.$patch(`/api/notifications/${payload.id}`, payload)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Notification updated')
|
this.$toast.success(this.$strings.ToastNotificationUpdateSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update notification', error)
|
console.error('Failed to update notification', error)
|
||||||
this.$toast.error('Failed to update notification')
|
this.$toast.error(this.$strings.ToastNotificationUpdateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -149,12 +149,11 @@ export default {
|
|||||||
.$post('/api/notifications', payload)
|
.$post('/api/notifications', payload)
|
||||||
.then((updatedSettings) => {
|
.then((updatedSettings) => {
|
||||||
this.$emit('update', updatedSettings)
|
this.$emit('update', updatedSettings)
|
||||||
this.$toast.success('Notification created')
|
|
||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create notification', error)
|
console.error('Failed to create notification', error)
|
||||||
this.$toast.error('Failed to create notification')
|
this.$toast.error(this.$strings.ToastNotificationCreateFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.processing = false
|
this.processing = false
|
||||||
|
|||||||
@@ -10,10 +10,10 @@
|
|||||||
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
<p v-if="isOpenInPlayer" class="text-sm text-right text-gray-400">{{ $strings.ButtonPlaying }}</p>
|
||||||
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
<div v-else-if="isHovering" class="flex items-center justify-end -mx-1">
|
||||||
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
<button class="outline-none mx-1 flex items-center" @click.stop="playClick">
|
||||||
<span class="material-icons text-2xl text-success">play_arrow</span>
|
<span class="material-symbols fill text-2xl text-success">play_arrow</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
<button class="outline-none mx-1 flex items-center" @click.stop="removeClick">
|
||||||
<span class="material-icons text-2xl text-error">close</span>
|
<span class="material-symbols text-2xl text-error">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
<p v-else class="text-gray-400 text-sm text-right">{{ durationPretty }}</p>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
|
<ui-checkbox v-model="playerQueueAutoPlay" label="Auto Play" medium checkbox-bg="primary" border-color="gray-600" label-class="pl-2 mb-px" />
|
||||||
</div>
|
</div>
|
||||||
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem" @remove="removeItem" />
|
<modals-player-queue-item-row v-for="(item, index) in playerQueueItems" :key="index" :item="item" :index="index" @play="playItem(index)" @remove="removeItem" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean
|
||||||
libraryItemId: String
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -50,11 +49,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
playItem(item) {
|
playItem(index) {
|
||||||
this.$eventBus.$emit('play-item', {
|
this.$eventBus.$emit('play-queue-item', {
|
||||||
libraryItemId: item.libraryItemId,
|
index
|
||||||
episodeId: item.episodeId || null,
|
|
||||||
queueItems: this.playerQueueItems
|
|
||||||
})
|
})
|
||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -130,12 +130,12 @@ export default {
|
|||||||
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
.$post(`/api/playlists/${playlist.id}/batch/remove`, { items: itemObjects })
|
||||||
.then((updatedPlaylist) => {
|
.then((updatedPlaylist) => {
|
||||||
console.log(`Items removed from playlist`, updatedPlaylist)
|
console.log(`Items removed from playlist`, updatedPlaylist)
|
||||||
this.$toast.success('Playlist item(s) removed')
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove items from playlist', error)
|
console.error('Failed to remove items from playlist', error)
|
||||||
this.$toast.error('Failed to remove playlist item(s)')
|
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -148,12 +148,12 @@ export default {
|
|||||||
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
.$post(`/api/playlists/${playlist.id}/batch/add`, { items: itemObjects })
|
||||||
.then((updatedPlaylist) => {
|
.then((updatedPlaylist) => {
|
||||||
console.log(`Items added to playlist`, updatedPlaylist)
|
console.log(`Items added to playlist`, updatedPlaylist)
|
||||||
this.$toast.success('Items added to playlist')
|
this.$toast.success(this.$strings.ToastPlaylistUpdateSuccess)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to add items to playlist', error)
|
console.error('Failed to add items to playlist', error)
|
||||||
this.$toast.error('Failed to add items to playlist')
|
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@@ -174,14 +174,14 @@ export default {
|
|||||||
.$post('/api/playlists', newPlaylist)
|
.$post('/api/playlists', newPlaylist)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log('New playlist created', data)
|
console.log('New playlist created', data)
|
||||||
this.$toast.success(`Playlist "${data.name}" created`)
|
this.$toast.success(this.$strings.ToastPlaylistCreateSuccess + ': ' + data.name)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.newPlaylistName = ''
|
this.newPlaylistName = ''
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to create playlist', error)
|
console.error('Failed to create playlist', error)
|
||||||
var errMsg = error.response ? error.response.data || '' : ''
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
this.$toast.error(`Failed to create playlist: ${errMsg}`)
|
this.$toast.error(this.$strings.ToastPlaylistCreateFailed + ': ' + errMsg)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove playlist', error)
|
console.error('Failed to remove playlist', error)
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.$toast.error(this.$strings.ToastPlaylistRemoveFailed)
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -95,7 +95,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newPlaylistName) {
|
if (!this.newPlaylistName) {
|
||||||
return this.$toast.error('Playlist must have a name')
|
return this.$toast.error(this.$strings.ToastNameRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processing = true
|
this.processing = true
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
<nuxt-link :to="`/playlist/${playlist.id}`" class="pl-2 pr-2 truncate hover:underline cursor-pointer" @click.native="clickNuxtLink">{{ playlist.name }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
<div class="h-full flex items-center justify-end transform" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||||
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-icons text-2xl pt-px">add</span></ui-btn>
|
<ui-btn v-if="!isItemIncluded" color="success" :padding-x="3" small class="h-9" @click.stop="clickAdd"><span class="material-symbols text-2xl pt-px">add</span></ui-btn>
|
||||||
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-icons text-2xl pt-px">remove</span></ui-btn>
|
<ui-btn v-else color="error" :padding-x="3" class="h-9" small @click.stop="clickRem"><span class="material-symbols text-2xl pt-px">remove</span></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevEpisode" @mousedown.prevent>arrow_back_ios</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
<div class="material-symbols text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextEpisode" @mousedown.prevent>arrow_forward_ios</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
@click="toggleSelectEpisode(episode)"
|
@click="toggleSelectEpisode(episode)"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||||
<span v-if="getIsEpisodeDownloaded(episode)" class="material-icons text-success text-xl">download_done</span>
|
<span v-if="getIsEpisodeDownloaded(episode)" class="material-symbols text-success text-xl">download_done</span>
|
||||||
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="px-8 py-2">
|
||||||
|
|||||||
@@ -16,11 +16,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-lg font-semibold mb-2">{{ $strings.HeaderPodcastsToAdd }}</p>
|
<p class="text-lg font-semibold mb-1">{{ $strings.HeaderPodcastsToAdd }}</p>
|
||||||
|
<p class="text-sm text-gray-300 mb-4">{{ $strings.MessageOpmlPreviewNote }}</p>
|
||||||
|
|
||||||
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
||||||
<template v-for="(feed, index) in feedMetadata">
|
<template v-for="(feed, index) in feeds">
|
||||||
<cards-podcast-feed-summary-card :key="index" :feed="feed" :library-folder-path="selectedFolderPath" class="my-1" />
|
<div :key="index" class="py-1 flex items-center">
|
||||||
|
<p class="text-lg font-semibold">{{ index + 1 }}.</p>
|
||||||
|
<div class="pl-2">
|
||||||
|
<p v-if="feed.title" class="text-sm font-semibold">{{ feed.title }}</p>
|
||||||
|
<p class="text-xs text-gray-400">{{ feed.feedUrl }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,9 +52,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedFolderId: null,
|
selectedFolderId: null,
|
||||||
fullPath: null,
|
autoDownloadEpisodes: false
|
||||||
autoDownloadEpisodes: false,
|
|
||||||
feedMetadata: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -96,73 +101,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toFeedMetadata(feed) {
|
|
||||||
const metadata = feed.metadata
|
|
||||||
return {
|
|
||||||
title: metadata.title,
|
|
||||||
author: metadata.author,
|
|
||||||
description: metadata.description,
|
|
||||||
releaseDate: '',
|
|
||||||
genres: [...metadata.categories],
|
|
||||||
feedUrl: metadata.feedUrl,
|
|
||||||
imageUrl: metadata.image,
|
|
||||||
itunesPageUrl: '',
|
|
||||||
itunesId: '',
|
|
||||||
itunesArtistId: '',
|
|
||||||
language: '',
|
|
||||||
numEpisodes: feed.numEpisodes
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init() {
|
init() {
|
||||||
this.feedMetadata = this.feeds.map(this.toFeedMetadata)
|
|
||||||
|
|
||||||
if (this.folderItems[0]) {
|
if (this.folderItems[0]) {
|
||||||
this.selectedFolderId = this.folderItems[0].value
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
const newFeedPayloads = this.feedMetadata.map((metadata) => {
|
|
||||||
return {
|
|
||||||
path: `${this.selectedFolderPath}/${this.$sanitizeFilename(metadata.title)}`,
|
|
||||||
folderId: this.selectedFolderId,
|
|
||||||
libraryId: this.currentLibrary.id,
|
|
||||||
media: {
|
|
||||||
metadata: {
|
|
||||||
...metadata
|
|
||||||
},
|
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log('New feed payloads', newFeedPayloads)
|
|
||||||
|
|
||||||
for (const podcastPayload of newFeedPayloads) {
|
const payload = {
|
||||||
await this.$axios
|
feeds: this.feeds.map((f) => f.feedUrl),
|
||||||
.$post('/api/podcasts', podcastPayload)
|
folderId: this.selectedFolderId,
|
||||||
.then(() => {
|
libraryId: this.currentLibrary.id,
|
||||||
this.$toast.success(`${podcastPayload.media.metadata.title}: ${this.$strings.ToastPodcastCreateSuccess}`)
|
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
|
|
||||||
console.error('Failed to create podcast', podcastPayload, error)
|
|
||||||
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.$axios
|
||||||
this.show = false
|
.$post('/api/podcasts/opml/create', payload)
|
||||||
|
.then(() => {
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = error.response?.data || this.$strings.ToastPodcastCreateFailed
|
||||||
|
console.error('Failed to create podcast', payload, error)
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#podcast-wrapper {
|
|
||||||
min-height: 400px;
|
|
||||||
max-height: 80vh;
|
|
||||||
}
|
|
||||||
#episodes-scroll {
|
|
||||||
max-height: calc(80vh - 200px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -142,7 +142,7 @@ export default {
|
|||||||
|
|
||||||
const updatedDetails = this.getUpdatePayload()
|
const updatedDetails = this.getUpdatePayload()
|
||||||
if (!Object.keys(updatedDetails).length) {
|
if (!Object.keys(updatedDetails).length) {
|
||||||
this.$toast.info('No changes were made')
|
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return this.updateDetails(updatedDetails)
|
return this.updateDetails(updatedDetails)
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default {
|
|||||||
}
|
}
|
||||||
const updatePayload = this.getUpdatePayload(episodeData)
|
const updatePayload = this.getUpdatePayload(episodeData)
|
||||||
if (!Object.keys(updatePayload).length) {
|
if (!Object.keys(updatePayload).length) {
|
||||||
return this.$toast.info('No updates are necessary')
|
return this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||||
}
|
}
|
||||||
console.log('Episode update payload', updatePayload)
|
console.log('Episode update payload', updatePayload)
|
||||||
|
|
||||||
@@ -126,13 +126,13 @@ export default {
|
|||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.episodeTitle || !this.episodeTitle.length) {
|
if (!this.episodeTitle || !this.episodeTitle.length) {
|
||||||
this.$toast.error('Must enter an episode title')
|
this.$toast.error(this.$strings.ToastTitleRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.searchedTitle = this.episodeTitle
|
this.searchedTitle = this.episodeTitle
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`)
|
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${encodeURIComponent(this.episodeTitle)}`)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
||||||
console.log('Episodes found', this.episodesFound)
|
console.log('Episodes found', this.episodesFound)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="currentFeed.meta" class="mt-5">
|
<div v-if="currentFeed.meta" class="mt-5">
|
||||||
@@ -121,14 +121,14 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
openFeed() {
|
openFeed() {
|
||||||
if (!this.newFeedSlug) {
|
if (!this.newFeedSlug) {
|
||||||
this.$toast.error('Must set a feed slug')
|
this.$toast.error(this.$strings.ToastSlugRequired)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
||||||
if (this.newFeedSlug !== sanitized) {
|
if (this.newFeedSlug !== sanitized) {
|
||||||
this.newFeedSlug = sanitized
|
this.newFeedSlug = sanitized
|
||||||
this.$toast.warning('Slug had to be modified - Run again')
|
this.$toast.warning(this.$strings.ToastSlugMustChange)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="feed.feedUrl" readonly />
|
<ui-text-input v-model="feed.feedUrl" readonly />
|
||||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
<span class="material-symbols absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feed.feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="feed.meta" class="mt-5">
|
<div v-if="feed.meta" class="mt-5">
|
||||||
|
|||||||
@@ -4,32 +4,32 @@
|
|||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
||||||
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
|
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpBackward">
|
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||||
<button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpForward">
|
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||||
<button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
|
<ui-tooltip direction="top" :text="hasNextLabel" class="ml-4 lg:ml-8">
|
||||||
<button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
<button :aria-label="hasNextLabel" :disabled="!hasNext" class="text-gray-300 disabled:text-gray-500" @mousedown.prevent @mouseup.prevent @click.stop="next">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
|
<span class="material-symbols text-2xl sm:text-3xl">last_page</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||||
<span class="material-icons">autorenew</span>
|
<span class="material-symbols text-2xl">autorenew</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -43,7 +43,8 @@ export default {
|
|||||||
seekLoading: Boolean,
|
seekLoading: Boolean,
|
||||||
playbackRate: Number,
|
playbackRate: Number,
|
||||||
paused: Boolean,
|
paused: Boolean,
|
||||||
hasNextChapter: Boolean
|
hasNextChapter: Boolean,
|
||||||
|
hasNextItemInQueue: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
@@ -56,6 +57,19 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('update:playbackRate', val)
|
this.$emit('update:playbackRate', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
jumpForwardText() {
|
||||||
|
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
||||||
|
},
|
||||||
|
jumpBackwardText() {
|
||||||
|
return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward)
|
||||||
|
},
|
||||||
|
hasNextLabel() {
|
||||||
|
if (this.hasNextItemInQueue && !this.hasNextChapter) return this.$strings.ButtonNextItemInQueue
|
||||||
|
return this.$strings.ButtonNextChapter
|
||||||
|
},
|
||||||
|
hasNext() {
|
||||||
|
return this.hasNextItemInQueue || this.hasNextChapter
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -65,9 +79,9 @@ export default {
|
|||||||
prevChapter() {
|
prevChapter() {
|
||||||
this.$emit('prevChapter')
|
this.$emit('prevChapter')
|
||||||
},
|
},
|
||||||
nextChapter() {
|
next() {
|
||||||
if (!this.hasNextChapter) return
|
if (!this.hasNext) return
|
||||||
this.$emit('nextChapter')
|
this.$emit('next')
|
||||||
},
|
},
|
||||||
jumpBackward() {
|
jumpBackward() {
|
||||||
this.$emit('jumpBackward')
|
this.$emit('jumpBackward')
|
||||||
@@ -83,6 +97,20 @@ export default {
|
|||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||||
console.error('Failed to update settings', err)
|
console.error('Failed to update settings', err)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
getJumpText(setting, prefix) {
|
||||||
|
const amount = this.$store.getters['user/getUserSetting'](setting)
|
||||||
|
if (!amount) return prefix
|
||||||
|
|
||||||
|
let formattedTime = ''
|
||||||
|
if (amount <= 60) {
|
||||||
|
formattedTime = this.$getString('LabelTimeDurationXSeconds', [amount])
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(amount / 60)
|
||||||
|
formattedTime = this.$getString('LabelTimeDurationXMinutes', [minutes])
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix} - ${formattedTime}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="w-full -mt-6">
|
<div class="w-full -mt-6">
|
||||||
<div class="w-full relative mb-1">
|
<div class="w-full relative mb-1">
|
||||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||||
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
<!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||||
|
|
||||||
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
||||||
@@ -10,40 +10,40 @@
|
|||||||
|
|
||||||
<ui-tooltip v-if="!hideSleepTimer" direction="top" :text="$strings.LabelSleepTimer">
|
<ui-tooltip v-if="!hideSleepTimer" direction="top" :text="$strings.LabelSleepTimer">
|
||||||
<button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
<button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||||
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<span class="material-icons text-lg text-warning">snooze</span>
|
<span class="material-symbols text-lg text-warning">snooze</span>
|
||||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast && !hideBookmarks" direction="top" :text="$strings.LabelViewBookmarks">
|
<ui-tooltip v-if="!isPodcast && !hideBookmarks" direction="top" :text="$strings.LabelViewBookmarks">
|
||||||
<button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
<button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||||
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
<span class="material-symbols text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
||||||
<button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<span class="material-symbols text-2xl">format_list_bulleted</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
|
||||||
<button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
<button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
|
||||||
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
|
<span class="material-symbols text-2.5xl sm:text-3xl">playlist_play</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
||||||
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
|
||||||
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate.sync="playbackRate" :paused="paused" :hasNextChapter="hasNextChapter" :hasNextItemInQueue="hasNextItemInQueue" @prevChapter="prevChapter" @next="goToNext" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
||||||
@@ -72,15 +72,18 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
currentChapter: Object,
|
||||||
bookmarks: {
|
bookmarks: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
sleepTimerSet: Boolean,
|
sleepTimerSet: Boolean,
|
||||||
sleepTimerRemaining: Number,
|
sleepTimerRemaining: Number,
|
||||||
|
sleepTimerType: String,
|
||||||
isPodcast: Boolean,
|
isPodcast: Boolean,
|
||||||
hideBookmarks: Boolean,
|
hideBookmarks: Boolean,
|
||||||
hideSleepTimer: Boolean
|
hideSleepTimer: Boolean,
|
||||||
|
hasNextItemInQueue: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -90,27 +93,34 @@ export default {
|
|||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
showChaptersModal: false,
|
showChaptersModal: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0,
|
duration: 0
|
||||||
useChapterTrack: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
playbackRate() {
|
playbackRate() {
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
|
},
|
||||||
|
useChapterTrack() {
|
||||||
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
|
this.updateTimestamp()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sleepTimerRemainingString() {
|
sleepTimerRemainingString() {
|
||||||
var rounded = Math.round(this.sleepTimerRemaining)
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER) {
|
||||||
if (rounded < 90) {
|
return 'EoC'
|
||||||
return `${rounded}s`
|
} else {
|
||||||
|
var rounded = Math.round(this.sleepTimerRemaining)
|
||||||
|
if (rounded < 90) {
|
||||||
|
return `${rounded}s`
|
||||||
|
}
|
||||||
|
var minutesRounded = Math.round(rounded / 60)
|
||||||
|
if (minutesRounded <= 90) {
|
||||||
|
return `${minutesRounded}m`
|
||||||
|
}
|
||||||
|
var hoursRounded = Math.round(minutesRounded / 60)
|
||||||
|
return `${hoursRounded}h`
|
||||||
}
|
}
|
||||||
var minutesRounded = Math.round(rounded / 60)
|
|
||||||
if (minutesRounded < 90) {
|
|
||||||
return `${minutesRounded}m`
|
|
||||||
}
|
|
||||||
var hoursRounded = Math.round(minutesRounded / 60)
|
|
||||||
return `${hoursRounded}h`
|
|
||||||
},
|
},
|
||||||
token() {
|
token() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
@@ -135,11 +145,8 @@ export default {
|
|||||||
if (!duration) return 0
|
if (!duration) return 0
|
||||||
return Math.round((100 * time) / duration)
|
return Math.round((100 * time) / duration)
|
||||||
},
|
},
|
||||||
currentChapter() {
|
|
||||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
|
||||||
},
|
|
||||||
currentChapterName() {
|
currentChapterName() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter?.title || ''
|
||||||
},
|
},
|
||||||
currentChapterDuration() {
|
currentChapterDuration() {
|
||||||
if (!this.currentChapter) return 0
|
if (!this.currentChapter) return 0
|
||||||
@@ -162,6 +169,10 @@ export default {
|
|||||||
},
|
},
|
||||||
playerQueueItems() {
|
playerQueueItems() {
|
||||||
return this.$store.state.playerQueueItems || []
|
return this.$store.state.playerQueueItems || []
|
||||||
|
},
|
||||||
|
useChapterTrack() {
|
||||||
|
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||||
|
return this.chapters.length ? _useChapterTrack : false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -268,10 +279,13 @@ export default {
|
|||||||
this.seek(this.currentChapter.start)
|
this.seek(this.currentChapter.start)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nextChapter() {
|
goToNext() {
|
||||||
if (!this.currentChapter || !this.hasNextChapter) return
|
if (this.hasNextChapter) {
|
||||||
var nextChapter = this.chapters[this.currentChapterIndex + 1]
|
const nextChapter = this.chapters[this.currentChapterIndex + 1]
|
||||||
this.seek(nextChapter.start)
|
this.seek(nextChapter.start)
|
||||||
|
} else if (this.hasNextItemInQueue) {
|
||||||
|
this.$emit('nextItemInQueue')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setStreamReady() {
|
setStreamReady() {
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
||||||
@@ -310,9 +324,6 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
|
||||||
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
|
||||||
|
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
this.setPlaybackRate(this.playbackRate)
|
this.setPlaybackRate(this.playbackRate)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
|
<div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
|
||||||
<span class="material-icons text-xl">menu</span>
|
<span class="material-symbols text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
|
<div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
|
||||||
<span class="material-icons text-xl">more</span>
|
<span class="material-symbols text-xl">more</span>
|
||||||
</div>
|
</div>
|
||||||
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'">
|
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'">
|
||||||
<span class="material-icons text-xl">download</span>
|
<span class="material-symbols text-xl">download</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
<div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative">
|
||||||
<div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
<div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
||||||
<div class="flex items-center justify-center h-full w-1/2">
|
<div class="flex items-center justify-center h-full w-1/2">
|
||||||
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
<span v-show="loadedFirstPage" class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
|
<div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
|
||||||
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
|
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
|
||||||
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
|
<span v-show="loadedFirstPage" class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="imageContainer" class="w-full h-full relative overflow-auto">
|
<div ref="imageContainer" class="w-full h-full relative overflow-auto">
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div id="epub-reader" class="h-full w-full">
|
<div id="epub-reader" class="h-full w-full">
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
|
<button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
|
||||||
<span v-if="hasPrev" class="material-icons text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-symbols text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
<div id="frame" class="w-full" style="height: 80%">
|
<div id="frame" class="w-full" style="height: 80%">
|
||||||
<div id="viewer"></div>
|
<div id="viewer"></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
|
<button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
|
||||||
<span v-if="hasNext" class="material-icons text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
<span v-if="hasNext" class="material-symbols text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<div class="w-full h-full pt-20 relative">
|
<div class="w-full h-full pt-20 relative">
|
||||||
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
||||||
<div class="flex items-center justify-center h-full w-1/2">
|
<div class="flex items-center justify-center h-full w-1/2">
|
||||||
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
<span class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
|
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
|
||||||
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
|
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
|
||||||
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
|
<span class="material-symbols text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
||||||
<div class="absolute top-4 left-4 z-20 flex items-center">
|
<div class="absolute top-4 left-4 z-20 flex items-center">
|
||||||
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
|
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
|
||||||
<span class="material-icons text-2xl">menu</span>
|
<span class="material-symbols text-2xl">menu</span>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="mx-4 inline-flex opacity-80 hover:opacity-100">
|
<button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="mx-4 inline-flex opacity-80 hover:opacity-100">
|
||||||
<span class="material-icons text-1.5xl">settings</span>
|
<span class="material-symbols text-1.5xl">settings</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<div class="absolute top-4 right-4 z-20">
|
<div class="absolute top-4 right-4 z-20">
|
||||||
<button @click="close" type="button" aria-label="Close ereader" class="inline-flex opacity-80 hover:opacity-100">
|
<button @click="close" type="button" aria-label="Close ereader" class="inline-flex opacity-80 hover:opacity-100">
|
||||||
<span class="material-icons text-2xl">close</span>
|
<span class="material-symbols text-2xl">close</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="flex flex-col p-4 h-full">
|
<div class="flex flex-col p-4 h-full">
|
||||||
<div class="flex items-center mb-2">
|
<div class="flex items-center mb-2">
|
||||||
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
|
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
|
||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<span class="material-symbols text-2xl">arrow_back</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
|
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<span class="material-icons text-5xl py-1">show_chart</span>
|
<span class="material-symbols text-5xl py-1">show_chart</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
|
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
|
||||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<span class="material-icons-outlined text-5xl pt-1">insert_drive_file</span>
|
<span class="material-symbols text-5xl pt-1">insert_drive_file</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
|
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
|
||||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<span class="material-icons-outlined text-5xl pt-1">audio_file</span>
|
<span class="material-symbols text-5xl pt-1">audio_file</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
|
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
|
||||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default {
|
|||||||
|
|
||||||
const addIcon = (icon, color, fontSize, x, y) => {
|
const addIcon = (icon, color, fontSize, x, y) => {
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = color
|
||||||
ctx.font = `${fontSize} Material Icons Outlined`
|
ctx.font = `${fontSize} Material Symbols Rounded`
|
||||||
ctx.fillText(icon, x, y)
|
ctx.fillText(icon, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +132,8 @@ export default {
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const twoColumnWidth = 210
|
||||||
|
|
||||||
ctx.globalAlpha = 1
|
ctx.globalAlpha = 1
|
||||||
ctx.textBaseline = 'middle'
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
@@ -150,12 +152,12 @@ export default {
|
|||||||
|
|
||||||
// Top text
|
// Top text
|
||||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
// Top left box
|
// Top left box
|
||||||
createRoundedRect(50, 100, 340, 160)
|
createRoundedRect(50, 100, 340, 160)
|
||||||
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
|
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
|
||||||
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
|
addText(this.$strings.StatsBooksFinished, '28px', 'normal', tanColor, '0px', 160, 210, twoColumnWidth)
|
||||||
const readIconPath = new Path2D()
|
const readIconPath = new Path2D()
|
||||||
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
|
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 })
|
||||||
ctx.fillStyle = '#ffffff'
|
ctx.fillStyle = '#ffffff'
|
||||||
@@ -164,40 +166,40 @@ export default {
|
|||||||
// Box top right
|
// Box top right
|
||||||
createRoundedRect(410, 100, 340, 160)
|
createRoundedRect(410, 100, 340, 160)
|
||||||
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
|
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
|
||||||
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
|
addText(this.$strings.StatsSpentListening, '28px', 'normal', tanColor, '0px', 500, 205, twoColumnWidth)
|
||||||
addIcon('watch_later', 'white', '52px', 440, 180)
|
addIcon('watch_later', 'white', '52px', 440, 180)
|
||||||
|
|
||||||
// Box bottom left
|
// Box bottom left
|
||||||
createRoundedRect(50, 280, 340, 160)
|
createRoundedRect(50, 280, 340, 160)
|
||||||
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
|
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
|
||||||
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
|
addText(this.$strings.StatsSessions, '28px', 'normal', tanColor, '1px', 160, 390, twoColumnWidth)
|
||||||
addIcon('headphones', 'white', '52px', 95, 360)
|
addIcon('headphones', 'white', '52px', 95, 360)
|
||||||
|
|
||||||
// Box bottom right
|
// Box bottom right
|
||||||
createRoundedRect(410, 280, 340, 160)
|
createRoundedRect(410, 280, 340, 160)
|
||||||
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
|
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
|
||||||
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
|
addText(this.$strings.StatsBooksListenedTo, '28px', 'normal', tanColor, '0px', 500, 390, twoColumnWidth)
|
||||||
addIcon('local_library', 'white', '52px', 440, 360)
|
addIcon('local_library', 'white', '52px', 440, 360)
|
||||||
|
|
||||||
if (!this.variant) {
|
if (!this.variant) {
|
||||||
// Text stats
|
// Text stats
|
||||||
const topNarrator = this.yearStats.mostListenedNarrator
|
const topNarrator = this.yearStats.mostListenedNarrator
|
||||||
if (topNarrator) {
|
if (topNarrator) {
|
||||||
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
|
addText(this.$strings.StatsTopNarrator, '24px', 'normal', tanColor, '1px', 70, 520, 330)
|
||||||
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
|
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
|
||||||
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
|
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
|
||||||
}
|
}
|
||||||
|
|
||||||
const topGenre = this.yearStats.topGenres[0]
|
const topGenre = this.yearStats.topGenres[0]
|
||||||
if (topGenre) {
|
if (topGenre) {
|
||||||
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
|
addText(this.$strings.StatsTopGenre, '24px', 'normal', tanColor, '1px', 430, 520, 330)
|
||||||
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
|
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
|
||||||
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
|
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
|
||||||
}
|
}
|
||||||
|
|
||||||
const topAuthor = this.yearStats.topAuthors[0]
|
const topAuthor = this.yearStats.topAuthors[0]
|
||||||
if (topAuthor) {
|
if (topAuthor) {
|
||||||
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
|
addText(this.$strings.StatsTopAuthor, '24px', 'normal', tanColor, '1px', 70, 670, 330)
|
||||||
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
|
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
|
||||||
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
|
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
|
||||||
}
|
}
|
||||||
@@ -205,7 +207,7 @@ export default {
|
|||||||
if (this.yearStats.mostListenedMonth?.time) {
|
if (this.yearStats.mostListenedMonth?.time) {
|
||||||
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
|
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
|
||||||
const monthName = this.$formatJsDate(jsdate, 'LLLL')
|
const monthName = this.$formatJsDate(jsdate, 'LLLL')
|
||||||
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
|
addText(this.$strings.StatsTopMonth, '24px', 'normal', tanColor, '1px', 430, 670, 330)
|
||||||
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
|
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
|
||||||
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
|
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
|
||||||
}
|
}
|
||||||
@@ -214,7 +216,7 @@ export default {
|
|||||||
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
|
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
|
||||||
if (finishedBookCoverImgs.length > 0) {
|
if (finishedBookCoverImgs.length > 0) {
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
|
addText(this.$strings.StatsBooksFinishedThisYear, '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
|
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
|
||||||
let imgToAdd = finishedBookCoverImgs[i]
|
let imgToAdd = finishedBookCoverImgs[i]
|
||||||
@@ -224,14 +226,14 @@ export default {
|
|||||||
} else if (this.variant === 2) {
|
} else if (this.variant === 2) {
|
||||||
// Text stats
|
// Text stats
|
||||||
if (this.yearStats.topAuthors.length) {
|
if (this.yearStats.topAuthors.length) {
|
||||||
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
|
addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 524)
|
||||||
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||||
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
|
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.yearStats.topGenres.length) {
|
if (this.yearStats.topGenres.length) {
|
||||||
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
|
addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 524)
|
||||||
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
||||||
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
|
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
|
||||||
}
|
}
|
||||||
@@ -259,11 +261,11 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to share', error)
|
console.error('Failed to share', error)
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
this.$toast.error('Failed to share: ' + error.message)
|
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Cannot share natively on this device')
|
this.$toast.error(this.$strings.ToastErrorCannotShare)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
|
||||||
<!-- hack to get icon fonts loaded on init -->
|
<!-- hack to get icon fonts loaded on init -->
|
||||||
<div class="h-0 w-0 overflow-hidden opacity-0">
|
<div class="h-0 w-0 overflow-hidden opacity-0">
|
||||||
<span class="material-icons-outlined">close</span>
|
<span class="material-symbols">close</span>
|
||||||
<span class="abs-icons icon-audiobookshelf" />
|
<span class="abs-icons icon-audiobookshelf" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
|
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
|
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide : $strings.LabelYearReviewShow }}</ui-btn>
|
||||||
$strings.LabelYearReviewShow }}</ui-btn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- your year in review -->
|
<!-- your year in review -->
|
||||||
@@ -20,29 +19,26 @@
|
|||||||
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
|
||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
|
||||||
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{ $strings.ButtonShare }} </ui-btn>
|
||||||
$strings.ButtonShare }}
|
|
||||||
</ui-btn>
|
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
|
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}</p>
|
||||||
</p>
|
|
||||||
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<!-- refresh button -->
|
<!-- refresh button -->
|
||||||
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
|
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
|
||||||
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
|
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
|
||||||
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
|
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
|
||||||
@@ -59,12 +55,11 @@
|
|||||||
<div class="flex items-center justify-center mb-2">
|
<div class="flex items-center justify-center mb-2">
|
||||||
<!-- previous button -->
|
<!-- previous button -->
|
||||||
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
|
||||||
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
<span class="material-symbols text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
|
||||||
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- share button -->
|
<!-- share button -->
|
||||||
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }}
|
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }} </ui-btn>
|
||||||
</ui-btn>
|
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
|
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
|
||||||
@@ -74,12 +69,12 @@
|
|||||||
<!-- refresh button -->
|
<!-- refresh button -->
|
||||||
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
|
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
|
||||||
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
|
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
|
||||||
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
|
<span class="material-symbols sm:!hidden text-lg py-px">refresh</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<!-- next button -->
|
<!-- next button -->
|
||||||
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
|
||||||
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
|
||||||
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
<span class="material-symbols text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ export default {
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const threeColumnTextWidth = 200
|
||||||
|
|
||||||
ctx.globalAlpha = 1
|
ctx.globalAlpha = 1
|
||||||
ctx.textBaseline = 'middle'
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
@@ -141,33 +143,33 @@ export default {
|
|||||||
|
|
||||||
// Top text
|
// Top text
|
||||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
// Top left box
|
// Top left box
|
||||||
createRoundedRect(40, 100, 230, 100)
|
createRoundedRect(40, 100, 230, 100)
|
||||||
ctx.textAlign = 'center'
|
ctx.textAlign = 'center'
|
||||||
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
|
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
|
||||||
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
|
addText(this.$strings.StatsBooksAdded, '18px', 'normal', tanColor, '0px', 155, 170, threeColumnTextWidth)
|
||||||
|
|
||||||
// Box top right
|
// Box top right
|
||||||
createRoundedRect(285, 100, 230, 100)
|
createRoundedRect(285, 100, 230, 100)
|
||||||
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
|
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
|
||||||
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
|
addText(this.$strings.StatsAuthorsAdded, '18px', 'normal', tanColor, '0px', 400, 170, threeColumnTextWidth)
|
||||||
|
|
||||||
// Box bottom left
|
// Box bottom left
|
||||||
createRoundedRect(530, 100, 230, 100)
|
createRoundedRect(530, 100, 230, 100)
|
||||||
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
|
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
|
||||||
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
|
addText(this.$strings.StatsSessions, '18px', 'normal', tanColor, '1px', 645, 170, threeColumnTextWidth)
|
||||||
|
|
||||||
// Text stats
|
// Text stats
|
||||||
if (this.yearStats.totalBooksAddedSize) {
|
if (this.yearStats.totalBooksAddedSize) {
|
||||||
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
|
addText(this.$strings.StatsCollectionGrewTo, '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
|
||||||
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
|
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
|
||||||
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
|
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.yearStats.totalBooksAddedDuration) {
|
if (this.yearStats.totalBooksAddedDuration) {
|
||||||
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
|
addText(this.$strings.StatsTotalDuration, '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
|
||||||
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
|
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
|
||||||
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
|
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
|
||||||
}
|
}
|
||||||
@@ -176,7 +178,7 @@ export default {
|
|||||||
// Bottom images
|
// Bottom images
|
||||||
imgsToAdd = Object.values(imgsToAdd)
|
imgsToAdd = Object.values(imgsToAdd)
|
||||||
if (imgsToAdd.length > 0) {
|
if (imgsToAdd.length > 0) {
|
||||||
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
|
addText(this.$strings.StatsBooksAdditional, '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
|
||||||
|
|
||||||
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
|
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
|
||||||
let imgToAdd = imgsToAdd[i]
|
let imgToAdd = imgsToAdd[i]
|
||||||
@@ -187,14 +189,14 @@ export default {
|
|||||||
// Text stats
|
// Text stats
|
||||||
ctx.textAlign = 'left'
|
ctx.textAlign = 'left'
|
||||||
if (this.yearStats.topAuthors.length) {
|
if (this.yearStats.topAuthors.length) {
|
||||||
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
|
addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330)
|
||||||
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||||
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.yearStats.topNarrators.length) {
|
if (this.yearStats.topNarrators.length) {
|
||||||
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
|
addText(this.$strings.StatsTopNarrators, '24px', 'normal', tanColor, '1px', 430, 549)
|
||||||
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
|
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
|
||||||
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
||||||
}
|
}
|
||||||
@@ -203,14 +205,14 @@ export default {
|
|||||||
// Text stats
|
// Text stats
|
||||||
ctx.textAlign = 'left'
|
ctx.textAlign = 'left'
|
||||||
if (this.yearStats.topAuthors.length) {
|
if (this.yearStats.topAuthors.length) {
|
||||||
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
|
addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330)
|
||||||
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
|
||||||
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.yearStats.topGenres.length) {
|
if (this.yearStats.topGenres.length) {
|
||||||
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
|
addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 549)
|
||||||
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
|
||||||
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
|
||||||
}
|
}
|
||||||
@@ -235,11 +237,11 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to share', error)
|
console.error('Failed to share', error)
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
this.$toast.error('Failed to share: ' + error.message)
|
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Cannot share natively on this device')
|
this.$toast.error(this.$strings.ToastErrorCannotShare)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default {
|
|||||||
|
|
||||||
const addIcon = (icon, color, fontSize, x, y) => {
|
const addIcon = (icon, color, fontSize, x, y) => {
|
||||||
ctx.fillStyle = color
|
ctx.fillStyle = color
|
||||||
ctx.font = `${fontSize} Material Icons Outlined`
|
ctx.font = `${fontSize} Material Symbols Rounded`
|
||||||
ctx.fillText(icon, x, y)
|
ctx.fillText(icon, x, y)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +113,8 @@ export default {
|
|||||||
ctx.restore()
|
ctx.restore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const twoColumnWidth = 180
|
||||||
|
|
||||||
ctx.globalAlpha = 1
|
ctx.globalAlpha = 1
|
||||||
ctx.textBaseline = 'middle'
|
ctx.textBaseline = 'middle'
|
||||||
|
|
||||||
@@ -131,12 +133,12 @@ export default {
|
|||||||
|
|
||||||
// Top text
|
// Top text
|
||||||
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
|
||||||
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
|
addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51)
|
||||||
|
|
||||||
// Top left box
|
// Top left box
|
||||||
createRoundedRect(15, 75, 280, 110)
|
createRoundedRect(15, 75, 280, 110)
|
||||||
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
|
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
|
||||||
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
|
addText(this.$strings.StatsBooksFinished, '20px', 'normal', tanColor, '0px', 105, 155, twoColumnWidth)
|
||||||
const readIconPath = new Path2D()
|
const readIconPath = new Path2D()
|
||||||
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
|
readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 })
|
||||||
ctx.fillStyle = '#ffffff'
|
ctx.fillStyle = '#ffffff'
|
||||||
@@ -144,7 +146,7 @@ export default {
|
|||||||
|
|
||||||
createRoundedRect(305, 75, 280, 110)
|
createRoundedRect(305, 75, 280, 110)
|
||||||
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
|
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
|
||||||
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
|
addText(this.$strings.StatsBooksListenedTo, '20px', 'normal', tanColor, '0px', 400, 155, twoColumnWidth)
|
||||||
addIcon('local_library', 'white', '42px', 345, 130)
|
addIcon('local_library', 'white', '42px', 345, 130)
|
||||||
|
|
||||||
this.canvas = canvas
|
this.canvas = canvas
|
||||||
@@ -165,11 +167,11 @@ export default {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to share', error)
|
console.error('Failed to share', error)
|
||||||
if (error.name !== 'AbortError') {
|
if (error.name !== 'AbortError') {
|
||||||
this.$toast.error('Failed to share: ' + error.message)
|
this.$toast.error(this.$strings.ToastFailedToShare + ': ' + error.message)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Cannot share natively on this device')
|
this.$toast.error(this.$strings.ToastErrorCannotShare)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
<div class="w-full flex flex-row items-center justify-center">
|
<div class="w-full flex flex-row items-center justify-center">
|
||||||
<ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
|
<ui-btn v-if="backup.serverVersion && backup.key" small color="primary" @click="applyBackup(backup)">{{ $strings.ButtonRestore }}</ui-btn>
|
||||||
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
<ui-tooltip v-else text="This backup was created with an old version of audiobookshelf no longer supported" direction="bottom" class="mx-2 flex items-center">
|
||||||
<span class="material-icons-outlined text-2xl text-error">error_outline</span>
|
<span class="material-symbols text-2xl text-error">error_outline</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<button aria-label="Download Backup" class="inline-flex material-icons text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
|
<button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
|
||||||
|
|
||||||
<button aria-label="Delete Backup" class="inline-flex material-icons text-xl mx-1 text-white/70 hover:text-error" @click="deleteBackupClick(backup)">delete</button>
|
<button aria-label="Delete Backup" class="inline-flex material-symbols text-xl mx-1 text-white/70 hover:text-error" @click="deleteBackupClick(backup)">delete</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -186,7 +186,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.loadBackups()
|
this.loadBackups()
|
||||||
if (this.$route.query.backup) {
|
if (this.$route.query.backup) {
|
||||||
this.$toast.success('Backup applied successfully')
|
this.$toast.success(this.$strings.ToastBackupAppliedSuccess)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
|
<ui-btn v-if="userCanUpdate" small :to="`/audiobook/${libraryItemId}/chapters`" color="primary" class="mr-2" @click="clickEditChapters">{{ $strings.ButtonEditChapters }}</ui-btn>
|
||||||
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
|
<div v-if="!keepOpen" class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="expanded ? 'transform rotate-180' : ''">
|
||||||
<span class="material-icons text-4xl">expand_more</span>
|
<span class="material-symbols text-4xl"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update collection', error)
|
console.error('Failed to update collection', error)
|
||||||
this.$toast.error('Failed to save collection books order')
|
this.$toast.error(this.$strings.ToastCollectionUpdateFailed)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editBook(book) {
|
editBook(book) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="py-0">
|
<td class="py-0">
|
||||||
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
|
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
|
||||||
<button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button>
|
<button type="button" :aria-label="$strings.ButtonDelete" class="material-symbols text-base">delete</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -45,7 +45,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
removeProvider(provider) {
|
removeProvider(provider) {
|
||||||
const payload = {
|
const payload = {
|
||||||
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
|
message: this.$getString('MessageConfirmDeleteMetadataProvider', [provider.name]),
|
||||||
callback: (confirmed) => {
|
callback: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
this.$emit('update:processing', true)
|
this.$emit('update:processing', true)
|
||||||
@@ -53,12 +53,12 @@ export default {
|
|||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/custom-metadata-providers/${provider.id}`)
|
.$delete(`/api/custom-metadata-providers/${provider.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success('Provider removed')
|
this.$toast.success(this.$strings.ToastProviderRemoveSuccess)
|
||||||
this.$emit('removed', provider.id)
|
this.$emit('removed', provider.id)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to remove provider', error)
|
console.error('Failed to remove provider', error)
|
||||||
this.$toast.error('Failed to remove provider')
|
this.$toast.error(this.$strings.ToastRemoveFailed)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.$emit('update:processing', false)
|
this.$emit('update:processing', false)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
|
<ui-btn v-if="userIsAdmin" small :color="showFullPath ? 'gray-600' : 'primary'" class="mr-2 hidden md:block" @click.stop="toggleFullPath">{{ $strings.ButtonFullPath }}</ui-btn>
|
||||||
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
<div class="cursor-pointer h-10 w-10 rounded-full hover:bg-black-400 flex justify-center items-center duration-500" :class="showFiles ? 'transform rotate-180' : ''">
|
||||||
<span class="material-icons text-4xl">expand_more</span>
|
<span class="material-symbols text-4xl"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
||||||
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
||||||
<th class="text-left px-4 w-24">
|
<th class="text-left px-4 w-24">
|
||||||
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-icons-outlined text-sm align-middle">info</span></ui-tooltip>
|
{{ $strings.LabelRead }} <ui-tooltip :text="$strings.LabelReadEbookWithoutProgress" direction="top" class="inline-block"><span class="material-symbols text-sm align-middle">info</span></ui-tooltip>
|
||||||
</th>
|
</th>
|
||||||
<th v-if="showMoreColumn" class="text-center w-16"></th>
|
<th v-if="showMoreColumn" class="text-center w-16"></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user