mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-02 00:40:39 +02:00
Compare commits
250 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2d61f38e | |||
| ca2c2f2702 | |||
| 1fc929ab33 | |||
| f5495d64a9 | |||
| d6afb17bf2 | |||
| 2cba9d8f4a | |||
| e02169907d | |||
| 24a142e718 | |||
| 2cb4f972d7 | |||
| 513d946faa | |||
| 87d1f457ba | |||
| 8810f90226 | |||
| 3d3571013f | |||
| 605a6d8b25 | |||
| 1bfa4b31f2 | |||
| 7a14b49aea | |||
| 95ac74d748 | |||
| fddf850a41 | |||
| d93d4f3236 | |||
| 91f15d5a23 | |||
| 516c5c3308 | |||
| f702c02859 | |||
| ad88de0571 | |||
| b64a651b27 | |||
| 06b8d1194c | |||
| 377ae7ab19 | |||
| 53cf6edd6a | |||
| 92bedeac15 | |||
| 3cf8b9dca9 | |||
| bcc2f847f9 | |||
| f1421f351b | |||
| ed23feaf3f | |||
| 668ebf8550 | |||
| a8c7905f6d | |||
| 45cd39ac0c | |||
| 21e1f62c65 | |||
| 8416f2d6be | |||
| 3b4ac3a230 | |||
| 6244909332 | |||
| 5db949e4a7 | |||
| c453d3e8c7 | |||
| 9d7ffdfcd0 | |||
| 976427b0b3 | |||
| 6cbfd8679b | |||
| 217bbb4a8e | |||
| 9916a1e8f6 | |||
| 372101592c | |||
| 18123664ee | |||
| 2e6e4f970c | |||
| 1c9e56ce2e | |||
| 9e7b84f289 | |||
| 7b83ab8970 | |||
| 86ee4dcff2 | |||
| 277a5fa37c | |||
| 51b87912f8 | |||
| 653019921e | |||
| ccc291067d | |||
| af7e3a03f0 | |||
| 7c40d26857 | |||
| 6c507de501 | |||
| 482a4340f5 | |||
| 21e704e12c | |||
| 2b91bff1af | |||
| d11f9608b4 | |||
| 2b0b691b69 | |||
| 5dfd5c4971 | |||
| 201f1bff3e | |||
| a22ebb257f | |||
| bf6e87d4bc | |||
| b823a93ae2 | |||
| 05afd12682 | |||
| 997e23150e | |||
| 3c5bf376b5 | |||
| bca2cfda13 | |||
| 916b41d587 | |||
| ab08d83c04 | |||
| 415e0a7b5a | |||
| d301c12acd | |||
| 7aa7e662b2 | |||
| 1dbfb5637a | |||
| 4e1aacb44f | |||
| 954cf3e14e | |||
| b61ecefce4 | |||
| 8562b8d1b3 | |||
| 06ec2159f5 | |||
| 68b565505e | |||
| 83ff2752dd | |||
| d0af1c3c9a | |||
| 1ad46d4fb8 | |||
| d3dd13eae5 | |||
| f27982d887 | |||
| 624a44f572 | |||
| e623bf7fde | |||
| 6fc70b8656 | |||
| 354cefb9f4 | |||
| a78aa88dbc | |||
| 9ac2453676 | |||
| bb70800b4e | |||
| 855272a558 | |||
| ebb2c5f791 | |||
| 2e466bb164 | |||
| 95ebe0f087 | |||
| 0a6aa43b07 | |||
| 806a8cf659 | |||
| 1a32fbfeec | |||
| 67396c16dd | |||
| b0684b6f1b | |||
| 661778c02c | |||
| 5c4241aefe | |||
| 3f6bc90824 | |||
| 4ade6e04a8 | |||
| 49d0835236 | |||
| d90bd92bcc | |||
| 41c016b8c7 | |||
| 5b4d3f71f9 | |||
| 256a9322ef | |||
| 793f82e445 | |||
| ab6da3914b | |||
| 0b53f0ebf3 | |||
| 76d668514e | |||
| 3c347bef7d | |||
| e837e5f780 | |||
| 26348ccc74 | |||
| 729a756e21 | |||
| 4dbddcf179 | |||
| f2fff34d4d | |||
| 59c5e2c1d9 | |||
| 067006f406 | |||
| 93d82b973e | |||
| a9a3423b58 | |||
| f4ee215ad8 | |||
| 48431b1c35 | |||
| ce961f90ba | |||
| 916d2f6bb3 | |||
| 01e7098f00 | |||
| e02fbac4cd | |||
| a8fce32e70 | |||
| d0637c1e3d | |||
| f6702d299d | |||
| 033b7ece28 | |||
| 5f5dce6d53 | |||
| 82c5c7518b | |||
| 7a60ffb3c4 | |||
| 2795f657b5 | |||
| 9ef5b5830e | |||
| 879adfa633 | |||
| b12a344776 | |||
| 50b1098797 | |||
| fdfaa7eba4 | |||
| 5525587513 | |||
| 1f20ed7640 | |||
| f741064843 | |||
| d5138e4c0a | |||
| 42a30c33db | |||
| e5d978f8e8 | |||
| ccc82520a9 | |||
| 22acf52a26 | |||
| 2ccd2786f4 | |||
| 0028136935 | |||
| 0edc46b771 | |||
| 2261f3d1c3 | |||
| 5c0e792782 | |||
| 644882e04f | |||
| 67f51c6de9 | |||
| 0c8fd6ab0e | |||
| 5452a57a14 | |||
| 19f020e7a6 | |||
| 825641f2a9 | |||
| 35ab4cb2fe | |||
| fd13607d89 | |||
| f79b4d44b9 | |||
| 91e30a6e84 | |||
| 8ab0f164a6 | |||
| 578bb03404 | |||
| 06582b5371 | |||
| 7f6baf35b7 | |||
| 6227d0baa1 | |||
| e334b585be | |||
| c83b3f19f7 | |||
| d31ec055f9 | |||
| 38c259a45e | |||
| b2ee24de98 | |||
| 9ba0e52bb7 | |||
| edc712e6f6 | |||
| 485888b2d9 | |||
| c2e90d4d83 | |||
| f5d89b8f52 | |||
| 378b40790a | |||
| be3d38392d | |||
| 27fef50983 | |||
| 167df85c1e | |||
| 009e16c9a4 | |||
| b40cc767b2 | |||
| 4f5f2d32be | |||
| 66be3e0281 | |||
| 987f188f00 | |||
| daca2bdf2a | |||
| 8894f52439 | |||
| 863f81e55a | |||
| d03d3735e5 | |||
| 3bb2df6e12 | |||
| 80c9efc618 | |||
| 3279901ab0 | |||
| d43d351721 | |||
| 8210eba439 | |||
| cbd7294b0b | |||
| 6064e8af87 | |||
| 8754f0c25f | |||
| f31700f668 | |||
| 9877b139f6 | |||
| 5643c846ee | |||
| 5a071babe9 | |||
| 3d85d0bce6 | |||
| 68afc2c718 | |||
| b3d9323f66 | |||
| effc63755b | |||
| b90934a72a | |||
| e01748eb2f | |||
| 430fbf5e46 | |||
| 27e6b9ce0d | |||
| fc614b9833 | |||
| a97c102369 | |||
| 745a491f90 | |||
| b2880ab0a9 | |||
| f916454c55 | |||
| 701b8ea12e | |||
| 2079942ccd | |||
| 140b718592 | |||
| 2de8c72131 | |||
| 089d4b5cee | |||
| e06a015d6e | |||
| b7e546f2f5 | |||
| 26ef275ab4 | |||
| 416db7c981 | |||
| 78079b2e60 | |||
| 03bffb725a | |||
| 46fc89e247 | |||
| fbbceaa642 | |||
| f5aae25cc8 | |||
| 8d03943acb | |||
| 853513b926 | |||
| c606a41314 | |||
| 35f29ca22b | |||
| ac00f3ebe7 | |||
| 6846de98f8 | |||
| 881baa818d | |||
| b671145e73 | |||
| 8809c7b900 | |||
| 9b6fa8fe8c | |||
| d6c02ebb2c |
@@ -9,6 +9,12 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
|
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "### Join the [discord server](https://discord.gg/pJsjuNCKRq) for questions or if you are not sure about a bug."
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant."
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: what-happened
|
id: what-happened
|
||||||
attributes:
|
attributes:
|
||||||
@@ -27,6 +33,7 @@ body:
|
|||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Audiobookshelf version
|
label: Audiobookshelf version
|
||||||
|
description: Do not put 'Latest version', please put the actual version here
|
||||||
placeholder: "e.g. v1.6.60"
|
placeholder: "e.g. v1.6.60"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@ test/
|
|||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
library/
|
||||||
|
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
|
|||||||
+1
-1
@@ -25,5 +25,5 @@ HEALTHCHECK \
|
|||||||
--interval=30s \
|
--interval=30s \
|
||||||
--timeout=3s \
|
--timeout=3s \
|
||||||
--start-period=10s \
|
--start-period=10s \
|
||||||
CMD curl -f http://127.0.0.1/ping || exit 1
|
CMD curl -f http://127.0.0.1/healthcheck || exit 1
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|||||||
@@ -225,4 +225,18 @@ Bookshelf Label
|
|||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
/* number of lines to show */
|
/* number of lines to show */
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
||||||
|
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
||||||
|
padding-top: 104px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-bar .Vue-Toastification__container.top-right {
|
||||||
|
padding-top: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-bars .Vue-Toastification__container.top-right {
|
||||||
|
padding-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -1,34 +1,40 @@
|
|||||||
.flip-list-move {
|
.flip-list-move {
|
||||||
transition: transform 0.5s;
|
transition: transform 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-move {
|
.no-move {
|
||||||
transition: transform 0s;
|
transition: transform 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background-color: rgba(255, 255, 255, 0.25);
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group {
|
.list-group {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
#librariesTable .item {
|
|
||||||
cursor: n-resize;
|
|
||||||
}
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: n-resize;
|
cursor: n-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:not(.exclude) {
|
.list-group-item:not(.exclude) {
|
||||||
cursor: n-resize;
|
cursor: n-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item.exclude {
|
.list-group-item.exclude {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:not(.ghost):not(.exclude):hover {
|
.list-group-item:not(.ghost):not(.exclude):hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
@@ -36,6 +42,7 @@
|
|||||||
.list-group-item.exclude:not(.ghost) {
|
.list-group-item.exclude:not(.ghost) {
|
||||||
background-color: rgba(255, 0, 0, 0.25);
|
background-color: rgba(255, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item.exclude:not(.ghost):hover {
|
.list-group-item.exclude:not(.ghost):hover {
|
||||||
background-color: rgba(223, 0, 0, 0.25);
|
background-color: rgba(223, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
+26
-26
@@ -74,7 +74,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+1F00-1FFF;
|
unicode-range: U+1F00-1FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+0370-03FF;
|
unicode-range: U+0370-03FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+1F00-1FFF;
|
unicode-range: U+1F00-1FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0370-03FF;
|
unicode-range: U+0370-03FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+1F00-1FFF;
|
unicode-range: U+1F00-1FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+0370-03FF;
|
unicode-range: U+0370-03FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,7 +284,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +294,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+1F00-1FFF;
|
unicode-range: U+1F00-1FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0370-03FF;
|
unicode-range: U+0370-03FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +334,6 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
@@ -3,16 +3,16 @@
|
|||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="/icon48.png" class="w-10 h-10 md:w-12 md:h-12 mr-4" />
|
<img src="/icon.svg" class="w-10 min-w-10 h-10 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf</h1>
|
<h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf</h1>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<ui-libraries-dropdown />
|
<ui-libraries-dropdown class="mr-2" />
|
||||||
|
|
||||||
<controls-global-search v-if="currentLibrary" class="hidden md:block" />
|
<controls-global-search v-if="currentLibrary" class="" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
<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-warning text-opacity-50"> cast </span>
|
<span class="material-icons-outlined text-warning text-opacity-50"> cast </span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<div v-if="isChromecastInitialized" class="w-6 h-6 mr-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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
|
<span class="material-icons" aria-label="User Stats" role="button">equalizer</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 22px">
|
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
||||||
<p class="transform text-sm">{{ shelf.label }}</p>
|
<p class="transform text-sm">{{ shelf.label }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,19 @@
|
|||||||
<div class="w-full h-20 md:h-10 relative">
|
<div class="w-full h-20 md:h-10 relative">
|
||||||
<div class="flex md:hidden h-10 items-center">
|
<div class="flex md:hidden h-10 items-center">
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link :to="`/library/${currentLibraryId}`" class="flex-grow h-full flex justify-center items-center" :class="homePage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p>Home</p>
|
<p class="text-sm">Home</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="flex-grow h-full flex justify-center items-center" :class="showLibrary ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p>Library</p>
|
<p class="text-sm">Library</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p>Series</p>
|
<p class="text-sm">Series</p>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
|
<p class="text-sm">Collections</p>
|
||||||
|
</nuxt-link>
|
||||||
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
|
<p class="text-sm">Search</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-30 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
@@ -98,6 +104,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
userCanDelete() {
|
userCanDelete() {
|
||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
},
|
},
|
||||||
@@ -129,6 +138,12 @@ export default {
|
|||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
currentLibraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
|
isPodcastLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
|
},
|
||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
@@ -156,6 +171,9 @@ export default {
|
|||||||
},
|
},
|
||||||
isIssuesFilter() {
|
isIssuesFilter() {
|
||||||
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
||||||
|
},
|
||||||
|
isPodcastSearchPage() {
|
||||||
|
return this.$route.name === 'library-library-podcast-search'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -4,19 +4,21 @@
|
|||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-opacity-0 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-4 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'">
|
||||||
<p>{{ route.title }}</p>
|
<p>{{ route.title }}</p>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
|
<div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<p class="font-mono text-sm">v{{ $config.version }}</p>
|
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
|
||||||
|
|
||||||
<p class="font-mono text-xs text-gray-300 italic">{{ Source }}</p>
|
<p class="font-mono 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: {{ latestVersion }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -26,7 +28,9 @@ export default {
|
|||||||
isOpen: Boolean
|
isOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
showChangelogModal: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
Source() {
|
Source() {
|
||||||
@@ -66,7 +70,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'config-sessions',
|
id: 'config-sessions',
|
||||||
title: 'Sessions',
|
title: 'Listening Sessions',
|
||||||
path: '/config/sessions'
|
path: '/config/sessions'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -76,7 +80,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'config-log',
|
id: 'config-log',
|
||||||
title: 'Log',
|
title: 'Logs',
|
||||||
path: '/config/log'
|
path: '/config/log'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -129,18 +133,24 @@ export default {
|
|||||||
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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickChangelog(){
|
||||||
|
this.showChangelogModal = true
|
||||||
|
},
|
||||||
clickOutside() {
|
clickOutside() {
|
||||||
if (!this.isOpen) return
|
if (!this.isOpen) return
|
||||||
this.closeDrawer()
|
this.closeDrawer()
|
||||||
},
|
},
|
||||||
closeDrawer() {
|
closeDrawer() {
|
||||||
this.$emit('update:isOpen', false)
|
this.$emit('update:isOpen', false)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -88,6 +88,7 @@ export default {
|
|||||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
if (this.page === 'collections') return "You haven't made any collections yet"
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
if (this.filterName === 'Issues') return 'No Issues'
|
if (this.filterName === 'Issues') return 'No Issues'
|
||||||
|
else if (this.filterName === 'Feed-open') return 'No RSS feeds are open'
|
||||||
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
||||||
}
|
}
|
||||||
return 'No results'
|
return 'No results'
|
||||||
|
|||||||
@@ -75,17 +75,21 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }">
|
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }">
|
||||||
<p class="font-mono text-xs text-center text-gray-300 leading-3 mb-1">v{{ $config.version }}</p>
|
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
||||||
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
showChangelogModal: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
Source() {
|
Source() {
|
||||||
@@ -150,17 +154,21 @@ 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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
clickChangelog(){
|
||||||
|
this.showChangelogModal = true
|
||||||
|
}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-24'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-base sm:text-lg">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||||
<span class="material-icons text-sm">person</span>
|
<span class="material-icons text-sm">person</span>
|
||||||
<p v-if="podcastAuthor">{{ podcastAuthor }}</p>
|
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
||||||
<p v-else-if="authors.length" class="pl-1.5 text-sm sm:text-base">
|
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="text-sm sm:text-base cursor-pointer pl-2">Unknown</p>
|
<p v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">Unknown</p>
|
||||||
</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-icons text-xs">schedule</span>
|
||||||
<p class="font-mono text-sm pl-2 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" />
|
||||||
<span class="material-icons px-2 py-1 md:p-4 cursor-pointer" @click="closePlayer">close</span>
|
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
|
||||||
</div>
|
</div>
|
||||||
<player-ui
|
<player-ui
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
@@ -364,7 +364,11 @@ export default {
|
|||||||
var episodeId = payload.episodeId || null
|
var episodeId = payload.episodeId || null
|
||||||
|
|
||||||
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
||||||
this.playerHandler.play()
|
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
||||||
|
this.seek(payload.startTime)
|
||||||
|
} else {
|
||||||
|
this.playerHandler.play()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +381,11 @@ export default {
|
|||||||
libraryItem,
|
libraryItem,
|
||||||
episodeId
|
episodeId
|
||||||
})
|
})
|
||||||
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate)
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.audioPlayer) this.$refs.audioPlayer.checkUpdateChapterTrack()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.playerHandler.load(libraryItem, episodeId, true, this.initialPlaybackRate, payload.startTime)
|
||||||
},
|
},
|
||||||
pauseItem() {
|
pauseItem() {
|
||||||
this.playerHandler.pause()
|
this.playerHandler.pause()
|
||||||
@@ -389,11 +397,13 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
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('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)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
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('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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full border-b border-gray-700 pb-2">
|
<div v-if="book" class="w-full border-b border-gray-700 pb-2">
|
||||||
<div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch">
|
<div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch">
|
||||||
<div class="h-24 bg-primary" :style="{ minWidth: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="min-w-12 max-w-12 md:min-w-20 md:max-w-20">
|
||||||
<img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" />
|
<div class="w-full bg-primary">
|
||||||
</div>
|
<img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" />
|
||||||
<div v-if="!isPodcast" class="px-4 flex-grow">
|
</div>
|
||||||
<div class="flex items-center">
|
</div>
|
||||||
<h1>{{ book.title }}</h1>
|
<div v-if="!isPodcast" class="px-2 md:px-4 flex-grow">
|
||||||
<div class="flex-grow" />
|
<div class="flex items-center">
|
||||||
<p>{{ book.publishedYear }}</p>
|
<h1 class="text-sm md:text-base">{{ book.title }}</h1>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
|
||||||
|
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
|
||||||
|
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration) }}</p>
|
||||||
|
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
|
||||||
|
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
||||||
|
<p class="leading-3 text-xs text-gray-400">
|
||||||
|
{{ series.series }}<span v-if="series.volumeNumber"> #{{ series.volumeNumber }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-400">{{ book.author }}</p>
|
|
||||||
<div class="w-full max-h-12 overflow-hidden">
|
<div class="w-full max-h-12 overflow-hidden">
|
||||||
<p class="text-gray-500 text-xs">{{ book.description }}</p>
|
<p class="text-gray-500 text-xs">{{ book.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +29,7 @@
|
|||||||
<div v-else class="px-4 flex-grow">
|
<div v-else class="px-4 flex-grow">
|
||||||
<h1>{{ book.title }}</h1>
|
<h1>{{ book.title }}</h1>
|
||||||
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,7 +67,7 @@ export default {
|
|||||||
selectMatch() {
|
selectMatch() {
|
||||||
var book = { ...this.book }
|
var book = { ...this.book }
|
||||||
book.cover = this.selectedCover
|
book.cover = this.selectedCover
|
||||||
this.$emit('select', this.book)
|
this.$emit('select', book)
|
||||||
},
|
},
|
||||||
clickCover(cover) {
|
clickCover(cover) {
|
||||||
this.selectedCover = cover
|
this.selectedCover = cover
|
||||||
|
|||||||
@@ -62,21 +62,10 @@ export default {
|
|||||||
matchHtml() {
|
matchHtml() {
|
||||||
if (!this.matchText || !this.search) return ''
|
if (!this.matchText || !this.search) return ''
|
||||||
if (this.matchKey === 'subtitle') return ''
|
if (this.matchKey === 'subtitle') return ''
|
||||||
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
|
|
||||||
if (matchSplit.length < 2) return ''
|
|
||||||
|
|
||||||
var html = ''
|
// This used to highlight the part of the search found
|
||||||
var totalLenSoFar = 0
|
// but with removing commas periods etc this is no longer plausible
|
||||||
for (let i = 0; i < matchSplit.length - 1; i++) {
|
const html = this.matchText
|
||||||
var indexOf = matchSplit[i].length
|
|
||||||
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
|
|
||||||
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
|
|
||||||
totalLenSoFar += indexOf + this.search.length
|
|
||||||
|
|
||||||
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
|
|
||||||
}
|
|
||||||
var lastPart = this.matchText.substr(totalLenSoFar)
|
|
||||||
html += lastPart
|
|
||||||
|
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
||||||
if (this.matchKey === 'authors') return `by ${html}`
|
if (this.matchKey === 'authors') return `by ${html}`
|
||||||
|
|||||||
@@ -78,6 +78,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
|
||||||
|
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
||||||
@@ -144,6 +148,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
dateFormat() {
|
||||||
|
return this.store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.store.state.showExperimentalFeatures
|
return this.store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
@@ -245,13 +252,15 @@ export default {
|
|||||||
return this.mediaMetadata.authorNameLF
|
return this.mediaMetadata.authorNameLF
|
||||||
},
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix) {
|
if (this.recentEpisode) return this.recentEpisode.title
|
||||||
return this.mediaMetadata.titleIgnorePrefix
|
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
||||||
}
|
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
||||||
return this.title
|
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title
|
||||||
},
|
},
|
||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
|
if (this.collapsedSeries) return ''
|
||||||
if (this.isAuthorBookshelfView) {
|
if (this.isAuthorBookshelfView) {
|
||||||
return this.mediaMetadata.publishedYear || ''
|
return this.mediaMetadata.publishedYear || ''
|
||||||
}
|
}
|
||||||
@@ -259,9 +268,10 @@ export default {
|
|||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
if (this.collapsedSeries) return null
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
||||||
|
if (this.orderBy === 'addedAt') return 'Added ' + 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 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||||
@@ -438,6 +448,10 @@ export default {
|
|||||||
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
|
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
|
||||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||||
return 4.25 * this.sizeMultiplier
|
return 4.25 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
rssFeed() {
|
||||||
|
if (this.booksInSeries) return null
|
||||||
|
return this.store.getters['feeds/getFeedForItem'](this.libraryItemId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -494,7 +508,21 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$emit('edit', this.libraryItem)
|
this.$emit('edit', this.libraryItem)
|
||||||
},
|
},
|
||||||
toggleFinished() {
|
toggleFinished(confirmed = false) {
|
||||||
|
if (!this.itemIsFinished && this.userProgressPercent > 0 && !confirmed) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to mark "${this.displayTitle}" as finished?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.toggleFinished(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
isFinished: !this.itemIsFinished
|
isFinished: !this.itemIsFinished
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden z-0">
|
||||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" />
|
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="displayTitle" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
||||||
@@ -10,16 +10,16 @@
|
|||||||
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,7 +39,8 @@ export default {
|
|||||||
seriesMount: {
|
seriesMount: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
}
|
},
|
||||||
|
sortingIgnorePrefix: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -65,6 +66,13 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.series ? this.series.name : ''
|
return this.series ? this.series.name : ''
|
||||||
},
|
},
|
||||||
|
nameIgnorePrefix() {
|
||||||
|
return this.series ? this.series.nameIgnorePrefix : ''
|
||||||
|
},
|
||||||
|
displayTitle() {
|
||||||
|
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
||||||
|
return this.title
|
||||||
|
},
|
||||||
books() {
|
books() {
|
||||||
return this.series ? this.series.books || [] : []
|
return this.series ? this.series.books || [] : []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ export default {
|
|||||||
text: 'Issues',
|
text: 'Issues',
|
||||||
value: 'issues',
|
value: 'issues',
|
||||||
sublist: false
|
sublist: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'RSS Feed Open',
|
||||||
|
value: 'feed-open',
|
||||||
|
sublist: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
podcastItems: [
|
podcastItems: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-80 ml-6 relative">
|
<div class="sm:w-80 w-full relative">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||||
</form>
|
</form>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
||||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full 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-40 sm:w-full 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">
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<li v-if="isTyping" class="py-2 px-2">
|
<li v-if="isTyping" class="py-2 px-2">
|
||||||
<p>Thinking...</p>
|
<p>Thinking...</p>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
||||||
<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">
|
<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" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
||||||
<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">
|
<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" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p>
|
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</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">
|
<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="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
||||||
<cards-author-search-card :author="item" />
|
<cards-author-search-card :author="item" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
||||||
<template v-for="item in seriesResults">
|
<template v-for="item in seriesResults">
|
||||||
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
<li :key="item.series.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}/series/${item.series.id}`">
|
<nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
|
||||||
<cards-series-search-card :series="item.series" :book-items="item.books" />
|
<cards-series-search-card :series="item.series" :book-items="item.books" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
|
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</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">
|
<li :key="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" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -97,6 +97,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickOption() {
|
||||||
|
this.clearResults()
|
||||||
|
},
|
||||||
submitSearch() {
|
submitSearch() {
|
||||||
if (!this.search) return
|
if (!this.search) return
|
||||||
var search = this.search
|
var search = this.search
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative ml-8" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||||
<span class="font-mono uppercase text-gray-200">{{ playbackRate.toFixed(1) }}<span class="text-lg">⨯</span></span>
|
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base sm:text-lg">⨯</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu" class="absolute -top-20 left-0 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" style="left: -92px">
|
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||||
<div class="absolute -bottom-2 left-0 right-0 w-full flex justify-center">
|
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||||
<div class="arrow-down" />
|
<div class="arrow-down" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center h-9 relative overflow-hidden rounded-lg" style="width: 220px">
|
<div class="flex items-center h-9 relative overflow-hidden rounded-lg" style="width: 220px">
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<div class="w-full py-1 px-4">
|
<div class="w-full py-1 px-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||||
<p class="px-2 text-3xl">{{ playbackRate }}<span class="text-2xl">⨯</span></p>
|
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">⨯</span></p>
|
||||||
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,9 @@ export default {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
currentPlaybackRate: 0,
|
currentPlaybackRate: 0,
|
||||||
MIN_SPEED: 0.5,
|
MIN_SPEED: 0.5,
|
||||||
MAX_SPEED: 3
|
MAX_SPEED: 3,
|
||||||
|
menuLeft: -92,
|
||||||
|
arrowLeft: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -80,8 +82,22 @@ export default {
|
|||||||
var newPlaybackRate = this.playbackRate - 0.1
|
var newPlaybackRate = this.playbackRate - 0.1
|
||||||
this.playbackRate = Number(newPlaybackRate.toFixed(1))
|
this.playbackRate = Number(newPlaybackRate.toFixed(1))
|
||||||
},
|
},
|
||||||
|
updateMenuPositions() {
|
||||||
|
if (!this.$refs.wrapper) return
|
||||||
|
const boundingBox = this.$refs.wrapper.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (boundingBox.left + 110 > window.innerWidth - 10) {
|
||||||
|
this.menuLeft = window.innerWidth - 230 - boundingBox.left
|
||||||
|
|
||||||
|
this.arrowLeft = Math.abs(this.menuLeft) - 92
|
||||||
|
} else {
|
||||||
|
this.menuLeft = -92
|
||||||
|
this.arrowLeft = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
setShowMenu(val) {
|
setShowMenu(val) {
|
||||||
if (val) {
|
if (val) {
|
||||||
|
this.updateMenuPositions()
|
||||||
this.currentPlaybackRate = this.playbackRate
|
this.currentPlaybackRate = this.playbackRate
|
||||||
} else if (this.currentPlaybackRate !== this.playbackRate) {
|
} else if (this.currentPlaybackRate !== this.playbackRate) {
|
||||||
this.$emit('change', this.playbackRate)
|
this.$emit('change', this.playbackRate)
|
||||||
|
|||||||
@@ -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">
|
||||||
<div class="cursor-pointer" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
<div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
||||||
<span class="material-icons text-3xl">{{ volumeIcon }}</span>
|
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export default {
|
|||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
resolution() {
|
||||||
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
<div class="relative rounded-sm" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative overflow-hidden">
|
||||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!imageFailed" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,6 +56,9 @@ export default {
|
|||||||
},
|
},
|
||||||
placeholderCoverPadding() {
|
placeholderCoverPadding() {
|
||||||
return 0.8 * this.sizeMultiplier
|
return 0.8 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
resolution() {
|
||||||
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
show: {
|
show: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
console.log('accoutn modal show change', newVal)
|
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
@@ -162,6 +161,9 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
|
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
|
||||||
},
|
},
|
||||||
@@ -250,6 +252,12 @@ export default {
|
|||||||
this.$toast.error(`Failed to update account: ${data.error}`)
|
this.$toast.error(`Failed to update account: ${data.error}`)
|
||||||
} else {
|
} else {
|
||||||
console.log('Account updated', data.user)
|
console.log('Account updated', data.user)
|
||||||
|
|
||||||
|
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
|
||||||
|
console.log('Current user token was updated')
|
||||||
|
this.$store.commit('user/setUserToken', data.user.token)
|
||||||
|
}
|
||||||
|
|
||||||
this.$toast.success('Account updated')
|
this.$toast.success('Account updated')
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
@@ -305,7 +313,6 @@ export default {
|
|||||||
|
|
||||||
this.isNew = !this.account
|
this.isNew = !this.account
|
||||||
if (this.account) {
|
if (this.account) {
|
||||||
console.log(this.account)
|
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
username: this.account.username,
|
username: this.account.username,
|
||||||
password: this.account.password,
|
password: this.account.password,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">Your Bookmarks</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<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="show" class="w-full h-full">
|
<div v-if="show" class="w-full h-full">
|
||||||
<template v-for="bookmark in bookmarks">
|
<template v-for="bookmark in bookmarks">
|
||||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
|
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
|
||||||
@@ -8,8 +13,8 @@
|
|||||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||||
<p class="text-xl">No Bookmarks</p>
|
<p class="text-xl">No Bookmarks</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-px bg-white bg-opacity-10" />
|
<div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
|
||||||
<form @submit.prevent="submitCreateBookmark">
|
<form v-if="!hideCreate" @submit.prevent="submitCreateBookmark">
|
||||||
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||||
<div class="w-16 max-w-16 text-center">
|
<div class="w-16 max-w-16 text-center">
|
||||||
<p class="text-sm font-mono text-gray-400">
|
<p class="text-sm font-mono text-gray-400">
|
||||||
@@ -39,7 +44,8 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
libraryItemId: String
|
libraryItemId: String,
|
||||||
|
hideCreate: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="chapters" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="chapters" :width="600" :height="'unset'">
|
||||||
<div 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-primary 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 <= 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 hover:bg-bg relative" :class="chap.id === currentChapterId ? 'bg-yellow-400 bg-opacity-10' : chap.end <= currentChapterStart ? 'bg-success bg-opacity-5' : 'bg-opacity-20'" @click="clickChapter(chap)">
|
||||||
{{ chap.title }}
|
<p class="chapter-title truncate text-sm md:text-base">
|
||||||
|
{{ chap.title }}
|
||||||
|
</p>
|
||||||
|
<span class="font-mono text-xxs sm:text-xs text-gray-400 pl-2 whitespace-nowrap">{{ $elapsedPrettyExtended(chap.end - chap.start) }}</span>
|
||||||
<span class="flex-grow" />
|
<span class="flex-grow" />
|
||||||
<span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span>
|
<span class="font-mono text-xs sm:text-sm text-gray-300">{{ $secondsToTimestamp(chap.start) }}</span>
|
||||||
|
|
||||||
<div v-show="chap.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
|
<div v-show="chap.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
@@ -70,4 +73,15 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#chapter-modal-wrapper .chapter-title {
|
||||||
|
max-width: calc(100% - 120px);
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
#chapter-modal-wrapper .chapter-title {
|
||||||
|
max-width: calc(100% - 150px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" :width="300" height="100%">
|
||||||
|
<template #outer>
|
||||||
|
<div v-if="title" class="absolute top-7 left-4 z-40" style="max-width: 80%">
|
||||||
|
<p class="text-white text-lg truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
|
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||||
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
|
<template v-for="item in items">
|
||||||
|
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
|
||||||
|
<div class="relative flex items-center px-3">
|
||||||
|
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
title: String,
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
selected: String // optional
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickedOption(action) {
|
||||||
|
this.$emit('action', action)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
<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: 51" @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: 51" @click="clickClose">
|
||||||
<div class="absolute top-5 right-5 h-12 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-4xl">close</span>
|
<span class="material-icons 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 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-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="!selectedSeries.id.startsWith('new')" label="Series Name" />
|
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" label="Series Name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-40 p-1">
|
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||||
<ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" />
|
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" label="Sequence" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
@@ -59,9 +59,26 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isNewSeries() {
|
||||||
|
if (!this.selectedSeries || !this.selectedSeries.id) return false
|
||||||
|
return this.selectedSeries.id.startsWith('new')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setInputFocus() {
|
||||||
|
if (this.isNewSeries) {
|
||||||
|
// Focus on series input if new series
|
||||||
|
if (this.$refs.newSeriesSelect) {
|
||||||
|
this.$refs.newSeriesSelect.setFocus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Focus on sequence input if existing series
|
||||||
|
if (this.$refs.sequenceInput) {
|
||||||
|
this.$refs.sequenceInput.setFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
if (this.$refs.newSeriesSelect) {
|
if (this.$refs.newSeriesSelect) {
|
||||||
this.$refs.newSeriesSelect.blur()
|
this.$refs.newSeriesSelect.blur()
|
||||||
@@ -89,15 +106,15 @@ export default {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.content.style.transform = 'scale(1)'
|
this.content.style.transform = 'scale(1)'
|
||||||
}, 10)
|
}, 10)
|
||||||
document.documentElement.classList.add('modal-open')
|
|
||||||
|
|
||||||
this.$store.commit('setInnerModalOpen', true)
|
this.$store.commit('setInnerModalOpen', true)
|
||||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
|
|
||||||
|
this.setInputFocus()
|
||||||
},
|
},
|
||||||
setHide() {
|
setHide() {
|
||||||
if (this.content) this.content.style.transform = 'scale(0)'
|
if (this.content) this.content.style.transform = 'scale(0)'
|
||||||
if (this.el) this.el.remove()
|
if (this.el) this.el.remove()
|
||||||
document.documentElement.classList.remove('modal-open')
|
|
||||||
|
|
||||||
this.$store.commit('setInnerModalOpen', false)
|
this.$store.commit('setInnerModalOpen', false)
|
||||||
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<p class="font-book text-3xl text-white truncate">Session {{ _session.id }}</p>
|
<p class="font-book text-3xl text-white truncate">Session {{ _session.id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-base text-gray-200">{{ _session.displayTitle }}</p>
|
<p class="text-base text-gray-200">{{ _session.displayTitle }}</p>
|
||||||
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p>
|
<p v-if="_session.displayAuthor" class="text-xs text-gray-400 px-4">by {{ _session.displayAuthor }}</p>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary items-center justify-center opacity-0 hidden" :class="`z-${zIndex} bg-opacity-${bgOpacity}`">
|
||||||
<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" />
|
||||||
|
|
||||||
<div class="absolute top-5 right-5 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="clickClose">
|
<div class="absolute top-3 right-3 landscape:top-2 landscape:right-2 md:portrait:top-5 md:portrait:right-5 lg:top-5 lg:right-5 h-8 w-8 landscape:h-8 landscape:w-8 md:portrait:h-12 md:portrait:w-12 lg:w-12 lg:h-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="clickClose">
|
||||||
<span class="material-icons text-4xl">close</span>
|
<span class="material-icons text-2xl landscape:text-2xl md:portrait:text-4xl lg:text-4xl">close</span>
|
||||||
</div>
|
</div>
|
||||||
<slot name="outer" />
|
<slot name="outer" />
|
||||||
<div ref="content" style="min-width: 400px; min-height: 200px" class="relative text-white" :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" :style="{ height: modalHeight, width: modalWidth, marginTop: contentMarginTop + 'px' }" @mousedown="mousedownModal" @mouseup="mouseupModal" v-click-outside="clickBg">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
<div v-if="processing" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-black bg-opacity-60 rounded-lg flex items-center justify-center">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
@@ -50,7 +50,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
el: null,
|
el: null,
|
||||||
content: null,
|
content: null,
|
||||||
preventClickoutside: false
|
preventClickoutside: false,
|
||||||
|
isShowingPrompt: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -93,7 +94,7 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
clickBg(ev) {
|
clickBg(ev) {
|
||||||
if (!this.show) return
|
if (!this.show || this.isShowingPrompt) return
|
||||||
if (this.preventClickoutside) {
|
if (this.preventClickoutside) {
|
||||||
this.preventClickoutside = false
|
this.preventClickoutside = false
|
||||||
return
|
return
|
||||||
@@ -147,8 +148,16 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
console.warn('Invalid modal init', this.name)
|
console.warn('Invalid modal init', this.name)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
showingPrompt(isShowing) {
|
||||||
|
this.isShowingPrompt = isShowing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
this.$eventBus.$on('showing-prompt', this.showingPrompt)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('showing-prompt', this.showingPrompt)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -25,6 +25,9 @@
|
|||||||
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" label="Photo Path/URL" />
|
||||||
|
</div>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
|
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
|
||||||
</div>
|
</div>
|
||||||
@@ -43,19 +46,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
// props: {
|
|
||||||
// value: Boolean,
|
|
||||||
// author: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => {}
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
authorCopy: {
|
authorCopy: {
|
||||||
name: '',
|
name: '',
|
||||||
asin: '',
|
asin: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
imagePath: ''
|
||||||
},
|
},
|
||||||
processing: false
|
processing: false
|
||||||
}
|
}
|
||||||
@@ -95,9 +92,10 @@ export default {
|
|||||||
this.authorCopy.name = this.author.name
|
this.authorCopy.name = this.author.name
|
||||||
this.authorCopy.asin = this.author.asin
|
this.authorCopy.asin = this.author.asin
|
||||||
this.authorCopy.description = this.author.description
|
this.authorCopy.description = this.author.description
|
||||||
|
this.authorCopy.imagePath = this.author.imagePath
|
||||||
},
|
},
|
||||||
async submitForm() {
|
async submitForm() {
|
||||||
var keysToCheck = ['name', 'asin', 'description']
|
var keysToCheck = ['name', 'asin', 'description', 'imagePath']
|
||||||
var updatePayload = {}
|
var updatePayload = {}
|
||||||
keysToCheck.forEach((key) => {
|
keysToCheck.forEach((key) => {
|
||||||
if (this.authorCopy[key] !== this.author[key]) {
|
if (this.authorCopy[key] !== this.author[key]) {
|
||||||
@@ -118,6 +116,9 @@ export default {
|
|||||||
if (result.updated) {
|
if (result.updated) {
|
||||||
this.$toast.success('Author updated')
|
this.$toast.success('Author updated')
|
||||||
this.show = false
|
this.show = false
|
||||||
|
} else if (result.merged) {
|
||||||
|
this.$toast.success('Author merged')
|
||||||
|
this.show = false
|
||||||
} else this.$toast.info('No updates were needed')
|
} else this.$toast.info('No updates were needed')
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
@@ -167,4 +168,4 @@ export default {
|
|||||||
mounted() {},
|
mounted() {},
|
||||||
beforeDestroy() {}
|
beforeDestroy() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center px-4 py-4 justify-start relative hover:bg-bg" :class="wrapperClass" @click="click" @mouseover="mouseover" @mouseleave="mouseleave">
|
<div class="flex items-center px-4 py-4 justify-start relative bg-primary hover:bg-opacity-25" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<!-- <span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-80'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span> -->
|
|
||||||
<div class="w-16 max-w-16 text-center">
|
<div class="w-16 max-w-16 text-center">
|
||||||
<p class="text-sm font-mono text-gray-400">
|
<p class="text-sm font-mono text-gray-400">
|
||||||
{{ this.$secondsToTimestamp(bookmark.time) }}
|
{{ this.$secondsToTimestamp(bookmark.time) }}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">Changelog</p>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
<p class="text-xl font-bold pb-4">Changelog v{{ currentVersionNumber }}</p>
|
||||||
|
<div class="custom-text" v-html="compiledMarkedown" />
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { marked } from '@/static/libs/marked/index.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
changelog: String,
|
||||||
|
currentVersion: String
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
compiledMarkedown() {
|
||||||
|
return marked.parse(this.changelog, { gfm: true, breaks: true })
|
||||||
|
},
|
||||||
|
currentVersionNumber() {
|
||||||
|
return this.currentVersion
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/*
|
||||||
|
1. we need to manually define styles to apply to the parsed markdown elements,
|
||||||
|
since we don't have access to the actual elements in this component
|
||||||
|
|
||||||
|
2. v-deep allows these to take effect on the content passed in to the v-html in the div above
|
||||||
|
*/
|
||||||
|
.custom-text ::v-deep > h2 {
|
||||||
|
@apply text-lg font-bold;
|
||||||
|
}
|
||||||
|
.custom-text ::v-deep > h3 {
|
||||||
|
@apply text-lg font-bold;
|
||||||
|
}
|
||||||
|
.custom-text ::v-deep > ul {
|
||||||
|
@apply list-disc list-inside pb-4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="75">
|
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
<p class="font-book text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in availableTabs">
|
<template v-for="tab in availableTabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,6 +30,8 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
libraryItem: null,
|
libraryItem: null,
|
||||||
|
availableHeight: 0,
|
||||||
|
marginTop: 0,
|
||||||
tabs: [
|
tabs: [
|
||||||
{
|
{
|
||||||
id: 'details',
|
id: 'details',
|
||||||
@@ -133,8 +135,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
height() {
|
height() {
|
||||||
var maxHeightAllowed = window.innerHeight - 150
|
return Math.min(this.availableHeight, 650)
|
||||||
return Math.min(maxHeightAllowed, 650)
|
|
||||||
},
|
},
|
||||||
tabName() {
|
tabName() {
|
||||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
@@ -189,7 +190,6 @@ export default {
|
|||||||
if (prevBook) {
|
if (prevBook) {
|
||||||
this.unregisterListeners()
|
this.unregisterListeners()
|
||||||
this.libraryItem = prevBook
|
this.libraryItem = prevBook
|
||||||
this.selectedTab = 'details'
|
|
||||||
this.$store.commit('setSelectedLibraryItem', prevBook)
|
this.$store.commit('setSelectedLibraryItem', prevBook)
|
||||||
this.$nextTick(this.registerListeners)
|
this.$nextTick(this.registerListeners)
|
||||||
} else {
|
} else {
|
||||||
@@ -209,7 +209,6 @@ export default {
|
|||||||
if (nextBook) {
|
if (nextBook) {
|
||||||
this.unregisterListeners()
|
this.unregisterListeners()
|
||||||
this.libraryItem = nextBook
|
this.libraryItem = nextBook
|
||||||
this.selectedTab = 'details'
|
|
||||||
this.$store.commit('setSelectedLibraryItem', nextBook)
|
this.$store.commit('setSelectedLibraryItem', nextBook)
|
||||||
this.$nextTick(this.registerListeners)
|
this.$nextTick(this.registerListeners)
|
||||||
} else {
|
} else {
|
||||||
@@ -246,15 +245,29 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
registerListeners() {
|
registerListeners() {
|
||||||
|
window.addEventListener('orientationchange', this.orientationChange)
|
||||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
this.$eventBus.$on(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated)
|
this.$eventBus.$on(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated)
|
||||||
},
|
},
|
||||||
unregisterListeners() {
|
unregisterListeners() {
|
||||||
|
window.removeEventListener('orientationchange', this.orientationChange)
|
||||||
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
this.$eventBus.$off('modal-hotkey', this.hotkey)
|
||||||
this.$eventBus.$off(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated)
|
this.$eventBus.$off(`${this.selectedLibraryItemId}_updated`, this.libraryItemUpdated)
|
||||||
|
},
|
||||||
|
orientationChange() {
|
||||||
|
setTimeout(this.setHeight, 50)
|
||||||
|
},
|
||||||
|
setHeight() {
|
||||||
|
const smAndBelow = window.innerWidth < 1024 && window.innerWidth > window.innerHeight
|
||||||
|
|
||||||
|
this.marginTop = smAndBelow ? 90 : 75
|
||||||
|
const heightModifier = smAndBelow ? 95 : 150
|
||||||
|
this.availableHeight = window.innerHeight - heightModifier
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {
|
||||||
|
this.setHeight()
|
||||||
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.unregisterListeners()
|
this.unregisterListeners()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||||
<div class="flex">
|
<div class="flex flex-wrap">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<!-- book cover overlay -->
|
<!-- book cover overlay -->
|
||||||
@@ -11,14 +11,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow pl-6 pr-2">
|
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div v-if="userCanUpload" class="w-40 pr-2 pt-4" style="min-width: 160px">
|
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
||||||
<ui-file-input ref="fileInput" @change="fileUploadSelected">Upload Cover</ui-file-input>
|
<ui-file-input ref="fileInput" @change="fileUploadSelected"><span class="hidden md:inline-block">Upload Cover</span><span class="material-icons inline-block md:!hidden">upload</span></ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||||
<ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" />
|
<ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" />
|
||||||
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-3 w-24">Update</ui-btn>
|
<ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-2 sm:ml-3 w-24">Update</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,9 +31,9 @@
|
|||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
<covers-preview-cover :src="`${cover.localPath}?token=${userToken}`" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
||||||
<p v-if="!coversFound.length">No Covers Found</p>
|
<p v-if="!coversFound.length">No Covers Found</p>
|
||||||
<template v-for="cover in coversFound">
|
<template v-for="cover in coversFound">
|
||||||
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||||
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,23 +5,24 @@
|
|||||||
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" />
|
<widgets-podcast-details-edit v-else ref="itemDetailsEdit" :library-item="libraryItem" @submit="saveAndClose" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
|
<div class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-white border-opacity-5'">
|
||||||
<div class="flex items-center px-4">
|
<div class="flex items-center px-4">
|
||||||
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
|
<ui-btn v-if="userCanDelete" color="error" type="button" class="h-8 hidden md:block" :padding-x="3" small @click.stop.prevent="removeItem">Remove</ui-btn>
|
||||||
|
<ui-icon-btn bg-color="error" icon="delete" class="md:hidden" :size="7" icon-font-size="1rem" @click.stop.prevent="removeItem" />
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
|
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-2 md:mr-4">
|
||||||
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
|
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-2 md:mr-4">
|
||||||
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-btn @click="save" class="mx-2">Save</ui-btn>
|
<ui-btn @click="save" class="mx-2 hidden md:block">Save</ui-btn>
|
||||||
|
|
||||||
<ui-btn @click="saveAndClose">Save & Close</ui-btn>
|
<ui-btn @click="saveAndClose">Save<span class="hidden md:inline-block"> & Close</span></ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
|
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
|
||||||
<!-- Merge to m4b -->
|
<!-- Merge to m4b -->
|
||||||
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
|
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
|
||||||
<div class="flex items-center">
|
<div class="flex flex-wrap items-center">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-lg">Make M4B Audiobook File <span class="text-error">*</span></p>
|
<p class="text-lg">Make M4B Audiobook File <span class="text-error">*</span></p>
|
||||||
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded metadata, cover image, and chapters. <br /><span class="text-warning">*</span> Does not delete existing audio files.</p>
|
<p class="max-w-sm text-sm pt-2 text-gray-300">Generate a .M4B audiobook file with embedded metadata, cover image, and chapters. <br /><span class="text-warning">*</span> Does not delete existing audio files.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div>
|
<div class="mt-2 md:mt-0">
|
||||||
<p v-if="abmergeStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
|
<p v-if="abmergeStatus === $constants.DownloadStatus.FAILED" class="text-error mb-2">Download Failed</p>
|
||||||
<p v-if="abmergeStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
|
<p v-if="abmergeStatus === $constants.DownloadStatus.READY" class="text-success mb-2">Download Ready!</p>
|
||||||
<p v-if="abmergeStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
|
<p v-if="abmergeStatus === $constants.DownloadStatus.EXPIRED" class="text-error mb-2">Download Expired</p>
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
<p v-if="showM4bDownload" class="text-left text-base mb-4 py-4">
|
<p v-if="showM4bDownload" class="text-left text-base mb-4 py-4">
|
||||||
<span class="text-error">* <strong>Experimental</strong></span
|
<span class="text-error">* <strong>Experimental</strong></span
|
||||||
> - M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 20 minutes.
|
> - M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 30 minutes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> -->
|
<!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> -->
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="match-wrapper" class="w-full h-full overflow-hidden px-4 py-6 relative">
|
<div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<div class="flex items-center justify-start -mx-1 h-20">
|
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
|
||||||
<div class="w-40 px-1">
|
<div class="w-36 px-1">
|
||||||
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
|
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-72 px-1">
|
<div class="flex-grow md:w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
|
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="provider != 'itunes'" class="w-72 px-1">
|
<div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
|
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
|
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
|
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
|
||||||
<p>No Results</p>
|
<p>No Results</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper">
|
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
|
||||||
<template v-for="(res, index) in searchResults">
|
<template v-for="(res, index) in searchResults">
|
||||||
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
||||||
</template>
|
</template>
|
||||||
@@ -299,7 +299,7 @@ export default {
|
|||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = searchQuery
|
this.lastSearch = searchQuery
|
||||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`).catch((error) => {
|
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 10000 }).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -363,23 +363,33 @@ export default {
|
|||||||
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
||||||
if (this.isPodcast) this.provider = 'itunes'
|
if (this.isPodcast) this.provider = 'itunes'
|
||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else this.provider = localStorage.getItem('book-provider') || 'google'
|
||||||
|
|
||||||
|
if (this.searchTitle) {
|
||||||
|
this.submitSearch()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
selectMatch(match) {
|
selectMatch(match) {
|
||||||
if (match && match.series) {
|
if (match) {
|
||||||
if (!match.series.length) {
|
if (match.series) {
|
||||||
delete match.series
|
if (!match.series.length) {
|
||||||
} else {
|
delete match.series
|
||||||
match.series = match.series.map((se) => {
|
} else {
|
||||||
return {
|
match.series = match.series.map((se) => {
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
return {
|
||||||
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
name: se.series,
|
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
||||||
sequence: se.volumeNumber || ''
|
name: se.series,
|
||||||
}
|
sequence: se.volumeNumber || ''
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match.genres && Array.isArray(match.genres)) {
|
||||||
|
match.genres = match.genres.join(',')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Select Match', match)
|
||||||
this.selectedMatch = match
|
this.selectedMatch = match
|
||||||
},
|
},
|
||||||
buildMatchUpdatePayload() {
|
buildMatchUpdatePayload() {
|
||||||
@@ -409,11 +419,12 @@ export default {
|
|||||||
|
|
||||||
updatePayload.metadata.series = seriesPayload
|
updatePayload.metadata.series = seriesPayload
|
||||||
} else if (key === 'author' && !this.isPodcast) {
|
} else if (key === 'author' && !this.isPodcast) {
|
||||||
if (!Array.isArray(this.selectedMatch[key])) {
|
var authors = this.selectedMatch[key]
|
||||||
this.selectedMatch[key] = this.selectedMatch[key].split(',').map((au) => au.trim())
|
if (!Array.isArray(authors)) {
|
||||||
|
authors = authors.split(',').map((au) => au.trim())
|
||||||
}
|
}
|
||||||
var authorPayload = []
|
var authorPayload = []
|
||||||
this.selectedMatch[key].forEach((authorName) =>
|
authors.forEach((authorName) =>
|
||||||
authorPayload.push({
|
authorPayload.push({
|
||||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||||
name: authorName
|
name: authorName
|
||||||
@@ -421,11 +432,11 @@ export default {
|
|||||||
)
|
)
|
||||||
updatePayload.metadata.authors = authorPayload
|
updatePayload.metadata.authors = authorPayload
|
||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrator') {
|
||||||
updatePayload.metadata.narrators = [this.selectedMatch[key]]
|
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'genres') {
|
} else if (key === 'genres') {
|
||||||
updatePayload.metadata.genres = this.selectedMatch[key].split(',')
|
updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
updatePayload.tags = this.selectedMatch[key].split(',')
|
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||||
} else if (key === 'itunesId') {
|
} else if (key === 'itunesId') {
|
||||||
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
|
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
|
||||||
} else {
|
} else {
|
||||||
@@ -441,6 +452,8 @@ export default {
|
|||||||
if (!Object.keys(updatePayload).length) {
|
if (!Object.keys(updatePayload).length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Match payload', updatePayload)
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
|
||||||
if (updatePayload.metadata.cover) {
|
if (updatePayload.metadata.cover) {
|
||||||
@@ -488,6 +501,11 @@ export default {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.matchListWrapper {
|
.matchListWrapper {
|
||||||
height: calc(100% - 80px);
|
height: calc(100% - 124px);
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.matchListWrapper {
|
||||||
|
height: calc(100% - 80px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,33 +5,14 @@
|
|||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<div class="flex flex-wrap">
|
<template v-for="tab in tabs">
|
||||||
<div class="w-1/5 p-1">
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 p-1">
|
|
||||||
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
<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>
|
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
<div class="w-1/5 p-1">
|
|
||||||
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
|
||||||
</div>
|
|
||||||
<div class="w-2/5 p-1">
|
|
||||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full p-1">
|
|
||||||
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full p-1">
|
|
||||||
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full p-1 default-style">
|
|
||||||
<ui-rich-text-editor v-if="show" label="Description" v-model="newEpisode.description" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end pt-4">
|
|
||||||
<ui-btn @click="submit">Submit</ui-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,25 +22,19 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newEpisode: {
|
selectedTab: 'details',
|
||||||
season: null,
|
tabs: [
|
||||||
episode: null,
|
{
|
||||||
episodeType: null,
|
id: 'details',
|
||||||
title: null,
|
title: 'Details',
|
||||||
subtitle: null,
|
component: 'modals-podcast-tabs-episode-details'
|
||||||
description: null,
|
},
|
||||||
pubDate: null,
|
{
|
||||||
publishedAt: null
|
id: 'match',
|
||||||
},
|
title: 'Match',
|
||||||
pubDateInput: null
|
component: 'modals-podcast-tabs-episode-match'
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
watch: {
|
|
||||||
episode: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newVal) {
|
|
||||||
if (newVal) this.init()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -77,67 +52,29 @@ export default {
|
|||||||
episode() {
|
episode() {
|
||||||
return this.$store.state.globals.selectedEpisode
|
return this.$store.state.globals.selectedEpisode
|
||||||
},
|
},
|
||||||
episodeId() {
|
|
||||||
return this.episode ? this.episode.id : null
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
if (!this.libraryItem) return ''
|
if (!this.libraryItem) return ''
|
||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
|
},
|
||||||
|
tabComponentName() {
|
||||||
|
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
|
return _tab ? _tab.component : ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updatePubDate(val) {
|
selectTab(tab) {
|
||||||
if (val) {
|
this.selectedTab = tab
|
||||||
this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx')
|
|
||||||
this.newEpisode.publishedAt = new Date(val).valueOf()
|
|
||||||
} else {
|
|
||||||
this.newEpisode.pubDate = null
|
|
||||||
this.newEpisode.publishedAt = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init() {
|
|
||||||
this.newEpisode.season = this.episode.season || ''
|
|
||||||
this.newEpisode.episode = this.episode.episode || ''
|
|
||||||
this.newEpisode.episodeType = this.episode.episodeType || ''
|
|
||||||
this.newEpisode.title = this.episode.title || ''
|
|
||||||
this.newEpisode.subtitle = this.episode.subtitle || ''
|
|
||||||
this.newEpisode.description = this.episode.description || ''
|
|
||||||
this.newEpisode.pubDate = this.episode.pubDate || ''
|
|
||||||
this.newEpisode.publishedAt = this.episode.publishedAt
|
|
||||||
|
|
||||||
this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null
|
|
||||||
},
|
|
||||||
getUpdatePayload() {
|
|
||||||
var updatePayload = {}
|
|
||||||
for (const key in this.newEpisode) {
|
|
||||||
if (this.newEpisode[key] != this.episode[key]) {
|
|
||||||
updatePayload[key] = this.newEpisode[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updatePayload
|
|
||||||
},
|
|
||||||
submit() {
|
|
||||||
const payload = this.getUpdatePayload()
|
|
||||||
if (!Object.keys(payload).length) {
|
|
||||||
return this.$toast.info('No updates were made')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processing = true
|
|
||||||
this.$axios
|
|
||||||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
|
||||||
.then(() => {
|
|
||||||
this.processing = false
|
|
||||||
this.$toast.success('Podcast episode updated')
|
|
||||||
this.show = false
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed update episode'
|
|
||||||
console.error('Failed update episode', error)
|
|
||||||
this.processing = false
|
|
||||||
this.$toast.error(errorMsg)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.tab.tab-selected {
|
||||||
|
height: 41px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
|
||||||
|
</div>
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
||||||
|
</div>
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
||||||
|
</div>
|
||||||
|
<div class="w-2/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full p-1">
|
||||||
|
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full p-1 default-style">
|
||||||
|
<ui-rich-text-editor label="Description" v-model="newEpisode.description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end pt-4">
|
||||||
|
<ui-btn @click="submit">Submit</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
episode: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newEpisode: {
|
||||||
|
season: null,
|
||||||
|
episode: null,
|
||||||
|
episodeType: null,
|
||||||
|
title: null,
|
||||||
|
subtitle: null,
|
||||||
|
description: null,
|
||||||
|
pubDate: null,
|
||||||
|
publishedAt: null
|
||||||
|
},
|
||||||
|
pubDateInput: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
episode: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isProcessing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode ? this.episode.id : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updatePubDate(val) {
|
||||||
|
if (val) {
|
||||||
|
this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx')
|
||||||
|
this.newEpisode.publishedAt = new Date(val).valueOf()
|
||||||
|
} else {
|
||||||
|
this.newEpisode.pubDate = null
|
||||||
|
this.newEpisode.publishedAt = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.newEpisode.season = this.episode.season || ''
|
||||||
|
this.newEpisode.episode = this.episode.episode || ''
|
||||||
|
this.newEpisode.episodeType = this.episode.episodeType || ''
|
||||||
|
this.newEpisode.title = this.episode.title || ''
|
||||||
|
this.newEpisode.subtitle = this.episode.subtitle || ''
|
||||||
|
this.newEpisode.description = this.episode.description || ''
|
||||||
|
this.newEpisode.pubDate = this.episode.pubDate || ''
|
||||||
|
this.newEpisode.publishedAt = this.episode.publishedAt
|
||||||
|
|
||||||
|
this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null
|
||||||
|
},
|
||||||
|
getUpdatePayload() {
|
||||||
|
var updatePayload = {}
|
||||||
|
for (const key in this.newEpisode) {
|
||||||
|
if (this.newEpisode[key] != this.episode[key]) {
|
||||||
|
updatePayload[key] = this.newEpisode[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatePayload
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
const payload = this.getUpdatePayload()
|
||||||
|
if (!Object.keys(payload).length) {
|
||||||
|
return this.$toast.info('No updates were made')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.success('Podcast episode updated')
|
||||||
|
this.$emit('close')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
|
||||||
|
console.error('Failed update episode', error)
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div style="min-height: 200px">
|
||||||
|
<template v-if="!podcastFeedUrl">
|
||||||
|
<div class="py-8">
|
||||||
|
<widgets-alert type="error">Podcast has no RSS feed url to use for matching</widgets-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="flex mb-2">
|
||||||
|
<ui-text-input-with-label v-model="episodeTitle" :disabled="isProcessing" label="Episode Title" class="pr-1" />
|
||||||
|
<ui-btn class="mt-5 ml-1" :loading="isProcessing" type="submit">Search</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div v-if="!isProcessing && searchedTitle && !episodesFound.length" class="w-full py-8">
|
||||||
|
<p class="text-center text-lg">No episode matches found</p>
|
||||||
|
</div>
|
||||||
|
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
|
||||||
|
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
||||||
|
<p class="break-words mb-1">{{ episode.title }}</p>
|
||||||
|
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
||||||
|
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
episode: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
episodeTitle: '',
|
||||||
|
searchedTitle: '',
|
||||||
|
episodesFound: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
episode: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isProcessing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode ? this.episode.id : null
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
podcastFeedUrl() {
|
||||||
|
return this.mediaMetadata.feedUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getUpdatePayload(episodeData) {
|
||||||
|
var updatePayload = {}
|
||||||
|
for (const key in episodeData) {
|
||||||
|
if (key === 'enclosure') {
|
||||||
|
if (!this.episode.enclosure || JSON.stringify(this.episode.enclosure) !== JSON.stringify(episodeData.enclosure)) {
|
||||||
|
updatePayload[key] = {
|
||||||
|
...episodeData.enclosure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (episodeData[key] != this.episode[key]) {
|
||||||
|
updatePayload[key] = episodeData[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatePayload
|
||||||
|
},
|
||||||
|
selectEpisode(episode) {
|
||||||
|
const episodeData = {
|
||||||
|
title: episode.title || '',
|
||||||
|
subtitle: episode.subtitle || '',
|
||||||
|
description: episode.description || '',
|
||||||
|
enclosure: episode.enclosure || null,
|
||||||
|
episode: episode.episode || '',
|
||||||
|
episodeType: episode.episodeType || '',
|
||||||
|
season: episode.season || '',
|
||||||
|
pubDate: episode.pubDate || '',
|
||||||
|
publishedAt: episode.publishedAt
|
||||||
|
}
|
||||||
|
const updatePayload = this.getUpdatePayload(episodeData)
|
||||||
|
if (!Object.keys(updatePayload).length) {
|
||||||
|
return this.$toast.info('No updates are necessary')
|
||||||
|
}
|
||||||
|
console.log('Episode update payload', updatePayload)
|
||||||
|
|
||||||
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.success('Podcast episode updated')
|
||||||
|
this.$emit('selectTab', 'details')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
|
||||||
|
console.error('Failed update episode', error)
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
if (!this.episodeTitle || !this.episodeTitle.length) {
|
||||||
|
this.$toast.error('Must enter an episode title')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.searchedTitle = this.episodeTitle
|
||||||
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`)
|
||||||
|
.then((results) => {
|
||||||
|
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
||||||
|
console.log('Episodes found', this.episodesFound)
|
||||||
|
this.isProcessing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to search for episode', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || 'Failed to search for episode')
|
||||||
|
this.isProcessing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.searchedTitle = null
|
||||||
|
this.episodesFound = []
|
||||||
|
this.episodeTitle = this.episode ? this.episode.title || '' : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -27,7 +27,6 @@
|
|||||||
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p>
|
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||||
<p class="text-xs text-gray-300">Note: RSS feed URLs are not authenticated</p>
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
||||||
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
|
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
|
||||||
|
|||||||
@@ -3,21 +3,21 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||||
<span class="material-icons text-3xl">first_page</span>
|
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
<span class="material-icons text-3xl">replay_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-icons text-3xl">forward_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
||||||
<span class="material-icons text-3xl">last_page</span>
|
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
|
||||||
</div>
|
</div>
|
||||||
<controls-playback-speed-control v-model="playbackRate" @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">
|
||||||
@@ -40,7 +40,16 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
|
playbackRateInput: {
|
||||||
|
get() {
|
||||||
|
return this.playbackRate
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:playbackRate', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
playPause() {
|
playPause() {
|
||||||
this.$emit('playPause')
|
this.$emit('playPause')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
||||||
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
|
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
|
||||||
</div>
|
</div>
|
||||||
<div ref="track" class="w-full h-2 relative overflow-hidden">
|
<div class="w-full h-2 relative overflow-hidden" :class="useChapterTrack ? 'opacity-0' : ''">
|
||||||
<template v-for="(tick, index) in chapterTicks">
|
<template v-for="(tick, index) in chapterTicks">
|
||||||
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
|
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
|
||||||
</template>
|
</template>
|
||||||
@@ -34,6 +34,10 @@ export default {
|
|||||||
chapters: {
|
chapters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
currentChapter: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -46,7 +50,8 @@ export default {
|
|||||||
trackOffsetLeft: 16, // Track is 16px from edge
|
trackOffsetLeft: 16, // Track is 16px from edge
|
||||||
playedTrackWidth: 0,
|
playedTrackWidth: 0,
|
||||||
readyTrackWidth: 0,
|
readyTrackWidth: 0,
|
||||||
bufferTrackWidth: 0
|
bufferTrackWidth: 0,
|
||||||
|
useChapterTrack: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -57,14 +62,30 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
|
currentChapterDuration() {
|
||||||
|
if (!this.currentChapter) return 0
|
||||||
|
return this.currentChapter.end - this.currentChapter.start
|
||||||
|
},
|
||||||
|
currentChapterStart() {
|
||||||
|
if (!this.currentChapter) return 0
|
||||||
|
return this.currentChapter.start
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setUseChapterTrack(useChapterTrack) {
|
||||||
|
this.useChapterTrack = useChapterTrack
|
||||||
|
this.updateBufferTrack()
|
||||||
|
this.updatePlayedTrackWidth()
|
||||||
|
},
|
||||||
clickTrack(e) {
|
clickTrack(e) {
|
||||||
if (this.loading) return
|
if (this.loading) return
|
||||||
|
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
var perc = offsetX / this.trackWidth
|
var perc = offsetX / this.trackWidth
|
||||||
var time = perc * this.duration
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
|
||||||
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
|
||||||
|
const time = baseTime + (perc * duration);
|
||||||
if (isNaN(time) || time === null) {
|
if (isNaN(time) || time === null) {
|
||||||
console.error('Invalid time', perc, time)
|
console.error('Invalid time', perc, time)
|
||||||
return
|
return
|
||||||
@@ -76,7 +97,10 @@ export default {
|
|||||||
this.updateBufferTrack()
|
this.updateBufferTrack()
|
||||||
},
|
},
|
||||||
updateBufferTrack() {
|
updateBufferTrack() {
|
||||||
var bufferlen = (this.bufferTime / this.duration) * this.trackWidth
|
const time = this.useChapterTrack ? Math.max(0, this.bufferTime - this.currentChapterStart) : this.bufferTime
|
||||||
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
|
|
||||||
|
var bufferlen = (time / duration) * this.trackWidth
|
||||||
bufferlen = Math.round(bufferlen)
|
bufferlen = Math.round(bufferlen)
|
||||||
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
||||||
if (this.$refs.bufferTrack) this.$refs.bufferTrack.style.width = bufferlen + 'px'
|
if (this.$refs.bufferTrack) this.$refs.bufferTrack.style.width = bufferlen + 'px'
|
||||||
@@ -97,8 +121,10 @@ export default {
|
|||||||
this.updatePlayedTrackWidth()
|
this.updatePlayedTrackWidth()
|
||||||
},
|
},
|
||||||
updatePlayedTrackWidth() {
|
updatePlayedTrackWidth() {
|
||||||
var perc = this.currentTime / this.duration
|
const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime
|
||||||
var ptWidth = Math.round(perc * this.trackWidth)
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
|
|
||||||
|
var ptWidth = Math.round((time / duration) * this.trackWidth)
|
||||||
if (this.playedTrackWidth === ptWidth) {
|
if (this.playedTrackWidth === ptWidth) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -116,9 +142,11 @@ export default {
|
|||||||
},
|
},
|
||||||
mousemoveTrack(e) {
|
mousemoveTrack(e) {
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
var time = (offsetX / this.trackWidth) * this.duration
|
|
||||||
|
|
||||||
console.log('Mousemove track', this.trackWidth, this.duration)
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
|
||||||
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
|
||||||
|
const progressTime = (offsetX / this.trackWidth) * duration;
|
||||||
|
const totalTime = baseTime + progressTime;
|
||||||
|
|
||||||
if (this.$refs.hoverTimestamp) {
|
if (this.$refs.hoverTimestamp) {
|
||||||
var width = this.$refs.hoverTimestamp.clientWidth
|
var width = this.$refs.hoverTimestamp.clientWidth
|
||||||
@@ -139,9 +167,9 @@ export default {
|
|||||||
this.$refs.hoverTimestampArrow.style.left = posLeft + 'px'
|
this.$refs.hoverTimestampArrow.style.left = posLeft + 'px'
|
||||||
}
|
}
|
||||||
if (this.$refs.hoverTimestampText) {
|
if (this.$refs.hoverTimestampText) {
|
||||||
var hoverText = this.$secondsToTimestamp(time)
|
var hoverText = this.$secondsToTimestamp(progressTime)
|
||||||
|
|
||||||
var chapter = this.chapters.find((chapter) => chapter.start <= time && time < chapter.end)
|
var chapter = this.chapters.find((chapter) => chapter.start <= totalTime && totalTime < chapter.end)
|
||||||
if (chapter && chapter.title) {
|
if (chapter && chapter.title) {
|
||||||
hoverText += ` - ${chapter.title}`
|
hoverText += ` - ${chapter.title}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<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 md:top-0 right-0 md:right-2 flex items-center h-full">
|
<div class="absolute -top-10 md: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-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||||
|
|
||||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||||
|
|
||||||
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
<div class="cursor-pointer 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" style="font-size: 1.7rem">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-icons text-2xl sm:text-2.5xl">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-icons 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-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isPodcast" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
<div v-if="!isPodcast" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
|
||||||
<span class="material-icons" style="font-size: 1.7rem">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
<span class="material-icons text-2xl sm:text-2.5xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="chapters.length" class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<div v-if="chapters.length" class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
<span class="material-icons text-3xl">format_list_bulleted</span>
|
<span class="material-icons text-2xl sm:text-3xl">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? 'Use full track' : 'Use chapter track'">
|
||||||
|
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
||||||
|
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate="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" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" @seek="seek" />
|
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" @seek="seek" />
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p ref="currentTimestamp" class="font-mono text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="text-sm text-gray-300 pt-0.5">{{ currentChapterName }}</p>
|
<p class="text-xs sm:text-sm text-gray-300 pt-0.5">
|
||||||
|
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span>
|
||||||
|
</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||||
@@ -66,7 +74,8 @@ export default {
|
|||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
showChaptersModal: false,
|
showChaptersModal: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0
|
duration: 0,
|
||||||
|
useChapterTrack: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -86,6 +95,10 @@ export default {
|
|||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
|
var currChapTime = this.currentTime - this.currentChapter.start
|
||||||
|
return (this.currentChapterDuration - currChapTime) / this.playbackRate
|
||||||
|
}
|
||||||
return (this.duration - this.currentTime) / this.playbackRate
|
return (this.duration - this.currentTime) / this.playbackRate
|
||||||
},
|
},
|
||||||
timeRemainingPretty() {
|
timeRemainingPretty() {
|
||||||
@@ -95,8 +108,11 @@ export default {
|
|||||||
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
||||||
},
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
if (!this.duration) return 0
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
return Math.round((100 * this.currentTime) / this.duration)
|
const time = this.useChapterTrack ? Math.max(this.currentTime - this.currentChapterStart) : this.currentTime
|
||||||
|
|
||||||
|
if (!duration) return 0
|
||||||
|
return Math.round((100 * time) / duration)
|
||||||
},
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||||
@@ -104,6 +120,14 @@ export default {
|
|||||||
currentChapterName() {
|
currentChapterName() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter ? this.currentChapter.title : ''
|
||||||
},
|
},
|
||||||
|
currentChapterDuration() {
|
||||||
|
if (!this.currentChapter) return 0
|
||||||
|
return this.currentChapter.end - this.currentChapter.start
|
||||||
|
},
|
||||||
|
currentChapterStart() {
|
||||||
|
if (!this.currentChapter) return 0
|
||||||
|
return this.currentChapter.start
|
||||||
|
},
|
||||||
isFullscreen() {
|
isFullscreen() {
|
||||||
return this.$store.state.playerIsFullscreen
|
return this.$store.state.playerIsFullscreen
|
||||||
},
|
},
|
||||||
@@ -192,6 +216,23 @@ export default {
|
|||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
this.showChaptersModal = false
|
this.showChaptersModal = false
|
||||||
},
|
},
|
||||||
|
setUseChapterTrack() {
|
||||||
|
var useChapterTrack = !this.useChapterTrack
|
||||||
|
this.useChapterTrack = useChapterTrack
|
||||||
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(useChapterTrack)
|
||||||
|
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { useChapterTrack }).catch((err) => {
|
||||||
|
console.error('Failed to update settings', err)
|
||||||
|
})
|
||||||
|
this.updateTimestamp()
|
||||||
|
},
|
||||||
|
checkUpdateChapterTrack() {
|
||||||
|
// Changing media in player may not have chapters
|
||||||
|
if (!this.chapters.length && this.useChapterTrack) {
|
||||||
|
this.useChapterTrack = false
|
||||||
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
|
}
|
||||||
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
this.$emit('seek', time)
|
this.$emit('seek', time)
|
||||||
},
|
},
|
||||||
@@ -239,10 +280,10 @@ export default {
|
|||||||
console.error('No timestamp el')
|
console.error('No timestamp el')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var currTimeClean = this.$secondsToTimestamp(this.currentTime)
|
const time = this.useChapterTrack ? Math.max(0, this.currentTime - this.currentChapterStart) : this.currentTime
|
||||||
|
var currTimeClean = this.$secondsToTimestamp(time)
|
||||||
ts.innerText = currTimeClean
|
ts.innerText = currTimeClean
|
||||||
},
|
},
|
||||||
|
|
||||||
setBufferTime(bufferTime) {
|
setBufferTime(bufferTime) {
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setBufferTime(bufferTime)
|
if (this.$refs.trackbar) this.$refs.trackbar.setBufferTime(bufferTime)
|
||||||
},
|
},
|
||||||
@@ -252,6 +293,11 @@ export default {
|
|||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
|
var _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)
|
||||||
this.$emit('setPlaybackRate', this.playbackRate)
|
this.$emit('setPlaybackRate', this.playbackRate)
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="modal modal-bg w-full h-full fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-60 opacity-0">
|
||||||
|
<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 ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
|
||||||
|
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
|
<p class="text-base mb-8 mt-2 px-1">{{ message }}</p>
|
||||||
|
<div class="flex px-1 items-center">
|
||||||
|
<ui-btn v-if="isYesNo" color="primary" @click="nevermind">Cancel</ui-btn>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn v-if="isYesNo" color="success" @click="confirm">Yes</ui-btn>
|
||||||
|
<ui-btn v-else color="primary" @click="confirm">Ok</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
el: null,
|
||||||
|
content: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.setShow()
|
||||||
|
} else {
|
||||||
|
this.setHide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.globals.showConfirmPrompt
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('globals/setShowConfirmPrompt', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmPromptOptions() {
|
||||||
|
return this.$store.state.globals.confirmPromptOptions || {}
|
||||||
|
},
|
||||||
|
message() {
|
||||||
|
return this.confirmPromptOptions.message || ''
|
||||||
|
},
|
||||||
|
callback() {
|
||||||
|
return this.confirmPromptOptions.callback
|
||||||
|
},
|
||||||
|
type() {
|
||||||
|
return this.confirmPromptOptions.type || 'ok'
|
||||||
|
},
|
||||||
|
persistent() {
|
||||||
|
return !!this.confirmPromptOptions.persistent
|
||||||
|
},
|
||||||
|
isYesNo() {
|
||||||
|
return this.type === 'yesNo'
|
||||||
|
},
|
||||||
|
modalHeight() {
|
||||||
|
return 'unset'
|
||||||
|
},
|
||||||
|
modalWidth() {
|
||||||
|
return '500px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickedOutside(evt) {
|
||||||
|
if (!this.show) return
|
||||||
|
if (evt) {
|
||||||
|
evt.stopPropagation()
|
||||||
|
evt.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.persistent) return
|
||||||
|
if (this.callback) this.callback(false)
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
nevermind() {
|
||||||
|
if (this.callback) this.callback(false)
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
if (this.callback) this.callback(true)
|
||||||
|
this.show = false
|
||||||
|
},
|
||||||
|
setShow() {
|
||||||
|
this.$eventBus.$emit('showing-prompt', true)
|
||||||
|
document.body.appendChild(this.el)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.content.style.transform = 'scale(1)'
|
||||||
|
}, 10)
|
||||||
|
},
|
||||||
|
setHide() {
|
||||||
|
this.$eventBus.$emit('showing-prompt', false)
|
||||||
|
this.content.style.transform = 'scale(0)'
|
||||||
|
this.el.remove()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.el = this.$refs.wrapper
|
||||||
|
this.content = this.$refs.content
|
||||||
|
this.content.style.transform = 'scale(0)'
|
||||||
|
this.content.style.transition = 'transform 0.25s cubic-bezier(0.16, 1, 0.3, 1)'
|
||||||
|
this.el.style.opacity = 1
|
||||||
|
this.el.remove()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.show) {
|
||||||
|
this.$eventBus.$emit('showing-prompt', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -65,12 +65,10 @@ export default {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.content.style.transform = 'scale(1)'
|
this.content.style.transform = 'scale(1)'
|
||||||
}, 10)
|
}, 10)
|
||||||
document.documentElement.classList.add('modal-open')
|
|
||||||
},
|
},
|
||||||
setHide() {
|
setHide() {
|
||||||
this.content.style.transform = 'scale(0)'
|
this.content.style.transform = 'scale(0)'
|
||||||
this.el.remove()
|
this.el.remove()
|
||||||
document.documentElement.classList.remove('modal-open')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div id="heatmap" class="w-full">
|
<div id="heatmap" class="w-full">
|
||||||
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
|
<div class="mx-auto" :style="{ height: innerHeight + 160 + 'px', width: innerWidth + 52 + 'px' }" style="background-color: rgba(13, 17, 23, 0)">
|
||||||
<p class="mb-2 px-1 text-sm text-gray-200">{{ Object.values(daysListening).length }} listening sessions in the last year</p>
|
<p class="mb-2 px-1 text-sm text-gray-200">{{ Object.values(daysListening).length }} listening sessions in the last year</p>
|
||||||
<div class="border border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
|
<div class="border border-white border-opacity-25 rounded py-2 w-full" style="background-color: #232323" :style="{ height: innerHeight + 80 + 'px' }">
|
||||||
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
|
<div :style="{ width: innerWidth + 'px', height: innerHeight + 'px' }" class="ml-10 mt-5 absolute" @mouseover="mouseover" @mouseout="mouseout">
|
||||||
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
|
<div v-for="dayLabel in dayLabels" :key="dayLabel.label" :style="dayLabel.style" class="absolute top-0 left-0 text-gray-300">{{ dayLabel.label }}</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@
|
|||||||
<td class="font-book">
|
<td class="font-book">
|
||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-center">
|
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
|
||||||
{{ $secondsToTimestamp(chapter.start) }}
|
{{ $secondsToTimestamp(chapter.start) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-center">
|
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
|
||||||
{{ $secondsToTimestamp(chapter.end) }}
|
{{ $secondsToTimestamp(chapter.end) }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -57,6 +57,9 @@ export default {
|
|||||||
media() {
|
media() {
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
},
|
},
|
||||||
|
metadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
chapters() {
|
chapters() {
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
@@ -67,6 +70,30 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
clickBar() {
|
clickBar() {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
|
},
|
||||||
|
goToTimestamp(time) {
|
||||||
|
if (this.$store.getters['getIsMediaStreaming'](this.libraryItemId)) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
episodeId: null,
|
||||||
|
startTime: time
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const payload = {
|
||||||
|
message: `Start playback for "${this.metadata.title}" at ${this.$secondsToTimestamp(time)}?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
episodeId: null,
|
||||||
|
startTime: time
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full bg-primary bg-opacity-40">
|
<div class="w-full bg-primary bg-opacity-40">
|
||||||
<div class="w-full h-14 flex items-center px-4 bg-primary">
|
<div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
|
||||||
<p>Collection List</p>
|
<p class="pr-4">Collection List</p>
|
||||||
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
|
|
||||||
<p class="font-mono text-sm">{{ books.length }}</p>
|
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<!-- <p v-if="totalDuration">{{ totalDurationPretty }}</p> -->
|
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
<transition-group type="transition" :name="!drag ? 'collection-book' : null">
|
<transition-group type="transition" :name="!drag ? 'collection-book' : null">
|
||||||
@@ -56,6 +57,16 @@ export default {
|
|||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||||
|
},
|
||||||
|
totalDuration() {
|
||||||
|
var _total = 0
|
||||||
|
this.books.forEach((book) => {
|
||||||
|
_total += book.media.duration
|
||||||
|
})
|
||||||
|
return _total
|
||||||
|
},
|
||||||
|
totalDurationPretty() {
|
||||||
|
return this.$elapsedPrettyExtended(this.totalDuration)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -192,7 +192,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#accounts tr:nth-child(even) {
|
#accounts tr:nth-child(even) {
|
||||||
background-color: #3a3a3a;
|
background-color: #373838;
|
||||||
|
}
|
||||||
|
|
||||||
|
#accounts tr:nth-child(odd) {
|
||||||
|
background-color: #2f2f2f;
|
||||||
}
|
}
|
||||||
|
|
||||||
#accounts tr:hover {
|
#accounts tr:hover {
|
||||||
@@ -204,6 +208,6 @@ export default {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
background-color: #333;
|
background-color: #272727
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||||
<div v-if="book" class="flex h-20">
|
<div v-if="book" class="flex h-16 md:h-20">
|
||||||
<div class="w-16 max-w-16 h-full">
|
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<span class="material-icons drag-handle text-xl">menu</span>
|
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full relative" :style="{ width: coverWidth + 'px' }">
|
<div class="h-full relative" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
@@ -107,9 +107,12 @@ export default {
|
|||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||||
},
|
},
|
||||||
|
coverSize() {
|
||||||
|
return this.$store.state.globals.isMobile ? 40 : 50
|
||||||
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.6
|
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||||
return 50
|
return this.coverSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraryCopies">
|
<template v-for="library in libraryCopies">
|
||||||
<div :key="library.id" class="item">
|
<div :key="library.id" class="item">
|
||||||
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|||||||
@@ -7,18 +7,26 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-show="isHovering && !libraryScan" small color="success" @click.stop="scan">Scan</ui-btn>
|
<ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">Scan</ui-btn>
|
||||||
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2" @click.stop="forceScan">Force Re-Scan</ui-btn>
|
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">Force Re-Scan</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2" @click.stop="matchAll">Match Books</ui-btn>
|
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">Match Books</ui-btn>
|
||||||
|
|
||||||
<span v-show="isHovering && !libraryScan && showEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
<span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
||||||
<span v-show="!libraryScan && isHovering && showEdit && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50 cursor-pointer'" @click.stop="deleteClick">delete</span>
|
<span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span>
|
||||||
|
|
||||||
|
<!-- For mobile -->
|
||||||
|
<span v-if="!libraryScan" class="!block md:!hidden material-icons text-xl text-gray-300 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
||||||
|
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-2xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
|
||||||
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
||||||
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
||||||
|
|
||||||
|
<!-- For mobile -->
|
||||||
|
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -30,22 +38,19 @@ export default {
|
|||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
showEdit: Boolean,
|
|
||||||
dragging: Boolean
|
dragging: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
mouseover: false,
|
mouseover: false,
|
||||||
isDeleting: false
|
isDeleting: false,
|
||||||
|
showMobileMenu: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isHovering() {
|
isHovering() {
|
||||||
return this.mouseover && !this.dragging
|
return this.mouseover && !this.dragging
|
||||||
},
|
},
|
||||||
isMain() {
|
|
||||||
return this.library.id === 'main'
|
|
||||||
},
|
|
||||||
libraryScan() {
|
libraryScan() {
|
||||||
return this.$store.getters['scanners/getLibraryScan'](this.library.id)
|
return this.$store.getters['scanners/getLibraryScan'](this.library.id)
|
||||||
},
|
},
|
||||||
@@ -54,12 +59,53 @@ export default {
|
|||||||
},
|
},
|
||||||
isBookLibrary() {
|
isBookLibrary() {
|
||||||
return this.mediaType === 'book'
|
return this.mediaType === 'book'
|
||||||
|
},
|
||||||
|
menuTitle() {
|
||||||
|
return this.library.name
|
||||||
|
},
|
||||||
|
mobileMenuItems() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
text: 'Scan',
|
||||||
|
value: 'scan'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Force Re-Scan',
|
||||||
|
value: 'force-scan'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (this.isBookLibrary) {
|
||||||
|
items.push({
|
||||||
|
text: 'Match Books',
|
||||||
|
value: 'match-books'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
text: 'Delete',
|
||||||
|
value: 'delete'
|
||||||
|
})
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
mobileMenuAction(action) {
|
||||||
|
this.showMobileMenu = false
|
||||||
|
if (action === 'scan') {
|
||||||
|
this.scan()
|
||||||
|
} else if (action === 'force-scan') {
|
||||||
|
this.forceScan()
|
||||||
|
} else if (action === 'match-books') {
|
||||||
|
this.matchAll()
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
this.deleteClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMenu() {
|
||||||
|
this.showMobileMenu = true
|
||||||
|
},
|
||||||
matchAll() {
|
matchAll() {
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/libraries/${this.library.id}/matchall`)
|
.$get(`/api/libraries/${this.library.id}/matchall`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Starting scan for matches')
|
console.log('Starting scan for matches')
|
||||||
})
|
})
|
||||||
@@ -97,7 +143,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteClick() {
|
deleteClick() {
|
||||||
if (this.isMain) return
|
|
||||||
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
|
if (confirm(`Are you sure you want to permanently delete library "${this.library.name}"?`)) {
|
||||||
this.isDeleting = true
|
this.isDeleting = true
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export default {
|
|||||||
return this.$secondsToTimestamp(this.episode.duration)
|
return this.$secondsToTimestamp(this.episode.duration)
|
||||||
},
|
},
|
||||||
isStreaming() {
|
isStreaming() {
|
||||||
return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.episode.id)
|
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id)
|
||||||
},
|
},
|
||||||
streamIsPlaying() {
|
streamIsPlaying() {
|
||||||
return this.$store.state.streamIsPlaying && this.isStreaming
|
return this.$store.state.streamIsPlaying && this.isStreaming
|
||||||
@@ -124,7 +124,21 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleFinished() {
|
toggleFinished(confirmed = false) {
|
||||||
|
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to mark "${this.title}" as finished?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.toggleFinished(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
isFinished: !this.userIsFinished
|
isFinished: !this.userIsFinished
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
|
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
|
||||||
<ui-btn @click="clickUpload" color="primary" type="text"><slot /></ui-btn>
|
<ui-btn @click="clickUpload" color="primary" class="hidden md:block" type="text"><slot /></ui-btn>
|
||||||
|
<ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ export default {
|
|||||||
// this.currentSearch = this.textInput
|
// this.currentSearch = this.textInput
|
||||||
}, 100)
|
}, 100)
|
||||||
},
|
},
|
||||||
|
setFocus() {
|
||||||
|
if (this.$refs.input && this.editable) this.$refs.input.focus()
|
||||||
|
},
|
||||||
inputFocus() {
|
inputFocus() {
|
||||||
this.isFocused = true
|
this.isFocused = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative h-8 max-w-52 min-w-32" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="relative h-full w-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
|
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
<span class="block truncate text-sm">{{ currentLibrary.name }}</span>
|
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
||||||
</span>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||||
<template v-for="library in librariesFiltered">
|
<template v-for="library in librariesFiltered">
|
||||||
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
|
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-3">
|
<div class="flex items-center px-2">
|
||||||
<widgets-library-icon :icon="library.icon" class="mr-2" />
|
<widgets-library-icon :icon="library.icon" class="mr-1.5 text-gray-400" />
|
||||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center relative">
|
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||||
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
<span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
||||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ export default {
|
|||||||
blur() {
|
blur() {
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
},
|
},
|
||||||
|
setFocus() {
|
||||||
|
if (this.$refs.input) this.$refs.input.focus()
|
||||||
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
this.isHovering = true
|
this.isHovering = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setFocus() {
|
||||||
|
if (this.$refs.input && this.$refs.input.setFocus) {
|
||||||
|
this.$refs.input.setFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
blur() {
|
blur() {
|
||||||
if (this.$refs.input && this.$refs.input.blur) {
|
if (this.$refs.input && this.$refs.input.blur) {
|
||||||
this.$refs.input.blur()
|
this.$refs.input.blur()
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="rounded text-gray-200 border w-full px-3 py-2" :class="focusedDigit ? 'bg-primary bg-opacity-50 border-gray-300' : 'bg-primary border-gray-600'" @click="clickInput" v-click-outside="clickOutsideObj">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<template v-for="(digit, index) in digitDisplay">
|
||||||
|
<div v-if="digit == ':'" :key="index" class="px-px" @click.stop="clickMedian(index)">:</div>
|
||||||
|
<div v-else :key="index" class="px-px" :class="{ 'digit-focused': focusedDigit == digit }" @click.stop="focusDigit(digit)">{{ digits[digit] }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: [String, Number],
|
||||||
|
showThreeDigitHour: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickOutside,
|
||||||
|
events: ['mousedown'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
digitDisplay: ['hour1', 'hour0', ':', 'minute1', 'minute0', ':', 'second1', 'second0'],
|
||||||
|
focusedDigit: null,
|
||||||
|
digits: {
|
||||||
|
hour2: 0,
|
||||||
|
hour1: 0,
|
||||||
|
hour0: 0,
|
||||||
|
minute1: 0,
|
||||||
|
minute0: 0,
|
||||||
|
second1: 0,
|
||||||
|
second0: 0
|
||||||
|
},
|
||||||
|
isOver99Hours: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.initDigits()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
initDigits() {
|
||||||
|
var totalSeconds = !this.value || isNaN(this.value) ? 0 : Number(this.value)
|
||||||
|
totalSeconds = Math.round(totalSeconds)
|
||||||
|
|
||||||
|
var minutes = Math.floor(totalSeconds / 60)
|
||||||
|
var seconds = totalSeconds - minutes * 60
|
||||||
|
var hours = Math.floor(minutes / 60)
|
||||||
|
minutes -= hours * 60
|
||||||
|
|
||||||
|
this.digits.second1 = seconds <= 9 ? 0 : Number(String(seconds)[0])
|
||||||
|
this.digits.second0 = seconds <= 9 ? seconds : Number(String(seconds)[1])
|
||||||
|
|
||||||
|
this.digits.minute1 = minutes <= 9 ? 0 : Number(String(minutes)[0])
|
||||||
|
this.digits.minute0 = minutes <= 9 ? minutes : Number(String(minutes)[1])
|
||||||
|
|
||||||
|
if (hours > 99) {
|
||||||
|
this.digits.hour2 = Number(String(hours)[0])
|
||||||
|
this.digits.hour1 = Number(String(hours)[1])
|
||||||
|
this.digits.hour0 = Number(String(hours)[2])
|
||||||
|
this.isOver99Hours = true
|
||||||
|
} else {
|
||||||
|
this.digits.hour1 = hours <= 9 ? 0 : Number(String(hours)[0])
|
||||||
|
this.digits.hour0 = hours <= 9 ? hours : Number(String(hours)[1])
|
||||||
|
this.isOver99Hours = this.showThreeDigitHour
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isOver99Hours) {
|
||||||
|
this.digitDisplay = ['hour2', 'hour1', 'hour0', ':', 'minute1', 'minute0', ':', 'second1', 'second0']
|
||||||
|
} else {
|
||||||
|
this.digitDisplay = ['hour1', 'hour0', ':', 'minute1', 'minute0', ':', 'second1', 'second0']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateSeconds() {
|
||||||
|
var seconds = this.digits.second0 + this.digits.second1 * 10
|
||||||
|
seconds += this.digits.minute0 * 60 + this.digits.minute1 * 600
|
||||||
|
seconds += this.digits.hour0 * 3600 + this.digits.hour1 * 36000
|
||||||
|
if (this.isOver99Hours) seconds += this.digits.hour2 * 360000
|
||||||
|
|
||||||
|
if (Number(this.value) !== seconds) {
|
||||||
|
this.$emit('input', seconds)
|
||||||
|
this.$emit('change', seconds)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickMedian(index) {
|
||||||
|
// Click colon select digit to right
|
||||||
|
if (index >= 5) {
|
||||||
|
this.focusedDigit = 'second1'
|
||||||
|
} else {
|
||||||
|
this.focusedDigit = 'minute1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickOutside() {
|
||||||
|
this.removeFocus()
|
||||||
|
},
|
||||||
|
removeFocus() {
|
||||||
|
this.focusedDigit = null
|
||||||
|
this.removeListeners()
|
||||||
|
},
|
||||||
|
focusDigit(digit) {
|
||||||
|
if (this.focusedDigit == null || isNaN(this.focusedDigit)) this.initListeners()
|
||||||
|
this.focusedDigit = digit
|
||||||
|
},
|
||||||
|
clickInput() {
|
||||||
|
if (this.focusedDigit) return
|
||||||
|
this.focusDigit('second0')
|
||||||
|
},
|
||||||
|
shiftFocusLeft() {
|
||||||
|
if (!this.focusedDigit) return
|
||||||
|
if (this.focusedDigit.endsWith('2')) return
|
||||||
|
|
||||||
|
const isDigit1 = this.focusedDigit.endsWith('1')
|
||||||
|
if (!isDigit1) {
|
||||||
|
const digit1Key = this.focusedDigit.replace('0', '1')
|
||||||
|
this.focusedDigit = digit1Key
|
||||||
|
} else if (this.focusedDigit.startsWith('second')) {
|
||||||
|
this.focusedDigit = 'minute0'
|
||||||
|
} else if (this.focusedDigit.startsWith('minute')) {
|
||||||
|
this.focusedDigit = 'hour0'
|
||||||
|
} else if (this.isOver99Hours && this.focusedDigit.startsWith('hour')) {
|
||||||
|
this.focusedDigit = 'hour2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shiftFocusRight() {
|
||||||
|
if (!this.focusedDigit) return
|
||||||
|
if (this.focusedDigit.endsWith('2')) {
|
||||||
|
// Must be hour2
|
||||||
|
this.focusedDigit = 'hour1'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isDigit1 = this.focusedDigit.endsWith('1')
|
||||||
|
if (isDigit1) {
|
||||||
|
const digit0Key = this.focusedDigit.replace('1', '0')
|
||||||
|
this.focusedDigit = digit0Key
|
||||||
|
} else if (this.focusedDigit.startsWith('hour')) {
|
||||||
|
this.focusedDigit = 'minute1'
|
||||||
|
} else if (this.focusedDigit.startsWith('minute')) {
|
||||||
|
this.focusedDigit = 'second1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
increaseFocused() {
|
||||||
|
if (!this.focusedDigit) return
|
||||||
|
const isDigit1 = this.focusedDigit.endsWith('1')
|
||||||
|
const digit = Number(this.digits[this.focusedDigit])
|
||||||
|
if (isDigit1 && !this.focusedDigit.startsWith('hour')) this.digits[this.focusedDigit] = (digit + 1) % 6
|
||||||
|
else this.digits[this.focusedDigit] = (digit + 1) % 10
|
||||||
|
this.updateSeconds()
|
||||||
|
},
|
||||||
|
decreaseFocused() {
|
||||||
|
if (!this.focusedDigit) return
|
||||||
|
const isDigit1 = this.focusedDigit.endsWith('1')
|
||||||
|
const digit = Number(this.digits[this.focusedDigit])
|
||||||
|
if (isDigit1 && !this.focusedDigit.startsWith('hour')) this.digits[this.focusedDigit] = digit - 1 < 0 ? 5 : digit - 1
|
||||||
|
else this.digits[this.focusedDigit] = digit - 1 < 0 ? 9 : digit - 1
|
||||||
|
this.updateSeconds()
|
||||||
|
},
|
||||||
|
keydown(evt) {
|
||||||
|
if (!this.focusedDigit || !evt.key) return
|
||||||
|
|
||||||
|
if (evt.key === 'ArrowLeft') {
|
||||||
|
return this.shiftFocusLeft()
|
||||||
|
} else if (evt.key === 'ArrowRight') {
|
||||||
|
return this.shiftFocusRight()
|
||||||
|
} else if (evt.key === 'ArrowUp') {
|
||||||
|
return this.increaseFocused()
|
||||||
|
} else if (evt.key === 'ArrowDown') {
|
||||||
|
return this.decreaseFocused()
|
||||||
|
} else if (evt.key === 'Enter' || evt.key === 'Escape') {
|
||||||
|
return this.removeFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(evt.key)) return
|
||||||
|
|
||||||
|
var digit = Number(evt.key)
|
||||||
|
const isDigit1 = this.focusedDigit.endsWith('1')
|
||||||
|
if (isDigit1 && !this.focusedDigit.startsWith('hour') && digit >= 6) {
|
||||||
|
digit = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
this.digits[this.focusedDigit] = digit
|
||||||
|
|
||||||
|
this.updateSeconds()
|
||||||
|
this.shiftFocusRight()
|
||||||
|
},
|
||||||
|
initListeners() {
|
||||||
|
window.addEventListener('keydown', this.keydown)
|
||||||
|
},
|
||||||
|
removeListeners() {
|
||||||
|
window.removeEventListener('keydown', this.keydown)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.removeListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.digit-focused {
|
||||||
|
background-color: #555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -53,7 +53,7 @@ export default {
|
|||||||
var tooltip = document.createElement('div')
|
var tooltip = document.createElement('div')
|
||||||
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
this.tooltipId = String(Math.floor(Math.random() * 10000))
|
||||||
tooltip.id = this.tooltipId
|
tooltip.id = this.tooltipId
|
||||||
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs'
|
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white pointer-events-none text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
|
||||||
tooltip.style.zIndex = 100
|
tooltip.style.zIndex = 100
|
||||||
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
|
||||||
tooltip.innerHTML = this.text
|
tooltip.innerHTML = this.text
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative">
|
||||||
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
|
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
|
||||||
<div class="flex -mx-1">
|
<div class="flex flex-wrap -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-full md:w-1/2 px-1">
|
||||||
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
|
<ui-text-input-with-label ref="titleInput" v-model="details.title" label="Title" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||||
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
|
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" label="Subtitle" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-3/4 px-1">
|
<div class="w-full md:w-3/4 px-1">
|
||||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
|
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" label="Authors" endpoint="authors/search" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||||
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
|
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" label="Publish Year" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,35 +28,35 @@
|
|||||||
|
|
||||||
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" label="Description" class="mt-2" />
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-full md:w-1/2 px-1">
|
||||||
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
|
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||||
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
|
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-full md:w-1/2 px-1">
|
||||||
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
|
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" label="Narrators" :items="narrators" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||||
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
|
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||||
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
|
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex flex-wrap mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-full md:w-1/2 px-1">
|
||||||
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
|
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" label="Publisher" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/4 px-1">
|
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
|
||||||
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
|
<ui-text-input-with-label ref="languageInput" v-model="details.language" label="Language" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1 pt-6">
|
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
<ui-checkbox v-model="details.explicit" label="Explicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="`h-${size} w-${size}`">
|
<div :class="`h-${size} w-${size} min-w-${size}`">
|
||||||
<component :is="iconComponentName" />
|
<component :is="iconComponentName" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<div :key="index" class="flex h-7 items-center px-2 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click="clickAction(item.func)">
|
<div :key="index" class="flex h-7 items-center px-2 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.func)">
|
||||||
<p>{{ item.text }}</p>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -5,5 +5,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {}
|
export default {
|
||||||
|
mounted() {
|
||||||
|
document.body.classList.remove('app-bar', 'app-bar-and-toolbar')
|
||||||
|
document.body.classList.add('no-bars')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
+33
-18
@@ -15,6 +15,7 @@
|
|||||||
<modals-podcast-edit-episode />
|
<modals-podcast-edit-episode />
|
||||||
<modals-podcast-view-episode />
|
<modals-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
|
<prompt-confirm />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -40,6 +41,7 @@ export default {
|
|||||||
if (this.$store.state.selectedLibraryItems) {
|
if (this.$store.state.selectedLibraryItems) {
|
||||||
this.$store.commit('setSelectedLibraryItems', [])
|
this.$store.commit('setSelectedLibraryItems', [])
|
||||||
}
|
}
|
||||||
|
this.updateBodyClass()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -53,11 +55,23 @@ export default {
|
|||||||
if (!this.$route.name) return false
|
if (!this.$route.name) return false
|
||||||
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
|
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
isShowingToolbar() {
|
||||||
|
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
|
||||||
|
},
|
||||||
appContentMarginLeft() {
|
appContentMarginLeft() {
|
||||||
return this.isShowingSideRail ? 80 : 0
|
return this.isShowingSideRail ? 80 : 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateBodyClass() {
|
||||||
|
if (this.isShowingToolbar) {
|
||||||
|
document.body.classList.remove('no-bars', 'app-bar')
|
||||||
|
document.body.classList.add('app-bar-and-toolbar')
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('no-bars', 'app-bar-and-toolbar')
|
||||||
|
document.body.classList.add('app-bar')
|
||||||
|
}
|
||||||
|
},
|
||||||
updateSocketConnectionToast(content, type, timeout) {
|
updateSocketConnectionToast(content, type, timeout) {
|
||||||
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
|
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
|
||||||
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
|
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
|
||||||
@@ -110,13 +124,7 @@ export default {
|
|||||||
}
|
}
|
||||||
console.log('Init Payload', payload)
|
console.log('Init Payload', payload)
|
||||||
if (payload.session) {
|
if (payload.session) {
|
||||||
if (this.$refs.streamContainer) {
|
this.$refs.streamContainer.sessionOpen(payload.session)
|
||||||
this.$refs.streamContainer.sessionOpen(payload.session)
|
|
||||||
} else {
|
|
||||||
console.warn('Stream Container not mounted')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (payload.serverSettings) {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start scans currently running
|
// Start scans currently running
|
||||||
@@ -203,6 +211,12 @@ export default {
|
|||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) {
|
if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) {
|
||||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
|
if (this.$store.state.globals.selectedEpisode && libraryItem.mediaType === 'podcast') {
|
||||||
|
const episode = libraryItem.media.episodes.find((ep) => ep.id === this.$store.state.globals.selectedEpisode.id)
|
||||||
|
if (episode) {
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
|
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
|
||||||
this.$store.commit('libraries/updateFilterDataWithItem', libraryItem)
|
this.$store.commit('libraries/updateFilterDataWithItem', libraryItem)
|
||||||
@@ -243,9 +257,9 @@ export default {
|
|||||||
|
|
||||||
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
|
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
|
||||||
if (existingScan && !isNaN(existingScan.toastId)) {
|
if (existingScan && !isNaN(existingScan.toastId)) {
|
||||||
this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, position: 'bottom-center', onClose: () => null } }, true)
|
this.$toast.update(existingScan.toastId, { content: message, options: { timeout: 5000, type: 'success', closeButton: false, onClose: () => null } }, true)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.success(message, { timeout: 5000, position: 'bottom-center' })
|
this.$toast.success(message, { timeout: 5000 })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('scanners/remove', data)
|
this.$store.commit('scanners/remove', data)
|
||||||
@@ -254,7 +268,7 @@ export default {
|
|||||||
this.$root.socket.emit('cancel_scan', id)
|
this.$root.socket.emit('cancel_scan', id)
|
||||||
},
|
},
|
||||||
scanStart(data) {
|
scanStart(data) {
|
||||||
data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
|
data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) })
|
||||||
this.$store.commit('scanners/addUpdate', data)
|
this.$store.commit('scanners/addUpdate', data)
|
||||||
},
|
},
|
||||||
scanProgress(data) {
|
scanProgress(data) {
|
||||||
@@ -263,7 +277,7 @@ export default {
|
|||||||
data.toastId = existingScan.toastId
|
data.toastId = existingScan.toastId
|
||||||
this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true)
|
this.$toast.update(existingScan.toastId, { content: `Scanning "${existingScan.name}"... ${data.progress.progress || 0}%`, options: { timeout: false } }, true)
|
||||||
} else {
|
} else {
|
||||||
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
|
data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, onClose: () => this.onScanToastCancel(data.id) })
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('scanners/addUpdate', data)
|
this.$store.commit('scanners/addUpdate', data)
|
||||||
@@ -347,11 +361,11 @@ export default {
|
|||||||
download.status = this.$constants.DownloadStatus.EXPIRED
|
download.status = this.$constants.DownloadStatus.EXPIRED
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
},
|
},
|
||||||
showErrorToast(message) {
|
rssFeedOpen(data) {
|
||||||
this.$toast.error(message)
|
this.$store.commit('feeds/addFeed', data)
|
||||||
},
|
},
|
||||||
showSuccessToast(message) {
|
rssFeedClosed(data) {
|
||||||
this.$toast.success(message)
|
this.$store.commit('feeds/removeFeed', data)
|
||||||
},
|
},
|
||||||
backupApplied() {
|
backupApplied() {
|
||||||
// Force refresh
|
// Force refresh
|
||||||
@@ -423,9 +437,9 @@ export default {
|
|||||||
this.socket.on('abmerge_killed', this.abmergeKilled)
|
this.socket.on('abmerge_killed', this.abmergeKilled)
|
||||||
this.socket.on('abmerge_expired', this.abmergeExpired)
|
this.socket.on('abmerge_expired', this.abmergeExpired)
|
||||||
|
|
||||||
// Toast Listeners
|
// Feed Listeners
|
||||||
this.socket.on('show_error_toast', this.showErrorToast)
|
this.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
this.socket.on('show_success_toast', this.showSuccessToast)
|
this.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
|
||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
},
|
},
|
||||||
@@ -527,6 +541,7 @@ export default {
|
|||||||
this.initializeSocket()
|
this.initializeSocket()
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.updateBodyClass()
|
||||||
this.resize()
|
this.resize()
|
||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
window.addEventListener('keydown', this.keyDown)
|
window.addEventListener('keydown', this.keyDown)
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ export default {
|
|||||||
width: this.entityWidth,
|
width: this.entityWidth,
|
||||||
height: this.entityHeight,
|
height: this.entityHeight,
|
||||||
bookCoverAspectRatio: this.bookCoverAspectRatio,
|
bookCoverAspectRatio: this.bookCoverAspectRatio,
|
||||||
bookshelfView: this.bookshelfView
|
bookshelfView: this.bookshelfView,
|
||||||
|
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.entityName === 'books') {
|
if (this.entityName === 'books') {
|
||||||
props.filterBy = this.filterBy
|
props.filterBy = this.filterBy
|
||||||
props.orderBy = this.orderBy
|
props.orderBy = this.orderBy
|
||||||
props.sortingIgnorePrefix = !!this.sortingIgnorePrefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _this = this
|
var _this = this
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ export default {
|
|||||||
var validOtherFiles = []
|
var validOtherFiles = []
|
||||||
var ignoredFiles = []
|
var ignoredFiles = []
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
var filetype = this.checkFileType(file.name)
|
// var filetype = this.checkFileType(file.name)
|
||||||
if (!filetype) ignoredFiles.push(file)
|
if (!file.filetype) ignoredFiles.push(file)
|
||||||
else {
|
else {
|
||||||
file.filetype = filetype
|
// file.filetype = filetype
|
||||||
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
||||||
else validOtherFiles.push(file)
|
else validOtherFiles.push(file)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -82,12 +82,18 @@ export default {
|
|||||||
items: itemResults,
|
items: itemResults,
|
||||||
ignoredFiles: ignoredFilesInRoot
|
ignoredFiles: ignoredFilesInRoot
|
||||||
}
|
}
|
||||||
} else {
|
} else if (filetree.some((f) => f.filetype !== 'audio') || mediaType !== 'book') {
|
||||||
// Single Book drop
|
// Single Book drop
|
||||||
return {
|
return {
|
||||||
items: this.itemFromTreeItems(filetree, mediaType),
|
items: this.itemFromTreeItems(filetree, mediaType),
|
||||||
ignoredFiles: []
|
ignoredFiles: []
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Only audio files dropped so treat each one as an audiobook
|
||||||
|
return {
|
||||||
|
items: filetree.map((audioFile) => ({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] })),
|
||||||
|
ignoredFiles: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getFilesDropped(dataTransferItems) {
|
getFilesDropped(dataTransferItems) {
|
||||||
@@ -95,11 +101,12 @@ export default {
|
|||||||
path: '/',
|
path: '/',
|
||||||
items: []
|
items: []
|
||||||
}
|
}
|
||||||
function traverseFileTreePromise(item, currtreemap) {
|
function traverseFileTreePromise(item, currtreemap, checkFileType) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (item.isFile) {
|
if (item.isFile) {
|
||||||
item.file((file) => {
|
item.file((file) => {
|
||||||
file.filepath = currtreemap.path + file.name //save full path
|
file.filepath = currtreemap.path + file.name //save full path
|
||||||
|
file.filetype = checkFileType(file.name)
|
||||||
currtreemap.items.push(file)
|
currtreemap.items.push(file)
|
||||||
resolve(file)
|
resolve(file)
|
||||||
})
|
})
|
||||||
@@ -119,7 +126,7 @@ export default {
|
|||||||
dirReader.readEntries((entries) => {
|
dirReader.readEntries((entries) => {
|
||||||
if (entries.length > 0) {
|
if (entries.length > 0) {
|
||||||
for (let entr of entries) {
|
for (let entr of entries) {
|
||||||
entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
|
entriesPromises.push(traverseFileTreePromise(entr, newtreemap, checkFileType))
|
||||||
}
|
}
|
||||||
readEntries()
|
readEntries()
|
||||||
} else {
|
} else {
|
||||||
@@ -135,7 +142,7 @@ export default {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let entriesPromises = []
|
let entriesPromises = []
|
||||||
for (let it of dataTransferItems) {
|
for (let it of dataTransferItems) {
|
||||||
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap)
|
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap, this.checkFileType)
|
||||||
entriesPromises.push(filetree)
|
entriesPromises.push(filetree)
|
||||||
}
|
}
|
||||||
Promise.all(entriesPromises).then(() => {
|
Promise.all(entriesPromises).then(() => {
|
||||||
@@ -152,7 +159,9 @@ export default {
|
|||||||
...book
|
...book
|
||||||
}
|
}
|
||||||
var firstBookFile = book.itemFiles[0]
|
var firstBookFile = book.itemFiles[0]
|
||||||
if (!firstBookFile.filepath) return audiobook // No path
|
if (!firstBookFile.filepath) {
|
||||||
|
return audiobook // No path
|
||||||
|
}
|
||||||
|
|
||||||
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
||||||
|
|
||||||
@@ -165,6 +174,9 @@ export default {
|
|||||||
if (dirs.length) {
|
if (dirs.length) {
|
||||||
audiobook.author = dirs.pop()
|
audiobook.author = dirs.pop()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Use file basename as title
|
||||||
|
audiobook.title = Path.basename(firstBookFile.name, Path.extname(firstBookFile.name))
|
||||||
}
|
}
|
||||||
return audiobook
|
return audiobook
|
||||||
},
|
},
|
||||||
@@ -178,7 +190,12 @@ export default {
|
|||||||
if (!firstAudioFile.filepath) return podcast // No path
|
if (!firstAudioFile.filepath) return podcast // No path
|
||||||
var firstPath = Path.dirname(firstAudioFile.filepath)
|
var firstPath = Path.dirname(firstAudioFile.filepath)
|
||||||
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
||||||
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
if (dirs.length) {
|
||||||
|
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
||||||
|
} else {
|
||||||
|
podcast.title = Path.basename(firstAudioFile.name, Path.extname(firstAudioFile.name))
|
||||||
|
}
|
||||||
|
|
||||||
return podcast
|
return podcast
|
||||||
},
|
},
|
||||||
cleanItem(item, mediaType, index) {
|
cleanItem(item, mediaType, index) {
|
||||||
@@ -188,6 +205,7 @@ export default {
|
|||||||
async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
|
async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
|
||||||
var files = await this.getFilesDropped(dataTransferItems)
|
var files = await this.getFilesDropped(dataTransferItems)
|
||||||
if (!files || !files.length) return { error: 'No files found ' }
|
if (!files || !files.length) return { error: 'No files found ' }
|
||||||
|
|
||||||
var itemData = this.fileTreeToItems(files, mediaType)
|
var itemData = this.fileTreeToItems(files, mediaType)
|
||||||
if (!itemData.items.length && !itemData.ignoredFiles.length) {
|
if (!itemData.items.length && !itemData.ignoredFiles.length) {
|
||||||
return { error: 'Invalid file drop' }
|
return { error: 'Invalid file drop' }
|
||||||
@@ -218,9 +236,12 @@ export default {
|
|||||||
else {
|
else {
|
||||||
file.filetype = filetype
|
file.filetype = filetype
|
||||||
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
|
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
|
||||||
|
else file.filepath = file.name
|
||||||
|
|
||||||
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
|
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
|
||||||
var dir = file.filepath ? Path.dirname(file.filepath) : ''
|
var dir = file.filepath ? Path.dirname(file.filepath) : ''
|
||||||
|
if (dir === '.') dir = ''
|
||||||
|
|
||||||
if (!itemMap[dir]) {
|
if (!itemMap[dir]) {
|
||||||
itemMap[dir] = {
|
itemMap[dir] = {
|
||||||
path: dir,
|
path: dir,
|
||||||
@@ -246,8 +267,17 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var items = []
|
||||||
var index = 1
|
var index = 1
|
||||||
var items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
// If book media type and all files are audio files then treat each one as an audiobook
|
||||||
|
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) {
|
||||||
|
items = itemMap[''].itemFiles.map((audioFile) => {
|
||||||
|
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
ignoredFiles: ignoredFiles
|
ignoredFiles: ignoredFiles
|
||||||
|
|||||||
+25
-6
@@ -58,7 +58,8 @@ module.exports = {
|
|||||||
buildModules: [
|
buildModules: [
|
||||||
// https://go.nuxtjs.dev/tailwindcss
|
// https://go.nuxtjs.dev/tailwindcss
|
||||||
'@nuxtjs/tailwindcss',
|
'@nuxtjs/tailwindcss',
|
||||||
'@nuxtjs/pwa'
|
'@nuxtjs/pwa',
|
||||||
|
'@nuxt/postcss8'
|
||||||
],
|
],
|
||||||
|
|
||||||
// Modules: https://go.nuxtjs.dev/config-modules
|
// Modules: https://go.nuxtjs.dev/config-modules
|
||||||
@@ -107,23 +108,33 @@ module.exports = {
|
|||||||
background_color: '#373838',
|
background_color: '#373838',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/icon64.png',
|
src: '/icon.svg',
|
||||||
sizes: "64x64"
|
sizes: "64x64"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/icon192.png',
|
src: '/icon.svg',
|
||||||
sizes: "192x192"
|
sizes: "192x192"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/Logo.png',
|
src: '/icon.svg',
|
||||||
sizes: "512x512"
|
sizes: "512x512"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
enabled: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Build Configuration: https://go.nuxtjs.dev/config-build
|
// Build Configuration: https://go.nuxtjs.dev/config-build
|
||||||
build: {},
|
build: {
|
||||||
|
postcss: {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
watchers: {
|
watchers: {
|
||||||
webpack: {
|
webpack: {
|
||||||
aggregateTimeout: 300,
|
aggregateTimeout: 300,
|
||||||
@@ -133,5 +144,13 @@ module.exports = {
|
|||||||
server: {
|
server: {
|
||||||
port: process.env.NODE_ENV === 'production' ? 80 : 3000,
|
port: process.env.NODE_ENV === 'production' ? 80 : 3000,
|
||||||
host: '0.0.0.0'
|
host: '0.0.0.0'
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary workaround for @nuxt-community/tailwindcss-module.
|
||||||
|
*
|
||||||
|
* Reported: 2022-05-23
|
||||||
|
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
||||||
|
*/
|
||||||
|
devServerHandlers: [],
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+566
-397
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.20",
|
"version": "2.1.2",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -29,8 +29,11 @@
|
|||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@nuxt/postcss8": "^1.1.3",
|
||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
"@nuxtjs/tailwindcss": "^4.2.1",
|
||||||
"postcss": "^8.3.6"
|
"autoprefixer": "^10.4.7",
|
||||||
|
"postcss": "^8.3.6",
|
||||||
|
"tailwindcss": "^3.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
|
<p class="text-lg mb-4 font-semibold">Audiobook Chapters</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" label="Show seconds" class="mx-2" />
|
||||||
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
|
<ui-btn color="primary" small class="mx-2" @click="showFindChaptersModal = true">Lookup</ui-btn>
|
||||||
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
|
<ui-btn color="success" small @click="saveChapters">Save</ui-btn>
|
||||||
<div class="w-40" />
|
<div class="w-40" />
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
<div :key="chapter.id" class="flex py-1">
|
<div :key="chapter.id" class="flex py-1">
|
||||||
<div class="w-12">#{{ chapter.id + 1 }}</div>
|
<div class="w-12">#{{ chapter.id + 1 }}</div>
|
||||||
<div class="w-32 px-1">
|
<div class="w-32 px-1">
|
||||||
<ui-text-input v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
|
||||||
|
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input v-model="chapter.title" class="text-xs" />
|
<ui-text-input v-model="chapter.title" class="text-xs" />
|
||||||
@@ -136,7 +138,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, app, redirect, route }) {
|
async asyncData({ store, params, app, redirect, from }) {
|
||||||
if (!store.getters['user/getUserCanUpdate']) {
|
if (!store.getters['user/getUserCanUpdate']) {
|
||||||
return redirect('/?error=unauthorized')
|
return redirect('/?error=unauthorized')
|
||||||
}
|
}
|
||||||
@@ -152,8 +154,12 @@ export default {
|
|||||||
console.error('Invalid media type')
|
console.error('Invalid media type')
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var previousRoute = from ? from.fullPath : null
|
||||||
|
if (from && from.path === '/login') previousRoute = null
|
||||||
return {
|
return {
|
||||||
libraryItem
|
libraryItem,
|
||||||
|
previousRoute
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -168,7 +174,8 @@ export default {
|
|||||||
asinInput: null,
|
asinInput: null,
|
||||||
findingChapters: false,
|
findingChapters: false,
|
||||||
showFindChaptersModal: false,
|
showFindChaptersModal: false,
|
||||||
chapterData: null
|
chapterData: null,
|
||||||
|
showSecondInputs: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -339,7 +346,6 @@ export default {
|
|||||||
|
|
||||||
this.saving = true
|
this.saving = true
|
||||||
|
|
||||||
console.log('udpated chapters', this.newChapters)
|
|
||||||
const payload = {
|
const payload = {
|
||||||
chapters: this.newChapters
|
chapters: this.newChapters
|
||||||
}
|
}
|
||||||
@@ -349,7 +355,11 @@ export default {
|
|||||||
this.saving = false
|
this.saving = false
|
||||||
if (data.updated) {
|
if (data.updated) {
|
||||||
this.$toast.success('Chapters updated')
|
this.$toast.success('Chapters updated')
|
||||||
this.$router.push(`/item/${this.libraryItem.id}`)
|
if (this.previousRoute) {
|
||||||
|
this.$router.push(this.previousRoute)
|
||||||
|
} else {
|
||||||
|
this.$router.push(`/item/${this.libraryItem.id}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No changes needed updating')
|
this.$toast.info('No changes needed updating')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
<div class="flex sm:items-end flex-col sm:flex-row">
|
<div class="flex items-end flex-row flex-wrap md:flex-nowrap">
|
||||||
<h1 class="text-2xl md:text-3xl font-sans">
|
<h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
|
||||||
{{ collectionName }}
|
{{ collectionName }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ export default {
|
|||||||
.configContent.page-library-stats {
|
.configContent.page-library-stats {
|
||||||
width: 1200px;
|
width: 1200px;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 1550px) {
|
||||||
|
.configContent.page-library-stats {
|
||||||
|
margin-left: 176px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 1240px) {
|
@media (max-width: 1240px) {
|
||||||
.configContent {
|
.configContent {
|
||||||
margin-left: 176px;
|
margin-left: 176px;
|
||||||
@@ -82,5 +87,8 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.configContent.page-library-stats {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -14,6 +14,12 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center py-2">
|
||||||
|
<ui-text-input v-model="cronExpression" :disabled="updatingServerSettings" class="w-32" @change="changedCronExpression" />
|
||||||
|
|
||||||
|
<p class="pl-4 text-lg">Cron expression</p>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
||||||
|
|
||||||
@@ -41,6 +47,7 @@ export default {
|
|||||||
dailyBackups: true,
|
dailyBackups: true,
|
||||||
backupsToKeep: 2,
|
backupsToKeep: 2,
|
||||||
maxBackupSize: 1,
|
maxBackupSize: 1,
|
||||||
|
// cronExpression: '',
|
||||||
newServerSettings: {}
|
newServerSettings: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -54,7 +61,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dailyBackupsTooltip() {
|
dailyBackupsTooltip() {
|
||||||
return 'Runs at 1am every day (your server time). Saved in /metadata/backups.'
|
return 'Runs at 1:30am every day (your server time). Saved in /metadata/backups.'
|
||||||
},
|
},
|
||||||
maxBackupSizeTooltip() {
|
maxBackupSizeTooltip() {
|
||||||
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
return 'As a safeguard against misconfiguration, backups will fail if they exceed the configured size.'
|
||||||
@@ -64,6 +71,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// changedCronExpression() {
|
||||||
|
// this.$axios
|
||||||
|
// .$post('/api/validate-cron', { expression: this.cronExpression })
|
||||||
|
// .then(() => {
|
||||||
|
// console.log('Cron is valid')
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error('Cron validation failed', error)
|
||||||
|
// const msg = (error.response ? error.response.data : null) || 'Unknown cron validation error'
|
||||||
|
// this.$toast.error(msg)
|
||||||
|
// })
|
||||||
|
// },
|
||||||
updateBackupsSettings() {
|
updateBackupsSettings() {
|
||||||
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
||||||
this.$toast.error('Invalid maximum backup size')
|
this.$toast.error('Invalid maximum backup size')
|
||||||
@@ -74,7 +93,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
backupSchedule: this.dailyBackups ? '0 1 * * *' : false,
|
backupSchedule: this.dailyBackups ? '30 1 * * *' : false,
|
||||||
backupsToKeep: Number(this.backupsToKeep),
|
backupsToKeep: Number(this.backupsToKeep),
|
||||||
maxBackupSize: Number(this.maxBackupSize)
|
maxBackupSize: Number(this.maxBackupSize)
|
||||||
}
|
}
|
||||||
@@ -99,6 +118,7 @@ export default {
|
|||||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||||
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
||||||
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
||||||
|
// this.cronExpression = '30 1 * * *'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
+220
-153
@@ -1,150 +1,210 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!-- <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> -->
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-2">
|
||||||
|
<div class="mb-2">
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
|
||||||
<div class="flex items-center mb-2">
|
|
||||||
<h1 class="text-xl">Settings</h1>
|
<h1 class="text-xl">Settings</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="lg:flex">
|
||||||
<ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
<div class="flex-1">
|
||||||
<ui-tooltip :text="tooltips.storeCoverWithItem">
|
<div class="pt-4">
|
||||||
<p class="pl-4 text-lg">
|
<h2 class="font-semibold">General</h2>
|
||||||
Store covers with item
|
</div>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<div class="flex items-end py-2">
|
||||||
</p>
|
<ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||||
</ui-tooltip>
|
<ui-tooltip :text="tooltips.storeCoverWithItem">
|
||||||
</div>
|
<p class="pl-4">
|
||||||
|
Store covers with item
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||||
<ui-tooltip :text="tooltips.storeMetadataWithItem">
|
<ui-tooltip :text="tooltips.storeMetadataWithItem">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Store metadata with item
|
Store metadata with item
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||||
<ui-tooltip :text="tooltips.coverAspectRatio">
|
<ui-tooltip :text="tooltips.sortingIgnorePrefix">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Use square book covers
|
Ignore prefixes when sorting
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
|
||||||
|
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" label="Prefixes to Ignore (case insensitive)" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
|
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||||
<ui-tooltip :text="tooltips.bookshelfView">
|
<p class="pl-4">Chromecast support</p>
|
||||||
<p class="pl-4 text-lg">
|
</div>
|
||||||
Use alternative bookshelf view
|
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="pt-4">
|
||||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
<h2 class="font-semibold">Display</h2>
|
||||||
<ui-tooltip :text="tooltips.sortingIgnorePrefix">
|
</div>
|
||||||
<p class="pl-4 text-lg">
|
|
||||||
Ignore prefixes when sorting title and series
|
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
<div v-if="newServerSettings.sortingIgnorePrefix" class="w-72 ml-14 mb-2">
|
|
||||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" label="Prefixes to Ignore (case insensitive)" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
||||||
<p class="pl-4 text-lg">Enable Chromecast</p>
|
<ui-tooltip :text="tooltips.coverAspectRatio">
|
||||||
</div>
|
<p class="pl-4">
|
||||||
|
Square book covers
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mb-2 mt-8">
|
<div class="flex items-center py-2">
|
||||||
<h1 class="text-xl">Scanner Settings</h1>
|
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
|
||||||
</div>
|
<ui-tooltip :text="tooltips.bookshelfView">
|
||||||
|
<p class="pl-4">
|
||||||
|
Alternative bookshelf view
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
<p class="pr-4">Date Format</p>
|
||||||
<ui-tooltip :text="tooltips.scannerParseSubtitle">
|
<ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-40" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
<p class="pl-4 text-lg">
|
</div>
|
||||||
Scanner parse subtitles
|
</div>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex-1">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
<div class="pt-4">
|
||||||
<ui-tooltip :text="tooltips.scannerFindCovers">
|
<h2 class="font-semibold">Scanner</h2>
|
||||||
<p class="pl-4 text-lg">
|
</div>
|
||||||
Scanner find covers
|
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
</div>
|
|
||||||
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
|
|
||||||
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerPreferAudioMetadata">
|
<ui-tooltip :text="tooltips.scannerParseSubtitle">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Scanner prefer audio metadata
|
Parse subtitles
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
<ui-tooltip :text="tooltips.scannerFindCovers">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Scanner prefer OPF metadata
|
Find covers
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
<div class="flex-grow" />
|
||||||
|
</div>
|
||||||
|
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
|
||||||
|
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerPreferMatchedMetadata">
|
<ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Scanner prefer matched metadata
|
Use Overdrive Media Markers for chapters
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
|
||||||
<ui-tooltip :text="tooltips.scannerDisableWatcher">
|
<ui-tooltip :text="tooltips.scannerPreferAudioMetadata">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Disable Watcher
|
Prefer audio metadata
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mb-2 mt-8">
|
<div class="flex items-center py-2">
|
||||||
<h1 class="text-xl">Experimental Feature Settings</h1>
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
||||||
</div>
|
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
||||||
|
<p class="pl-4">
|
||||||
|
Prefer OPF metadata
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||||
<ui-tooltip :text="tooltips.enableEReader">
|
<ui-tooltip :text="tooltips.scannerPreferMatchedMetadata">
|
||||||
<p class="pl-4 text-lg">
|
<p class="pl-4">
|
||||||
Enable e-reader for all users
|
Prefer matched metadata
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerDisableWatcher">
|
||||||
|
<p class="pl-4">
|
||||||
|
Disable Watcher
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center py-2">
|
||||||
|
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerMaxThreads">
|
||||||
|
<p class="pl-4">
|
||||||
|
Max # of threads to use
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div class="pt-4">
|
||||||
|
<h2 class="font-semibold">Experimental Features</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-toggle-switch v-model="showExperimentalFeatures" />
|
||||||
|
<ui-tooltip :text="tooltips.experimentalFeatures">
|
||||||
|
<p class="pl-4">
|
||||||
|
Experimental Features
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.enableEReader">
|
||||||
|
<p class="pl-4">
|
||||||
|
Enable e-reader for all users
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.scannerUseSingleThreadedProber" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseSingleThreadedProber', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerUseSingleThreadedProber">
|
||||||
|
<p class="pl-4">
|
||||||
|
Scanner use old single threaded audio prober
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -152,7 +212,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click="purgeCache">Purge Cache</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isPurgingCache" @click="purgeCache">Purge Cache</ui-btn>
|
||||||
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block" :loading="isResettingLibraryItems" @click="resetLibraryItems">Remove All Library Items</ui-btn>
|
<ui-btn color="bg" small :padding-x="4" class="hidden lg:block mr-2" :loading="isResettingLibraryItems" @click="resetLibraryItems">Remove All Library Items</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
<p class="pr-2 text-sm font-book text-yellow-400">
|
||||||
Report bugs, request features, and contribute on
|
Report bugs, request features, and contribute on
|
||||||
@@ -188,30 +248,12 @@
|
|||||||
|
|
||||||
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||||
|
|
||||||
<div class="py-12 mb-4 opacity-60 hover:opacity-100">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<ui-toggle-switch v-model="showExperimentalFeatures" />
|
|
||||||
<ui-tooltip :text="tooltips.experimentalFeatures">
|
|
||||||
<p class="pl-4 text-lg">
|
|
||||||
Experimental Features
|
|
||||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<prompt-dialog v-model="showConfirmPurgeCache" :width="675">
|
<prompt-dialog v-model="showConfirmPurgeCache" :width="675">
|
||||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
<p class="text-error text-lg font-semibold">Important Notice!</p>
|
<p class="text-error font-semibold">Important Notice!</p>
|
||||||
<p class="text-lg my-2 text-center">Purge cache will delete the entire directory at <span class="font-mono">/metadata/cache</span>.</p>
|
<p class="my-2 text-center">Purge cache will delete the entire directory at <span class="font-mono">/metadata/cache</span>.</p>
|
||||||
|
|
||||||
<p class="text-lg text-center mb-8">Are you sure you want to remove the cache directory?</p>
|
<p class="text-center mb-8">Are you sure you want to remove the cache directory?</p>
|
||||||
<div class="flex px-1 items-center">
|
<div class="flex px-1 items-center">
|
||||||
<ui-btn color="primary" @click="showConfirmPurgeCache = false">Nevermind</ui-btn>
|
<ui-btn color="primary" @click="showConfirmPurgeCache = false">Nevermind</ui-btn>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -245,7 +287,10 @@ export default {
|
|||||||
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
|
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
|
||||||
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
||||||
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
||||||
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle below just for you)'
|
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
|
||||||
|
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
|
||||||
|
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
|
||||||
|
scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage. <br><br>Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)'
|
||||||
},
|
},
|
||||||
showConfirmPurgeCache: false
|
showConfirmPurgeCache: false
|
||||||
}
|
}
|
||||||
@@ -271,11 +316,31 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$store.commit('setExperimentalFeatures', val)
|
this.$store.commit('setExperimentalFeatures', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
dateFormats() {
|
||||||
|
return this.$store.state.globals.dateFormats
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateEnableChromecast(val) {
|
updateScannerMaxThreads(val) {
|
||||||
this.updateServerSettings({ enableChromecast: val })
|
if (!val || isNaN(val)) {
|
||||||
|
this.$toast.error('Invalid max threads must be a number')
|
||||||
|
this.newServerSettings.scannerMaxThreads = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Number(val) < 0) {
|
||||||
|
this.$toast.error('Max threads must be >= 0')
|
||||||
|
this.newServerSettings.scannerMaxThreads = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Math.round(Number(val)) !== Number(val)) {
|
||||||
|
this.$toast.error('Max threads must be an integer')
|
||||||
|
this.newServerSettings.scannerMaxThreads = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateServerSettings({
|
||||||
|
scannerMaxThreads: Number(val)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
updateSortingPrefixes(val) {
|
updateSortingPrefixes(val) {
|
||||||
if (!val || !val.length) {
|
if (!val || !val.length) {
|
||||||
@@ -314,10 +379,12 @@ export default {
|
|||||||
.then((success) => {
|
.then((success) => {
|
||||||
console.log('Updated Server Settings', success)
|
console.log('Updated Server Settings', success)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
|
this.$toast.success('Server settings updated')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to update server settings', error)
|
console.error('Failed to update server settings', error)
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
|
this.$toast.error('Failed to update server settings')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
<p class="text-xl">Stats for library {{ currentLibraryName }}</p>
|
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
|
||||||
|
|
||||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||||
|
|
||||||
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12">
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
|
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
|
||||||
<p v-if="!top5Genres.length">No Genres</p>
|
<p v-if="!top5Genres.length">No Genres</p>
|
||||||
|
|||||||
+12
-14
@@ -1,24 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="page-wrapper" class="page overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
|
<div class="w-full h-full">
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
<div class="mb-4 flex flex-col sm:flex-row items-start sm:items-end">
|
<div class="flex items-center mb-2">
|
||||||
<p class="text-2xl mr-4 mb-2 sm:mb-0">Logger</p>
|
<h1 class="text-xl">Logs</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mb-2 place-items-end">
|
||||||
|
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
|
||||||
|
|
||||||
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm mb-2 sm:mb-0" />
|
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
|
||||||
|
|
||||||
<div class="flex-grow" />
|
|
||||||
|
|
||||||
<div class="w-full sm:w-44">
|
|
||||||
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 550px; min-height: 550px">
|
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 800px; min-height: 550px">
|
||||||
<template v-for="(log, index) in logs">
|
<template v-for="(log, index) in logs">
|
||||||
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
||||||
<p class="text-gray-400 w-40 font-mono">{{ log.timestamp.split('.')[0].split('T').join(' ') }}</p>
|
<p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
|
||||||
<p class="font-semibold w-12 text-right" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
|
<p class="font-semibold w-12 text-right text-sm" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
|
||||||
<p class="px-4 logmessage">{{ log.message }}</p>
|
<p class="px-4 logmessage">{{ log.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -164,6 +161,7 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (!this.$root.socket) return
|
if (!this.$root.socket) return
|
||||||
|
this.$root.socket.emit('remove_log_listener')
|
||||||
this.$root.socket.off('daily_logs', this.dailyLogsLoaded)
|
this.$root.socket.off('daily_logs', this.dailyLogsLoaded)
|
||||||
this.$root.socket.off('log', this.logEvtReceived)
|
this.$root.socket.off('log', this.logEvtReceived)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8">
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
<div class="py-2">
|
<div class="flex items-center mb-2">
|
||||||
<div class="flex items-center mb-1">
|
<h1 class="text-xl">Listening Sessions</h1>
|
||||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
|
|
||||||
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
|
|
||||||
</div>
|
|
||||||
<div v-if="listeningSessions.length" class="block max-w-full">
|
|
||||||
<table class="userSessionsTable">
|
|
||||||
<tr class="bg-primary bg-opacity-40">
|
|
||||||
<th class="w-48 min-w-48 text-left">Item</th>
|
|
||||||
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
|
|
||||||
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
|
||||||
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
|
||||||
<th class="w-32 min-w-32">Listened</th>
|
|
||||||
<th class="w-16 min-w-16">Last Time</th>
|
|
||||||
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
|
||||||
<td class="py-1 max-w-48">
|
|
||||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
|
||||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="hidden md:table-cell">
|
|
||||||
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
|
||||||
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="hidden md:table-cell">
|
|
||||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="hidden sm:table-cell">
|
|
||||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
|
||||||
</td>
|
|
||||||
<td class="text-center hidden sm:table-cell">
|
|
||||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
|
||||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
|
||||||
</ui-tooltip>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div class="flex items-center justify-end py-1">
|
|
||||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
|
||||||
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
|
||||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mb-2">
|
||||||
|
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="listeningSessions.length" class="block max-w-full">
|
||||||
|
<table class="userSessionsTable">
|
||||||
|
<tr class="bg-primary bg-opacity-40">
|
||||||
|
<th class="w-48 min-w-48 text-left">Item</th>
|
||||||
|
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
|
||||||
|
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||||
|
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||||
|
<th class="w-32 min-w-32">Listened</th>
|
||||||
|
<th class="w-16 min-w-16">Last Time</th>
|
||||||
|
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||||
|
<td class="py-1 max-w-48">
|
||||||
|
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||||
|
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="hidden md:table-cell">
|
||||||
|
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
||||||
|
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="hidden md:table-cell">
|
||||||
|
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="hidden sm:table-cell">
|
||||||
|
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
|
||||||
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-center hidden sm:table-cell">
|
||||||
|
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||||
|
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div class="flex items-center justify-end my-2">
|
||||||
|
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||||
|
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||||
|
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
||||||
@@ -89,7 +89,8 @@ export default {
|
|||||||
total: 0,
|
total: 0,
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
userFilter: null,
|
userFilter: null,
|
||||||
selectedUser: ''
|
selectedUser: '',
|
||||||
|
processingGoToTimestamp: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -110,6 +111,41 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async clickCurrentTime(session) {
|
||||||
|
if (this.processingGoToTimestamp) return
|
||||||
|
this.processingGoToTimestamp = true
|
||||||
|
const libraryItem = await this.$axios.$get(`/api/items/${session.libraryItemId}`).catch((error) => {
|
||||||
|
console.error('Failed to get library item', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!libraryItem) {
|
||||||
|
this.$toast.error('Failed to get library item')
|
||||||
|
this.processingGoToTimestamp = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
|
||||||
|
this.$toast.error('Failed to get podcast episode')
|
||||||
|
this.processingGoToTimestamp = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
episodeId: session.episodeId || null,
|
||||||
|
startTime: session.currentTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.processingGoToTimestamp = false
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
updateUserFilter() {
|
updateUserFilter() {
|
||||||
this.loadSessions(0)
|
this.loadSessions(0)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||||
|
<h1 class="text-xl">Stats for {{ username }}</h1>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex p-2">
|
<div class="flex p-2">
|
||||||
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
<div :key="item.id" class="w-full py-0.5">
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-6 truncate">{{ index + 1 }}. </p>
|
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
@@ -84,6 +86,9 @@ export default {
|
|||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
|
username() {
|
||||||
|
return this.user.username
|
||||||
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
<widgets-online-indicator :value="!!userOnline" />
|
<widgets-online-indicator :value="!!userOnline" />
|
||||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
|
<div v-if="userToken" class="flex text-xs mt-4">
|
||||||
<p v-if="userToken" class="py-2 text-xs">
|
<ui-text-input-with-label label="API Token" :value="userToken" readonly />
|
||||||
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
|
|
||||||
><span class="material-icons pl-2 text-base">content_copy</span>
|
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
|
||||||
</p>
|
<span class="material-icons pl-2 text-base">content_copy</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
@@ -138,12 +139,15 @@ export default {
|
|||||||
this.$copyToClipboard(str, this)
|
this.$copyToClipboard(str, this)
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => {
|
this.listeningSessions = await this.$axios
|
||||||
return data.sessions || []
|
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
|
||||||
}).catch((err) => {
|
.then((data) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
return data.sessions || []
|
||||||
return []
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
|
console.error('Failed to load listening sesions', err)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
|
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center">
|
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
|
||||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center hidden sm:table-cell">
|
<td class="text-center hidden sm:table-cell">
|
||||||
@@ -85,7 +85,8 @@ export default {
|
|||||||
listeningSessions: [],
|
listeningSessions: [],
|
||||||
numPages: 0,
|
numPages: 0,
|
||||||
total: 0,
|
total: 0,
|
||||||
currentPage: 0
|
currentPage: 0,
|
||||||
|
processingGoToTimestamp: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -97,6 +98,41 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async clickCurrentTime(session) {
|
||||||
|
if (this.processingGoToTimestamp) return
|
||||||
|
this.processingGoToTimestamp = true
|
||||||
|
const libraryItem = await this.$axios.$get(`/api/items/${session.libraryItemId}`).catch((error) => {
|
||||||
|
console.error('Failed to get library item', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!libraryItem) {
|
||||||
|
this.$toast.error('Failed to get library item')
|
||||||
|
this.processingGoToTimestamp = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (session.episodeId && !libraryItem.media.episodes.find((ep) => ep.id === session.episodeId)) {
|
||||||
|
this.$toast.error('Failed to get podcast episode')
|
||||||
|
this.processingGoToTimestamp = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message: `Start playback for "${session.displayTitle}" at ${this.$secondsToTimestamp(session.currentTime)}?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$eventBus.$emit('play-item', {
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
episodeId: session.episodeId || null,
|
||||||
|
startTime: session.currentTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.processingGoToTimestamp = false
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
prevPage() {
|
prevPage() {
|
||||||
this.loadSessions(this.currentPage - 1)
|
this.loadSessions(this.currentPage - 1)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- Item Progress Bar -->
|
<!-- Item Progress Bar -->
|
||||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<!-- Item Cover Overlay -->
|
<!-- Item Cover Overlay -->
|
||||||
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
||||||
<div v-show="showPlayButton && !streaming" class="h-full flex items-center justify-center pointer-events-none">
|
<div v-show="showPlayButton && !isStreaming" 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 cursor-pointer" @click.stop.prevent="startStream">
|
<div class="hover:text-white text-gray-200 hover:scale-110 transform duration-200 pointer-events-auto cursor-pointer" @click.stop.prevent="startStream">
|
||||||
<span class="material-icons text-4xl">play_circle_filled</span>
|
<span class="material-icons text-4xl">play_circle_filled</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,9 +118,9 @@
|
|||||||
<!-- Progress -->
|
<!-- Progress -->
|
||||||
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
||||||
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p>
|
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
||||||
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||||
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p>
|
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
||||||
|
|
||||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||||
<span class="material-icons text-sm">close</span>
|
<span class="material-icons text-sm">close</span>
|
||||||
@@ -129,12 +129,12 @@
|
|||||||
|
|
||||||
<!-- Icon buttons -->
|
<!-- Icon buttons -->
|
||||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||||
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="startStream">
|
||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ streaming ? 'Playing' : 'Play' }}
|
{{ isStreaming ? 'Playing' : 'Play' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
<span v-show="!isStreaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
||||||
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
@@ -160,7 +160,11 @@
|
|||||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<!-- Experimental RSS feed open -->
|
<ui-tooltip v-if="bookmarks.length" text="Your Bookmarks" direction="top">
|
||||||
|
<ui-icon-btn :icon="bookmarks.length ? 'bookmarks' : 'bookmark_border'" class="mx-0.5" @click="clickBookmarksBtn" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<!-- RSS feed -->
|
||||||
<ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top">
|
<ui-tooltip v-if="showRssFeedBtn" text="Open RSS Feed" direction="top">
|
||||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -189,6 +193,7 @@
|
|||||||
|
|
||||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||||
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
|
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
|
||||||
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -222,10 +227,14 @@ export default {
|
|||||||
podcastFeedEpisodes: [],
|
podcastFeedEpisodes: [],
|
||||||
episodesDownloading: [],
|
episodesDownloading: [],
|
||||||
episodeDownloadsQueued: [],
|
episodeDownloadsQueued: [],
|
||||||
showRssFeedModal: false
|
showRssFeedModal: false,
|
||||||
|
showBookmarksModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
dateFormat() {
|
||||||
|
return this.$store.state.serverSettings.dateFormat
|
||||||
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
@@ -293,6 +302,10 @@ export default {
|
|||||||
chapters() {
|
chapters() {
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
|
bookmarks() {
|
||||||
|
if (this.isPodcast) return []
|
||||||
|
return this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId)
|
||||||
|
},
|
||||||
tracks() {
|
tracks() {
|
||||||
return this.media.tracks || []
|
return this.media.tracks || []
|
||||||
},
|
},
|
||||||
@@ -386,7 +399,7 @@ export default {
|
|||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
streaming() {
|
isStreaming() {
|
||||||
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
|
return this.streamLibraryItem && this.streamLibraryItem.id === this.libraryItemId
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
@@ -406,6 +419,31 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickBookmarksBtn() {
|
||||||
|
this.showBookmarksModal = true
|
||||||
|
},
|
||||||
|
selectBookmark(bookmark) {
|
||||||
|
if (!bookmark) return
|
||||||
|
if (this.isStreaming) {
|
||||||
|
this.$eventBus.$emit('playback-seek', bookmark.time)
|
||||||
|
} else if (this.streamLibraryItem) {
|
||||||
|
this.showBookmarksModal = false
|
||||||
|
console.log('Already streaming library item so ask about it')
|
||||||
|
const payload = {
|
||||||
|
message: `Start playback for "${this.title}" at ${this.$secondsToTimestamp(bookmark.time)}?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.startStream(bookmark.time)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
} else {
|
||||||
|
this.startStream(bookmark.time)
|
||||||
|
}
|
||||||
|
this.showBookmarksModal = false
|
||||||
|
},
|
||||||
clearDownloadQueue() {
|
clearDownloadQueue() {
|
||||||
if (confirm('Are you sure you want to clear episode download queue?')) {
|
if (confirm('Are you sure you want to clear episode download queue?')) {
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -450,7 +488,21 @@ export default {
|
|||||||
openEbook() {
|
openEbook() {
|
||||||
this.$store.commit('showEReader', this.libraryItem)
|
this.$store.commit('showEReader', this.libraryItem)
|
||||||
},
|
},
|
||||||
toggleFinished() {
|
toggleFinished(confirmed = false) {
|
||||||
|
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to mark "${this.title}" as finished?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.toggleFinished(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
isFinished: !this.userIsFinished
|
isFinished: !this.userIsFinished
|
||||||
}
|
}
|
||||||
@@ -467,7 +519,7 @@ export default {
|
|||||||
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
startStream() {
|
startStream(startTime = null) {
|
||||||
var episodeId = null
|
var episodeId = null
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
var episode = this.podcastEpisodes.find((ep) => {
|
var episode = this.podcastEpisodes.find((ep) => {
|
||||||
@@ -480,7 +532,8 @@ export default {
|
|||||||
|
|
||||||
this.$eventBus.$emit('play-item', {
|
this.$eventBus.$emit('play-item', {
|
||||||
libraryItemId: this.libraryItem.id,
|
libraryItemId: this.libraryItem.id,
|
||||||
episodeId
|
episodeId,
|
||||||
|
startTime
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
@@ -534,13 +587,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
rssFeedOpen(data) {
|
rssFeedOpen(data) {
|
||||||
if (data.libraryItemId === this.libraryItemId) {
|
if (data.entityId === this.libraryItemId) {
|
||||||
console.log('RSS Feed Opened', data)
|
console.log('RSS Feed Opened', data)
|
||||||
this.rssFeedUrl = data.feedUrl
|
this.rssFeedUrl = data.feedUrl
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rssFeedClosed(data) {
|
rssFeedClosed(data) {
|
||||||
if (data.libraryItemId === this.libraryItemId) {
|
if (data.entityId === this.libraryItemId) {
|
||||||
console.log('RSS Feed Closed', data)
|
console.log('RSS Feed Closed', data)
|
||||||
this.rssFeedUrl = null
|
this.rssFeedUrl = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,26 @@
|
|||||||
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<app-book-shelf-toolbar page="podcast-search" />
|
<app-book-shelf-toolbar page="podcast-search" />
|
||||||
|
|
||||||
<div class="w-full h-full overflow-y-auto p-12 relative">
|
<div class="w-full h-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
|
||||||
<div class="w-full max-w-4xl mx-auto flex">
|
<div class="w-full max-w-4xl mx-auto flex">
|
||||||
<form @submit.prevent="submit" class="flex flex-grow">
|
<form @submit.prevent="submit" class="flex flex-grow">
|
||||||
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2" />
|
<ui-text-input v-model="searchInput" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" />
|
||||||
<ui-btn type="submit" :disabled="processing">Submit</ui-btn>
|
<ui-btn type="submit" :disabled="processing" class="hidden md:block">Submit</ui-btn>
|
||||||
|
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>Submit</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
<ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="mx-2" @change="opmlFileUpload"> Upload OPML File </ui-file-input>
|
<ui-file-input ref="fileInput" :accept="'.opml, .txt'" class="ml-2" @change="opmlFileUpload"> Upload OPML File </ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full max-w-3xl mx-auto py-4">
|
<div class="w-full max-w-3xl mx-auto py-4">
|
||||||
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No podcasts found</p>
|
<p v-if="termSearched && !results.length && !processing" class="text-center text-xl">No podcasts found</p>
|
||||||
<template v-for="podcast in results">
|
<template v-for="podcast in results">
|
||||||
<div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)">
|
<div :key="podcast.id" class="flex p-1 hover:bg-primary hover:bg-opacity-25 cursor-pointer" @click="selectPodcast(podcast)">
|
||||||
<div class="w-24 min-w-24 h-24 bg-primary">
|
<div class="w-20 min-w-20 h-20 md:w-24 md:min-w-24 md:h-24 bg-primary">
|
||||||
<img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" />
|
<img v-if="podcast.cover" :src="podcast.cover" class="h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow pl-4 max-w-2xl">
|
<div class="flex-grow pl-4 max-w-2xl">
|
||||||
<a :href="podcast.pageUrl" class="text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
|
||||||
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">by {{ podcast.artistName }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} Episodes</p>
|
<p class="text-xs text-gray-400 leading-5">{{ podcast.trackCount }} Episodes</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -124,9 +124,10 @@ export default {
|
|||||||
|
|
||||||
location.reload()
|
location.reload()
|
||||||
},
|
},
|
||||||
setUser({ user, userDefaultLibraryId, serverSettings, Source }) {
|
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
||||||
this.$store.commit('setServerSettings', serverSettings)
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
this.$store.commit('setSource', Source)
|
this.$store.commit('setSource', Source)
|
||||||
|
this.$store.commit('feeds/setFeeds', feeds)
|
||||||
|
|
||||||
if (serverSettings.chromecastEnabled) {
|
if (serverSettings.chromecastEnabled) {
|
||||||
console.log('Chromecast enabled import script')
|
console.log('Chromecast enabled import script')
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-8 text-center">
|
<div class="pt-8 text-center">
|
||||||
<p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
|
<p class="text-xs text-white text-opacity-50 font-mono mb-4"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
|
||||||
|
|
||||||
|
<p class="text-sm text-white text-opacity-70">Folders with media files will be treated as separate library items. <span v-if="selectedLibraryMediaType === 'book'">If uploading only audio files then each audio file will be treated as a separate audiobook.</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Item list header -->
|
<!-- Item list header -->
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default class PlayerHandler {
|
|||||||
this.isHlsTranscode = false
|
this.isHlsTranscode = false
|
||||||
this.isVideo = false
|
this.isVideo = false
|
||||||
this.currentSessionId = null
|
this.currentSessionId = null
|
||||||
|
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
|
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
@@ -51,12 +52,13 @@ export default class PlayerHandler {
|
|||||||
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
load(libraryItem, episodeId, playWhenReady, playbackRate) {
|
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = playbackRate
|
||||||
this.isVideo = libraryItem.mediaType === 'video'
|
this.isVideo = libraryItem.mediaType === 'video'
|
||||||
|
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
|
||||||
|
|
||||||
if (!this.player) this.switchPlayer(playWhenReady)
|
if (!this.player) this.switchPlayer(playWhenReady)
|
||||||
else this.prepare()
|
else this.prepare()
|
||||||
@@ -142,11 +144,13 @@ export default class PlayerHandler {
|
|||||||
} else {
|
} else {
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
}
|
}
|
||||||
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
|
if (this.player) {
|
||||||
this.ctx.setDuration(this.getDuration())
|
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
|
||||||
}
|
this.ctx.setDuration(this.getDuration())
|
||||||
if (this.playerState !== 'LOADING') {
|
}
|
||||||
this.ctx.setCurrentTime(this.player.getCurrentTime())
|
if (this.playerState !== 'LOADING') {
|
||||||
|
this.ctx.setCurrentTime(this.player.getCurrentTime())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ctx.setPlaying(this.playerState === 'PLAYING')
|
this.ctx.setPlaying(this.playerState === 'PLAYING')
|
||||||
@@ -177,18 +181,20 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
||||||
|
if (!this.player) this.switchPlayer() // Must set player first for open sessions
|
||||||
|
|
||||||
this.libraryItem = session.libraryItem
|
this.libraryItem = session.libraryItem
|
||||||
this.isVideo = session.libraryItem.mediaType === 'video'
|
this.isVideo = session.libraryItem.mediaType === 'video'
|
||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = playbackRate
|
||||||
|
this.startTimeOverride = undefined
|
||||||
|
|
||||||
if (!this.player) this.switchPlayer()
|
|
||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareSession(session) {
|
prepareSession(session) {
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
this.startTime = session.currentTime
|
this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime
|
||||||
this.currentSessionId = session.id
|
this.currentSessionId = session.id
|
||||||
this.displayTitle = session.displayTitle
|
this.displayTitle = session.displayTitle
|
||||||
this.displayAuthor = session.displayAuthor
|
this.displayAuthor = session.displayAuthor
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default (ctx) => {
|
|||||||
var castContext = cast.framework.CastContext.getInstance()
|
var castContext = cast.framework.CastContext.getInstance()
|
||||||
castContext.setOptions({
|
castContext.setOptions({
|
||||||
receiverApplicationId: process.env.chromecastReceiver,
|
receiverApplicationId: process.env.chromecastReceiver,
|
||||||
autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
|
autoJoinPolicy: chrome.cast ? chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED : null
|
||||||
});
|
});
|
||||||
|
|
||||||
castContext.addEventListener(
|
castContext.addEventListener(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const SupportedFileTypes = {
|
const SupportedFileTypes = {
|
||||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
image: ['png', 'jpg', 'jpeg', 'webp'],
|
||||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff', 'wav'],
|
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav'],
|
||||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
|||||||
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str.replace('.', '-') // replace a dot by a dash
|
str = str.replace('.', '-') // replace a dot by a dash
|
||||||
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
||||||
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
||||||
.replace(/-+/g, '-') // collapse dashes
|
.replace(/-+/g, '-') // collapse dashes
|
||||||
@@ -204,7 +204,6 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function xmlToJson(xml) {
|
function xmlToJson(xml) {
|
||||||
const json = {};
|
const json = {};
|
||||||
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
||||||
|
|||||||
@@ -47,8 +47,13 @@ export async function checkForUpdate() {
|
|||||||
largestVer = verObj
|
largestVer = verObj
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verObj.version == currVerObj.version) {
|
||||||
|
currVerObj.changelog = release.body
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
if (!largestVer) {
|
if (!largestVer) {
|
||||||
console.error('No valid version tags to compare with')
|
console.error('No valid version tags to compare with')
|
||||||
@@ -59,6 +64,7 @@ export async function checkForUpdate() {
|
|||||||
hasUpdate: largestVer.total > currVerObj.total,
|
hasUpdate: largestVer.total > currVerObj.total,
|
||||||
latestVersion: largestVer.version,
|
latestVersion: largestVer.version,
|
||||||
githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${largestVer.version}`,
|
githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${largestVer.version}`,
|
||||||
currentVersion: currVerObj.version
|
currentVersion: currVerObj.version,
|
||||||
|
currentVersionChangelog: currVerObj.changelog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 1235.7 1235.4" style="enable-background:new 0 0 6702.7 1277.4;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill:url(#SVGID_1_);}
|
||||||
|
.st2{fill:#C9C9C9;}
|
||||||
|
.st3{font-family:'GentiumBookBasic';}
|
||||||
|
.st4{font-size:800px;}
|
||||||
|
.st5{fill:#474747;}
|
||||||
|
</style>
|
||||||
|
<title>bgAsset 6</title>
|
||||||
|
<g id="Layer_2_1_">
|
||||||
|
<g id="Layer_2-2">
|
||||||
|
<g id="Layer_4">
|
||||||
|
<g id="Layer_5">
|
||||||
|
<circle class="st0" cx="618.6" cy="618.6" r="618.6"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="617.37" y1="1257.3" x2="617.37" y2="61.4399" gradientTransform="matrix(1 0 0 -1 0 1278)">
|
||||||
|
<stop offset="0.32" style="stop-color:#CD9D49"/>
|
||||||
|
<stop offset="0.99" style="stop-color:#875D27"/>
|
||||||
|
</linearGradient>
|
||||||
|
<circle class="st1" cx="617.4" cy="618.6" r="597.9"/>
|
||||||
|
</g>
|
||||||
|
<path class="st0" d="M1005.6,574.1c-4.8-4-12.4-10-22.6-17v-79.2c0-201.9-163.7-365.6-365.6-365.6l0,0
|
||||||
|
c-201.9,0-365.6,163.7-365.6,365.6v79.2c-10.2,7-17.7,13-22.6,17c-4.1,3.4-6.5,8.5-6.5,13.9v94.9c0,5.4,2.4,10.5,6.5,14
|
||||||
|
c11.3,9.4,37.2,29.1,77.5,49.3v9.2c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45V527.8c0-24.9-16-45-35.8-45l0,0
|
||||||
|
c-19,0-34.5,18.5-35.8,41.9h-0.1v-46.9c0-171.6,139.1-310.7,310.7-310.7l0,0C789,167.2,928,306.3,928,477.9v46.9H928
|
||||||
|
c-1.3-23.4-16.8-41.9-35.8-41.9l0,0c-19.8,0-35.8,20.2-35.8,45v227.6c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45v-9.2
|
||||||
|
c40.3-20.2,66.2-39.9,77.5-49.3c4.2-3.5,6.5-8.6,6.5-14V588C1012.1,582.6,1009.7,577.5,1005.6,574.1z"/>
|
||||||
|
<path class="st0" d="M489.9,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
||||||
|
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L489.9,969.7z M418.2,514.6h98.7v10.3h-98.7V514.6z"/>
|
||||||
|
<path class="st0" d="M639.7,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3H595c-23.9,0-43.3,19.4-43.3,43.3
|
||||||
|
v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/>
|
||||||
|
<path class="st0" d="M789.6,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
||||||
|
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L789.6,969.7z M717.9,514.6h98.7v10.3h-98.7V514.6z"/>
|
||||||
|
<path class="st0" d="M327.1,984.7h580.5c18,0,32.6,14.6,32.6,32.6v0c0,18-14.6,32.6-32.6,32.6H327.1c-18,0-32.6-14.6-32.6-32.6v0
|
||||||
|
C294.5,999.3,309.1,984.7,327.1,984.7z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
# License information
|
||||||
|
|
||||||
|
## Contribution License Agreement
|
||||||
|
|
||||||
|
If you contribute code to this project, you are implicitly allowing your code
|
||||||
|
to be distributed under the MIT license. You are also implicitly verifying that
|
||||||
|
all code is your original work. `</legalese>`
|
||||||
|
|
||||||
|
## Marked
|
||||||
|
|
||||||
|
Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/)
|
||||||
|
Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
## Markdown
|
||||||
|
|
||||||
|
Copyright © 2004, John Gruber
|
||||||
|
http://daringfireball.net/
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
export const state = () => ({
|
||||||
|
feeds: []
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getFeedForItem: state => id => {
|
||||||
|
return state.feeds.find(feed => feed.id === id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
addFeed(state, feed) {
|
||||||
|
var index = state.feeds.findIndex(f => f.id === feed.id)
|
||||||
|
if (index >= 0) state.feeds.splice(index, 1, feed)
|
||||||
|
else state.feeds.push(feed)
|
||||||
|
},
|
||||||
|
removeFeed(state, feed) {
|
||||||
|
state.feeds = state.feeds.filter(f => f.id !== feed.id)
|
||||||
|
},
|
||||||
|
setFeeds(state, feeds) {
|
||||||
|
state.feeds = feeds || []
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-2
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isMobileLandscape: false,
|
isMobileLandscape: false,
|
||||||
@@ -7,12 +6,28 @@ export const state = () => ({
|
|||||||
showEditCollectionModal: false,
|
showEditCollectionModal: false,
|
||||||
showEditPodcastEpisode: false,
|
showEditPodcastEpisode: false,
|
||||||
showViewPodcastEpisodeModal: false,
|
showViewPodcastEpisodeModal: false,
|
||||||
|
showConfirmPrompt: false,
|
||||||
|
confirmPromptOptions: null,
|
||||||
showEditAuthorModal: false,
|
showEditAuthorModal: false,
|
||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
selectedCollection: null,
|
selectedCollection: null,
|
||||||
selectedAuthor: null,
|
selectedAuthor: null,
|
||||||
isCasting: false, // Actively casting
|
isCasting: false, // Actively casting
|
||||||
isChromecastInitialized: false // Script loaded
|
isChromecastInitialized: false, // Script loaded
|
||||||
|
dateFormats: [
|
||||||
|
{
|
||||||
|
text: 'MM/DD/YYYY',
|
||||||
|
value: 'MM/dd/yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'DD/MM/YYYY',
|
||||||
|
value: 'dd/MM/yyyy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'YYYY-MM-DD',
|
||||||
|
value: 'yyyy-MM-dd'
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -56,6 +71,13 @@ export const mutations = {
|
|||||||
setShowViewPodcastEpisodeModal(state, val) {
|
setShowViewPodcastEpisodeModal(state, val) {
|
||||||
state.showViewPodcastEpisodeModal = val
|
state.showViewPodcastEpisodeModal = val
|
||||||
},
|
},
|
||||||
|
setShowConfirmPrompt(state, val) {
|
||||||
|
state.showConfirmPrompt = val
|
||||||
|
},
|
||||||
|
setConfirmPrompt(state, options) {
|
||||||
|
state.confirmPromptOptions = options
|
||||||
|
state.showConfirmPrompt = true
|
||||||
|
},
|
||||||
setEditCollection(state, collection) {
|
setEditCollection(state, collection) {
|
||||||
state.selectedCollection = collection
|
state.selectedCollection = collection
|
||||||
state.showEditCollectionModal = true
|
state.showEditCollectionModal = true
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ export const getters = {
|
|||||||
getLibraryItemIdStreaming: state => {
|
getLibraryItemIdStreaming: state => {
|
||||||
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
||||||
},
|
},
|
||||||
getIsEpisodeStreaming: state => (libraryItemId, episodeId) => {
|
getIsMediaStreaming: state => (libraryItemId, episodeId) => {
|
||||||
if (!state.streamLibraryItem) return null
|
if (!state.streamLibraryItem) return null
|
||||||
|
if (!episodeId) return state.streamLibraryItem.id == libraryItemId
|
||||||
return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId
|
return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ export const mutations = {
|
|||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setUserToken(state, token) {
|
||||||
|
state.user.token = token
|
||||||
|
localStorage.setItem('token', user.token)
|
||||||
|
},
|
||||||
updateMediaProgress(state, { id, data }) {
|
updateMediaProgress(state, { id, data }) {
|
||||||
if (!state.user) return
|
if (!state.user) return
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user