mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02:00
Compare commits
101 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 |
@@ -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
|
||||||
|
|||||||
+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,14 +3,14 @@
|
|||||||
<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-8 h-8 mr-8 sm:w-12 sm:h-12 sm: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="" />
|
<controls-global-search v-if="currentLibrary" class="" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|||||||
@@ -11,12 +11,14 @@
|
|||||||
|
|
||||||
<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() {
|
||||||
@@ -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>
|
||||||
@@ -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,20 @@
|
|||||||
<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 class="flex items-center">
|
|
||||||
<h1 class="text-base">{{ book.title }}</h1>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<p>{{ book.publishedYear }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm">{{ book.author }}</p>
|
</div>
|
||||||
|
<div v-if="!isPodcast" class="px-2 md:px-4 flex-grow">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<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-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">
|
<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">
|
<p class="leading-3 text-xs text-gray-400">
|
||||||
@@ -25,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>
|
||||||
@@ -63,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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -249,14 +253,14 @@ export default {
|
|||||||
},
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.recentEpisode) return this.recentEpisode.title
|
if (this.recentEpisode) return this.recentEpisode.title
|
||||||
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix) {
|
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
||||||
return this.mediaMetadata.titleIgnorePrefix
|
if (this.collapsedSeries) return ignorePrefix ? this.collapsedSeries.nameIgnorePrefix : this.collapsedSeries.name
|
||||||
}
|
return ignorePrefix ? this.mediaMetadata.titleIgnorePrefix : this.title
|
||||||
return this.title
|
|
||||||
},
|
},
|
||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
|
if (this.collapsedSeries) return ''
|
||||||
if (this.isAuthorBookshelfView) {
|
if (this.isAuthorBookshelfView) {
|
||||||
return this.mediaMetadata.publishedYear || ''
|
return this.mediaMetadata.publishedYear || ''
|
||||||
}
|
}
|
||||||
@@ -264,6 +268,7 @@ export default {
|
|||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
|
if (this.collapsedSeries) return null
|
||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
||||||
@@ -443,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: {
|
||||||
@@ -499,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="sm:w-80 w-full sm: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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!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-24 sm:w-28 md:w-40 p-1">
|
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||||
<ui-text-input-with-label 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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -116,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
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -190,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 {
|
||||||
@@ -210,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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,6 +363,10 @@ 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) {
|
if (match) {
|
||||||
@@ -497,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>
|
||||||
@@ -226,6 +226,13 @@ export default {
|
|||||||
})
|
})
|
||||||
this.updateTimestamp()
|
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)
|
||||||
},
|
},
|
||||||
@@ -286,7 +293,10 @@ export default {
|
|||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
|
||||||
|
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)
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
this.$emit('setPlaybackRate', this.playbackRate)
|
this.$emit('setPlaybackRate', this.playbackRate)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,7 +6,7 @@
|
|||||||
<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" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<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 text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
||||||
|
|
||||||
<!-- For mobile -->
|
<!-- For mobile -->
|
||||||
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
||||||
@@ -105,7 +105,7 @@ export default {
|
|||||||
},
|
},
|
||||||
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')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 sm:w-36 h-8 px-1.5" 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="w-10 sm:w-36 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">
|
<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 justify-center sm:justify-start">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-2" />
|
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
<span class="hidden sm:block">{{ 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-36 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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)
|
||||||
@@ -197,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)
|
||||||
@@ -341,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
|
||||||
@@ -417,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)
|
||||||
},
|
},
|
||||||
@@ -521,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
|
||||||
|
|||||||
@@ -108,15 +108,15 @@ 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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+9
-16
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.23",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.22",
|
"version": "2.1.2",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
"@nuxtjs/tailwindcss": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"postcss": "^8.3.6"
|
"postcss": "^8.3.6",
|
||||||
|
"tailwindcss": "^3.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@@ -1851,7 +1852,7 @@
|
|||||||
"version": "2.6.12",
|
"version": "2.6.12",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
|
||||||
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
"integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
|
||||||
"deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.",
|
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/builder": {
|
"node_modules/@nuxt/builder": {
|
||||||
@@ -5261,6 +5262,7 @@
|
|||||||
"version": "3.22.5",
|
"version": "3.22.5",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.5.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.22.5.tgz",
|
||||||
"integrity": "sha512-VP/xYuvJ0MJWRAobcmQ8F2H6Bsn+s7zqAAjFaHGBMc5AQm7zaelhD1LGduFn2EehEcQcU+br6t+fwbpQ5d1ZWA==",
|
"integrity": "sha512-VP/xYuvJ0MJWRAobcmQ8F2H6Bsn+s7zqAAjFaHGBMc5AQm7zaelhD1LGduFn2EehEcQcU+br6t+fwbpQ5d1ZWA==",
|
||||||
|
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -9854,7 +9856,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
@@ -11447,7 +11448,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
|
||||||
"integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
|
"integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"camelcase-css": "^2.0.1"
|
"camelcase-css": "^2.0.1"
|
||||||
},
|
},
|
||||||
@@ -14492,7 +14492,8 @@
|
|||||||
"node_modules/stable": {
|
"node_modules/stable": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||||
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="
|
"integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
|
||||||
|
"deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility"
|
||||||
},
|
},
|
||||||
"node_modules/stack-trace": {
|
"node_modules/stack-trace": {
|
||||||
"version": "0.0.10",
|
"version": "0.0.10",
|
||||||
@@ -14933,7 +14934,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz",
|
||||||
"integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==",
|
"integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
@@ -14974,7 +14974,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
},
|
},
|
||||||
@@ -14987,7 +14986,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
|
||||||
"integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
|
"integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.0.0",
|
"postcss-value-parser": "^4.0.0",
|
||||||
"read-cache": "^1.0.0",
|
"read-cache": "^1.0.0",
|
||||||
@@ -25025,8 +25023,7 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"object-inspect": {
|
"object-inspect": {
|
||||||
"version": "1.12.0",
|
"version": "1.12.0",
|
||||||
@@ -26229,7 +26226,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
|
||||||
"integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
|
"integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"camelcase-css": "^2.0.1"
|
"camelcase-css": "^2.0.1"
|
||||||
}
|
}
|
||||||
@@ -28918,7 +28914,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz",
|
||||||
"integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==",
|
"integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"arg": "^5.0.2",
|
"arg": "^5.0.2",
|
||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
@@ -28949,7 +28944,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
}
|
}
|
||||||
@@ -28959,7 +28953,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz",
|
||||||
"integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
|
"integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"postcss-value-parser": "^4.0.0",
|
"postcss-value-parser": "^4.0.0",
|
||||||
"read-cache": "^1.0.0",
|
"read-cache": "^1.0.0",
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.23",
|
"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": {
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
"@nuxtjs/tailwindcss": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"postcss": "^8.3.6"
|
"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() {
|
||||||
|
|||||||
@@ -157,6 +157,16 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</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">
|
<div class="pt-4">
|
||||||
<h2 class="font-semibold">Experimental Features</h2>
|
<h2 class="font-semibold">Experimental Features</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +194,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@@ -268,7 +288,9 @@ export default {
|
|||||||
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 just for use by 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'
|
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
|
||||||
}
|
}
|
||||||
@@ -300,6 +322,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateScannerMaxThreads(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) {
|
||||||
this.$toast.error('Must have at least 1 prefix')
|
this.$toast.error('Must have at least 1 prefix')
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
|
<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-8">
|
<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>
|
||||||
|
|||||||
@@ -161,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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,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">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -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)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,7 +227,8 @@ export default {
|
|||||||
podcastFeedEpisodes: [],
|
podcastFeedEpisodes: [],
|
||||||
episodesDownloading: [],
|
episodesDownloading: [],
|
||||||
episodeDownloadsQueued: [],
|
episodeDownloadsQueued: [],
|
||||||
showRssFeedModal: false
|
showRssFeedModal: false,
|
||||||
|
showBookmarksModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -296,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 || []
|
||||||
},
|
},
|
||||||
@@ -389,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() {
|
||||||
@@ -409,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
|
||||||
@@ -453,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
|
||||||
}
|
}
|
||||||
@@ -470,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) => {
|
||||||
@@ -483,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() {
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -183,13 +187,14 @@ export default class PlayerHandler {
|
|||||||
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
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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 || []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ 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,
|
||||||
@@ -69,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) {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
const defaultTheme = require('tailwindcss/defaultTheme')
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
purge: {
|
purge: {
|
||||||
content: [
|
content: [
|
||||||
@@ -16,7 +14,8 @@ module.exports = {
|
|||||||
'text-green-500',
|
'text-green-500',
|
||||||
'py-1.5',
|
'py-1.5',
|
||||||
'bg-info',
|
'bg-info',
|
||||||
'px-1.5'
|
'px-1.5',
|
||||||
|
'min-w-5'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
@@ -38,11 +37,14 @@ module.exports = {
|
|||||||
'32': '8rem',
|
'32': '8rem',
|
||||||
'40': '10rem',
|
'40': '10rem',
|
||||||
'48': '12rem',
|
'48': '12rem',
|
||||||
|
'52': '13rem',
|
||||||
'64': '16rem',
|
'64': '16rem',
|
||||||
'80': '20rem'
|
'80': '20rem'
|
||||||
},
|
},
|
||||||
minWidth: {
|
minWidth: {
|
||||||
|
'5': '1.25rem',
|
||||||
'6': '1.5rem',
|
'6': '1.5rem',
|
||||||
|
'10': '2.5rem',
|
||||||
'12': '3rem',
|
'12': '3rem',
|
||||||
'16': '4rem',
|
'16': '4rem',
|
||||||
'20': '5rem',
|
'20': '5rem',
|
||||||
@@ -80,8 +82,8 @@ module.exports = {
|
|||||||
none: 'none'
|
none: 'none'
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Source Sans Pro', ...defaultTheme.fontFamily.sans],
|
sans: ['Source Sans Pro'],
|
||||||
mono: ['Ubuntu Mono', ...defaultTheme.fontFamily.mono],
|
mono: ['Ubuntu Mono'],
|
||||||
book: ['Gentium Book Basic', 'serif']
|
book: ['Gentium Book Basic', 'serif']
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
@@ -89,7 +91,8 @@ module.exports = {
|
|||||||
'2.5xl': '1.6875rem'
|
'2.5xl': '1.6875rem'
|
||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
'50': 50
|
'50': 50,
|
||||||
|
'60': 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-3
@@ -7,6 +7,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 13378:80
|
- 13378:80
|
||||||
volumes:
|
volumes:
|
||||||
- /audiobooks:/audiobooks
|
- ./audiobooks:/audiobooks
|
||||||
- /metadata:/metadata
|
- ./metadata:/metadata
|
||||||
- /config:/config
|
- ./config:/config
|
||||||
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611'
|
|
||||||
const server = require('./server/Server')
|
const server = require('./server/Server')
|
||||||
global.appRoot = __dirname
|
global.appRoot = __dirname
|
||||||
|
|
||||||
|
|||||||
Generated
+4
-1783
File diff suppressed because it is too large
Load Diff
+2
-14
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.23",
|
"version": "2.1.2",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,22 +30,10 @@
|
|||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver": "^5.3.0",
|
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"command-line-args": "^5.2.0",
|
|
||||||
"date-and-time": "^2.3.1",
|
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-fileupload": "^1.2.1",
|
"graceful-fs": "^4.2.10",
|
||||||
"express-rate-limit": "^5.3.0",
|
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
|
||||||
"fs-extra": "^10.0.0",
|
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
|
||||||
"libgen": "^2.1.0",
|
|
||||||
"node-ffprobe": "^3.0.0",
|
|
||||||
"node-stream-zip": "^1.15.0",
|
|
||||||
"recursive-readdir-async": "^1.1.8",
|
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.4.1",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const optionDefinitions = [
|
|||||||
{ name: 'source', alias: 's', type: String }
|
{ name: 'source', alias: 's', type: String }
|
||||||
]
|
]
|
||||||
|
|
||||||
const commandLineArgs = require('command-line-args')
|
const commandLineArgs = require('./server/libs/commandLineArgs')
|
||||||
const options = commandLineArgs(optionDefinitions)
|
const options = commandLineArgs(optionDefinitions)
|
||||||
|
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
|||||||
+31
-10
@@ -1,5 +1,5 @@
|
|||||||
const bcrypt = require('bcryptjs')
|
const bcrypt = require('./libs/bcryptjs')
|
||||||
const jwt = require('jsonwebtoken')
|
const jwt = require('./libs/jsonwebtoken')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
class Auth {
|
class Auth {
|
||||||
@@ -31,6 +31,26 @@ class Auth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initTokenSecret() {
|
||||||
|
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
||||||
|
Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`)
|
||||||
|
this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[Auth] Setting token secret - using random bytes`)
|
||||||
|
this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||||
|
}
|
||||||
|
await this.db.updateServerSettings()
|
||||||
|
|
||||||
|
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
||||||
|
if (this.db.users.length) {
|
||||||
|
for (const user of this.db.users) {
|
||||||
|
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
||||||
|
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
|
||||||
|
}
|
||||||
|
await this.db.updateEntities('user', this.db.users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async authMiddleware(req, res, next) {
|
async authMiddleware(req, res, next) {
|
||||||
var token = null
|
var token = null
|
||||||
|
|
||||||
@@ -74,7 +94,7 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateAccessToken(payload) {
|
generateAccessToken(payload) {
|
||||||
return jwt.sign(payload, process.env.TOKEN_SECRET);
|
return jwt.sign(payload, global.ServerSettings.tokenSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateUser(token) {
|
authenticateUser(token) {
|
||||||
@@ -83,27 +103,28 @@ class Auth {
|
|||||||
|
|
||||||
verifyToken(token) {
|
verifyToken(token) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(token, process.env.TOKEN_SECRET, (err, payload) => {
|
jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => {
|
||||||
if (!payload || err) {
|
if (!payload || err) {
|
||||||
Logger.error('JWT Verify Token Failed', err)
|
Logger.error('JWT Verify Token Failed', err)
|
||||||
return resolve(null)
|
return resolve(null)
|
||||||
}
|
}
|
||||||
var user = this.users.find(u => u.id === payload.userId)
|
var user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
||||||
resolve(user || null)
|
resolve(user || null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserLoginResponsePayload(user) {
|
getUserLoginResponsePayload(user, feeds) {
|
||||||
return {
|
return {
|
||||||
user: user.toJSONForBrowser(),
|
user: user.toJSONForBrowser(),
|
||||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
||||||
|
feeds,
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(req, res) {
|
async login(req, res, feeds) {
|
||||||
var username = (req.body.username || '').toLowerCase()
|
var username = (req.body.username || '').toLowerCase()
|
||||||
var password = req.body.password || ''
|
var password = req.body.password || ''
|
||||||
|
|
||||||
@@ -122,14 +143,14 @@ class Auth {
|
|||||||
if (password) {
|
if (password) {
|
||||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
return res.json(this.getUserLoginResponsePayload(user))
|
return res.json(this.getUserLoginResponsePayload(user, feeds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check password match
|
// Check password match
|
||||||
var compare = await bcrypt.compare(password, user.pash)
|
var compare = await bcrypt.compare(password, user.pash)
|
||||||
if (compare) {
|
if (compare) {
|
||||||
res.json(this.getUserLoginResponsePayload(user))
|
res.json(this.getUserLoginResponsePayload(user, feeds))
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
|
|||||||
+19
-2
@@ -414,6 +414,23 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeEntities(entityName, selectFunc) {
|
||||||
|
var entityDb = this.getEntityDb(entityName)
|
||||||
|
return entityDb.delete(selectFunc).then((results) => {
|
||||||
|
Logger.debug(`[DB] Deleted entities ${entityName}: ${results.deleted}`)
|
||||||
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
|
if (this[arrayKey]) {
|
||||||
|
this[arrayKey] = this[arrayKey].filter(e => {
|
||||||
|
return !selectFunc(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return results.deleted
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[DB] Remove entities ${entityName} Failed: ${error}`)
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
recreateLibraryItemsDb() {
|
recreateLibraryItemsDb() {
|
||||||
return this.libraryItemsDb.drop().then((results) => {
|
return this.libraryItemsDb.drop().then((results) => {
|
||||||
Logger.info(`[DB] Dropped library items db`, results)
|
Logger.info(`[DB] Dropped library items db`, results)
|
||||||
@@ -426,8 +443,8 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllSessions() {
|
getAllSessions(selectFunc = () => true) {
|
||||||
return this.sessionsDb.select(() => true).then((results) => {
|
return this.sessionsDb.select(selectFunc).then((results) => {
|
||||||
return results.data || []
|
return results.data || []
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
Logger.error('[Db] Failed to select sessions', error)
|
Logger.error('[Db] Failed to select sessions', error)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const date = require('date-and-time')
|
const date = require('./libs/dateAndTime')
|
||||||
const { LogLevel } = require('./utils/constants')
|
const { LogLevel } = require('./utils/constants')
|
||||||
|
|
||||||
class Logger {
|
class Logger {
|
||||||
|
|||||||
+18
-18
@@ -2,9 +2,9 @@ const Path = require('path')
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
const SocketIO = require('socket.io')
|
const SocketIO = require('socket.io')
|
||||||
const fs = require('fs-extra')
|
const fs = require('./libs/fsExtra')
|
||||||
const fileUpload = require('express-fileupload')
|
const fileUpload = require('./libs/expressFileupload')
|
||||||
const rateLimit = require('express-rate-limit')
|
const rateLimit = require('./libs/expressRateLimit')
|
||||||
|
|
||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
|
|
||||||
@@ -136,8 +136,14 @@ class Server {
|
|||||||
await this.db.init()
|
await this.db.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create token secret if does not exist (Added v2.1.0)
|
||||||
|
if (!this.db.serverSettings.tokenSecret) {
|
||||||
|
await this.auth.initTokenSecret()
|
||||||
|
}
|
||||||
|
|
||||||
await this.checkUserMediaProgress() // Remove invalid user item progress
|
await this.checkUserMediaProgress() // Remove invalid user item progress
|
||||||
await this.purgeMetadata() // Remove metadata folders without library item
|
await this.purgeMetadata() // Remove metadata folders without library item
|
||||||
|
await this.playbackSessionManager.removeInvalidSessions()
|
||||||
await this.cacheManager.ensureCachePaths()
|
await this.cacheManager.ensureCachePaths()
|
||||||
await this.abMergeManager.ensureDownloadDirPath()
|
await this.abMergeManager.ensureDownloadDirPath()
|
||||||
|
|
||||||
@@ -171,7 +177,6 @@ class Server {
|
|||||||
const distPath = Path.join(global.appRoot, '/client/dist')
|
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||||
app.use(express.static(distPath))
|
app.use(express.static(distPath))
|
||||||
|
|
||||||
|
|
||||||
// Metadata folder static path
|
// Metadata folder static path
|
||||||
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
app.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||||
|
|
||||||
@@ -225,7 +230,7 @@ class Server {
|
|||||||
]
|
]
|
||||||
dyanimicRoutes.forEach((route) => app.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
dyanimicRoutes.forEach((route) => app.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||||
|
|
||||||
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res, this.rssFeedManager.feedsArray))
|
||||||
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||||
app.post('/init', (req, res) => {
|
app.post('/init', (req, res) => {
|
||||||
if (this.db.hasRootUser) {
|
if (this.db.hasRootUser) {
|
||||||
@@ -247,9 +252,10 @@ class Server {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
})
|
})
|
||||||
app.get('/ping', (req, res) => {
|
app.get('/ping', (req, res) => {
|
||||||
Logger.info('Recieved ping')
|
Logger.info('Received ping')
|
||||||
res.json({ success: true })
|
res.json({ success: true })
|
||||||
})
|
})
|
||||||
|
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||||
|
|
||||||
this.server.listen(this.Port, this.Host, () => {
|
this.server.listen(this.Port, this.Host, () => {
|
||||||
Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
||||||
@@ -278,6 +284,7 @@ class Server {
|
|||||||
|
|
||||||
// Logs
|
// Logs
|
||||||
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
|
||||||
|
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
|
||||||
socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket))
|
socket.on('fetch_daily_logs', () => this.logManager.socketRequestDailyLogs(socket))
|
||||||
|
|
||||||
socket.on('ping', () => {
|
socket.on('ping', () => {
|
||||||
@@ -287,21 +294,21 @@ class Server {
|
|||||||
socket.emit('pong')
|
socket.emit('pong')
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', (reason) => {
|
||||||
Logger.removeSocketListener(socket.id)
|
Logger.removeSocketListener(socket.id)
|
||||||
|
|
||||||
var _client = this.clients[socket.id]
|
var _client = this.clients[socket.id]
|
||||||
if (!_client) {
|
if (!_client) {
|
||||||
Logger.warn('[Server] Socket disconnect, no client ' + socket.id)
|
Logger.warn(`[Server] Socket ${socket.id} disconnect, no client (Reason: ${reason})`)
|
||||||
} else if (!_client.user) {
|
} else if (!_client.user) {
|
||||||
Logger.info('[Server] Unauth socket disconnected ' + socket.id)
|
Logger.info(`[Server] Unauth socket ${socket.id} disconnected (Reason: ${reason})`)
|
||||||
delete this.clients[socket.id]
|
delete this.clients[socket.id]
|
||||||
} else {
|
} else {
|
||||||
Logger.debug('[Server] User Offline ' + _client.user.username)
|
Logger.debug('[Server] User Offline ' + _client.user.username)
|
||||||
this.io.emit('user_offline', _client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
this.io.emit('user_offline', _client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||||
|
|
||||||
const disconnectTime = Date.now() - _client.connected_at
|
const disconnectTime = Date.now() - _client.connected_at
|
||||||
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms`)
|
Logger.info(`[Server] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
|
||||||
delete this.clients[socket.id]
|
delete this.clients[socket.id]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -313,7 +320,7 @@ class Server {
|
|||||||
const newRoot = req.body.newRoot
|
const newRoot = req.body.newRoot
|
||||||
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
||||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||||
let rootToken = await this.auth.generateAccessToken({ userId: 'root' })
|
let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username })
|
||||||
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@@ -458,8 +465,6 @@ class Server {
|
|||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
|
|
||||||
const initialPayload = {
|
const initialPayload = {
|
||||||
// TODO: this is sent with user auth now, update mobile app to use that then remove this
|
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
|
||||||
metadataPath: global.MetadataPath,
|
metadataPath: global.MetadataPath,
|
||||||
configPath: global.ConfigPath,
|
configPath: global.ConfigPath,
|
||||||
user: client.user.toJSONForBrowser(),
|
user: client.user.toJSONForBrowser(),
|
||||||
@@ -471,11 +476,6 @@ class Server {
|
|||||||
initialPayload.usersOnline = this.usersOnline
|
initialPayload.usersOnline = this.usersOnline
|
||||||
}
|
}
|
||||||
client.socket.emit('init', initialPayload)
|
client.socket.emit('init', initialPayload)
|
||||||
|
|
||||||
// Setup log listener for root user
|
|
||||||
if (user.type === 'root') {
|
|
||||||
Logger.addSocketListener(socket, this.db.serverSettings.logLevel || 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
|
|||||||
@@ -82,31 +82,59 @@ class AuthorController {
|
|||||||
|
|
||||||
var authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
var authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||||
|
|
||||||
var hasUpdated = req.author.update(payload)
|
// Check if author name matches another author and merge the authors
|
||||||
|
var existingAuthor = authorNameUpdate ? this.db.authors.find(au => au.id !== req.author.id && payload.name === au.name) : false
|
||||||
if (hasUpdated) {
|
if (existingAuthor) {
|
||||||
if (authorNameUpdate) { // Update author name on all books
|
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||||
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
itemsWithAuthor.forEach(libraryItem => { // Replace old author with merging author for each book
|
||||||
itemsWithAuthor.forEach(libraryItem => {
|
libraryItem.media.metadata.replaceAuthor(req.author, existingAuthor)
|
||||||
libraryItem.media.metadata.updateAuthor(req.author)
|
})
|
||||||
})
|
if (itemsWithAuthor.length) {
|
||||||
if (itemsWithAuthor.length) {
|
await this.db.updateLibraryItems(itemsWithAuthor)
|
||||||
await this.db.updateLibraryItems(itemsWithAuthor)
|
this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||||
this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateEntity('author', req.author)
|
// Remove old author
|
||||||
var numBooks = this.db.libraryItems.filter(li => {
|
await this.db.removeEntity('author', req.author.id)
|
||||||
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
this.emitter('author_removed', req.author.toJSON())
|
||||||
}).length
|
|
||||||
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
// Send updated num books for merged author
|
||||||
author: req.author.toJSON(),
|
var numBooks = this.db.libraryItems.filter(li => {
|
||||||
updated: hasUpdated
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(existingAuthor.id)
|
||||||
})
|
}).length
|
||||||
|
this.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
author: existingAuthor.toJSON(),
|
||||||
|
merged: true
|
||||||
|
})
|
||||||
|
} else { // Regular author update
|
||||||
|
var hasUpdated = req.author.update(payload)
|
||||||
|
|
||||||
|
if (hasUpdated) {
|
||||||
|
if (authorNameUpdate) { // Update author name on all books
|
||||||
|
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||||
|
itemsWithAuthor.forEach(libraryItem => {
|
||||||
|
libraryItem.media.metadata.updateAuthor(req.author)
|
||||||
|
})
|
||||||
|
if (itemsWithAuthor.length) {
|
||||||
|
await this.db.updateLibraryItems(itemsWithAuthor)
|
||||||
|
this.emitter('items_updated', itemsWithAuthor.map(li => li.toJSONExpanded()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.db.updateEntity('author', req.author)
|
||||||
|
var numBooks = this.db.libraryItems.filter(li => {
|
||||||
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||||
|
}).length
|
||||||
|
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
author: req.author.toJSON(),
|
||||||
|
updated: hasUpdated
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(req, res) {
|
async search(req, res) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('../libs/fsExtra')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Library = require('../objects/Library')
|
const Library = require('../objects/Library')
|
||||||
@@ -163,7 +163,7 @@ class LibraryController {
|
|||||||
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
||||||
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
||||||
|
|
||||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
|
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
||||||
payload.total = libraryItems.length
|
payload.total = libraryItems.length
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +176,8 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle server setting sortingIgnorePrefix
|
// Handle server setting sortingIgnorePrefix
|
||||||
if (sortKey === 'media.metadata.title' && this.db.serverSettings.sortingIgnorePrefix) {
|
const sortByTitle = sortKey === 'media.metadata.title'
|
||||||
|
if (sortByTitle && this.db.serverSettings.sortingIgnorePrefix) {
|
||||||
// BookMetadata.js has titleIgnorePrefix getter
|
// BookMetadata.js has titleIgnorePrefix getter
|
||||||
sortKey += 'IgnorePrefix'
|
sortKey += 'IgnorePrefix'
|
||||||
}
|
}
|
||||||
@@ -186,6 +187,16 @@ class LibraryController {
|
|||||||
var sortArray = [
|
var sortArray = [
|
||||||
{
|
{
|
||||||
[direction]: (li) => {
|
[direction]: (li) => {
|
||||||
|
// When collapsing by series and sorting by title use the series name instead of the book title
|
||||||
|
if (payload.mediaType === 'book' && payload.collapseseries && li.media.metadata.seriesName) {
|
||||||
|
if (sortByTitle) {
|
||||||
|
return this.db.serverSettings.sortingIgnorePrefix ? li.media.metadata.seriesNameIgnorePrefix : li.media.metadata.seriesName
|
||||||
|
} else {
|
||||||
|
// When not sorting by title always show the collapsed series at the end
|
||||||
|
return direction === 'desc' ? -1 : 'zzzz'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Supports dot notation strings i.e. "media.metadata.title"
|
// Supports dot notation strings i.e. "media.metadata.title"
|
||||||
return sortKey.split('.').reduce((a, b) => a[b], li)
|
return sortKey.split('.').reduce((a, b) => a[b], li)
|
||||||
}
|
}
|
||||||
@@ -262,7 +273,7 @@ class LibraryController {
|
|||||||
|
|
||||||
var series = libraryHelpers.getSeriesFromBooks(libraryItems, payload.minified)
|
var series = libraryHelpers.getSeriesFromBooks(libraryItems, payload.minified)
|
||||||
series = sort(series).asc(s => {
|
series = sort(series).asc(s => {
|
||||||
return s.name
|
return this.db.serverSettings.sortingIgnorePrefix ? s.nameIgnorePrefix : s.name
|
||||||
})
|
})
|
||||||
payload.total = series.length
|
payload.total = series.length
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class MeController {
|
|||||||
|
|
||||||
// GET: api/me/progress/:id/:episodeId?
|
// GET: api/me/progress/:id/:episodeId?
|
||||||
async getMediaProgress(req, res) {
|
async getMediaProgress(req, res) {
|
||||||
const mediaProgress = req.user.getMediaProgress(req.id, req.episodeId || null)
|
const mediaProgress = req.user.getMediaProgress(req.params.id, req.params.episodeId || null)
|
||||||
if (!mediaProgress) {
|
if (!mediaProgress) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
@@ -190,6 +190,7 @@ class MeController {
|
|||||||
const updatedLocalMediaProgress = []
|
const updatedLocalMediaProgress = []
|
||||||
var numServerProgressUpdates = 0
|
var numServerProgressUpdates = 0
|
||||||
var localMediaProgress = req.body.localMediaProgress || []
|
var localMediaProgress = req.body.localMediaProgress || []
|
||||||
|
|
||||||
localMediaProgress.forEach(localProgress => {
|
localMediaProgress.forEach(localProgress => {
|
||||||
if (!localProgress.libraryItemId) {
|
if (!localProgress.libraryItemId) {
|
||||||
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress)
|
||||||
@@ -216,7 +217,8 @@ class MeController {
|
|||||||
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
|
||||||
|
|
||||||
for (const key in localProgress) {
|
for (const key in localProgress) {
|
||||||
if (mediaProgress[key] != undefined && localProgress[key] !== mediaProgress[key]) {
|
// Local media progress ID uses the local library item id and server media progress uses the library item id
|
||||||
|
if (key !== 'id' && mediaProgress[key] != undefined && localProgress[key] !== mediaProgress[key]) {
|
||||||
// Logger.debug(`[MeController] syncLocalMediaProgress key ${key} changed from ${localProgress[key]} to ${mediaProgress[key]} - ${mediaProgress.id}`)
|
// Logger.debug(`[MeController] syncLocalMediaProgress key ${key} changed from ${localProgress[key]} to ${mediaProgress[key]} - ${mediaProgress.id}`)
|
||||||
localProgress[key] = mediaProgress[key]
|
localProgress[key] = mediaProgress[key]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||||
const { isObject } = require('../utils/index')
|
const { isObject } = require('../utils/index')
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -239,12 +239,7 @@ class MiscController {
|
|||||||
Logger.error('Invalid user in authorize')
|
Logger.error('Invalid user in authorize')
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
const userResponse = {
|
const userResponse = this.auth.getUserLoginResponsePayload(req.user, this.rssFeedManager.feedsArray)
|
||||||
user: req.user,
|
|
||||||
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
|
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
|
||||||
Source: global.Source
|
|
||||||
}
|
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,5 +258,20 @@ class MiscController {
|
|||||||
})
|
})
|
||||||
res.json(tags)
|
res.json(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateCronExpression(req, res) {
|
||||||
|
const expression = req.body.expression
|
||||||
|
if (!expression) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
patternValidation(expression)
|
||||||
|
res.sendStatus(200)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message)
|
||||||
|
res.status(400).send(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new MiscController()
|
module.exports = new MiscController()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const fs = require('fs-extra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||||
@@ -164,6 +164,25 @@ class PodcastController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findEpisode(req, res) {
|
||||||
|
const rssFeedUrl = req.libraryItem.media.metadata.feedUrl
|
||||||
|
if (!rssFeedUrl) {
|
||||||
|
Logger.error(`[PodcastController] findEpisode: Podcast has no feed url`)
|
||||||
|
return res.status(500).send('Podcast does not have an RSS feed URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchTitle = req.query.title
|
||||||
|
if (!searchTitle) {
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
searchTitle = searchTitle.toLowerCase().trim()
|
||||||
|
|
||||||
|
const episodes = await this.podcastManager.findEpisode(rssFeedUrl, searchTitle)
|
||||||
|
res.json({
|
||||||
|
episodes: episodes || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async downloadEpisodes(req, res) {
|
async downloadEpisodes(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||||
@@ -185,7 +204,7 @@ class PodcastController {
|
|||||||
|
|
||||||
var episodeId = req.params.episodeId
|
var episodeId = req.params.episodeId
|
||||||
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
||||||
return res.status(500).send('Episode not found')
|
return res.status(404).send('Episode not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class UserController {
|
|||||||
account.id = getId('usr')
|
account.id = getId('usr')
|
||||||
account.pash = await this.auth.hashPass(account.password)
|
account.pash = await this.auth.hashPass(account.password)
|
||||||
delete account.password
|
delete account.password
|
||||||
account.token = await this.auth.generateAccessToken({ userId: account.id })
|
account.token = await this.auth.generateAccessToken({ userId: account.id, username })
|
||||||
account.createdAt = Date.now()
|
account.createdAt = Date.now()
|
||||||
var newUser = new User(account)
|
var newUser = new User(account)
|
||||||
var success = await this.db.insertEntity('user', newUser)
|
var success = await this.db.insertEntity('user', newUser)
|
||||||
@@ -74,12 +74,14 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var account = req.body
|
var account = req.body
|
||||||
|
var shouldUpdateToken = false
|
||||||
|
|
||||||
if (account.username !== undefined && account.username !== user.username) {
|
if (account.username !== undefined && account.username !== user.username) {
|
||||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
return res.status(500).send('Username already taken')
|
return res.status(500).send('Username already taken')
|
||||||
}
|
}
|
||||||
|
shouldUpdateToken = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updating password
|
// Updating password
|
||||||
@@ -90,6 +92,10 @@ class UserController {
|
|||||||
|
|
||||||
var hasUpdated = user.update(account)
|
var hasUpdated = user.update(account)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
|
if (shouldUpdateToken) {
|
||||||
|
user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username })
|
||||||
|
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
||||||
|
}
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const fs = require('fs-extra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const OpenLibrary = require('../providers/OpenLibrary')
|
const OpenLibrary = require('../providers/OpenLibrary')
|
||||||
const LibGen = require('../providers/LibGen')
|
|
||||||
const GoogleBooks = require('../providers/GoogleBooks')
|
const GoogleBooks = require('../providers/GoogleBooks')
|
||||||
const Audible = require('../providers/Audible')
|
const Audible = require('../providers/Audible')
|
||||||
const iTunes = require('../providers/iTunes')
|
const iTunes = require('../providers/iTunes')
|
||||||
@@ -10,7 +9,6 @@ const { levenshteinDistance } = require('../utils/index')
|
|||||||
class BookFinder {
|
class BookFinder {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.openLibrary = new OpenLibrary()
|
this.openLibrary = new OpenLibrary()
|
||||||
this.libGen = new LibGen()
|
|
||||||
this.googleBooks = new GoogleBooks()
|
this.googleBooks = new GoogleBooks()
|
||||||
this.audible = new Audible()
|
this.audible = new Audible()
|
||||||
this.iTunesApi = new iTunes()
|
this.iTunesApi = new iTunes()
|
||||||
@@ -123,20 +121,6 @@ class BookFinder {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
|
||||||
var books = await this.libGen.search(title)
|
|
||||||
if (this.verbose) Logger.debug(`LibGen Book Search Results: ${books.length || 0}`)
|
|
||||||
if (books.errorCode) {
|
|
||||||
Logger.error(`LibGen Search Error ${books.errorCode}`)
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
|
|
||||||
if (!booksFiltered.length && books.length) {
|
|
||||||
if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
|
||||||
}
|
|
||||||
return booksFiltered
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var books = await this.openLibrary.searchTitle(title)
|
var books = await this.openLibrary.searchTitle(title)
|
||||||
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
||||||
@@ -185,27 +169,10 @@ class BookFinder {
|
|||||||
books = await this.getAudibleResults(title, author, asin)
|
books = await this.getAudibleResults(title, author, asin)
|
||||||
} else if (provider === 'itunes') {
|
} else if (provider === 'itunes') {
|
||||||
books = await this.getiTunesAudiobooksResults(title, author)
|
books = await this.getiTunesAudiobooksResults(title, author)
|
||||||
} else if (provider === 'libgen') {
|
|
||||||
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
|
||||||
} else if (provider === 'openlibrary') {
|
} else if (provider === 'openlibrary') {
|
||||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
} else if (provider === 'all') {
|
|
||||||
var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
|
||||||
var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
|
||||||
books = books.concat(lbBooks, olBooks)
|
|
||||||
} else {
|
} else {
|
||||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
var hasCloseMatch = books.find(b => (b.totalDistance < 2 && b.totalPossibleDistance > 6))
|
|
||||||
if (!hasCloseMatch) {
|
|
||||||
Logger.debug(`Book Search, openlib has no super close matches - get libgen results also`)
|
|
||||||
var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
|
||||||
books = books.concat(lbBooks)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!books.length && author && options.fallbackTitleOnly) {
|
|
||||||
Logger.debug(`Book Search, no matches for title and author.. check title only`)
|
|
||||||
return this.search(provider, title, null, options)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!books.length && !options.currentlyTryingCleaned) {
|
if (!books.length && !options.currentlyTryingCleaned) {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
Copyright (c) 2012-2014 Chris Talkington, contributors.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
(MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | RegExp} a
|
||||||
|
* @param {string | RegExp} b
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function balanced(a, b, str) {
|
||||||
|
if (a instanceof RegExp) a = maybeMatch(a, str)
|
||||||
|
if (b instanceof RegExp) b = maybeMatch(b, str)
|
||||||
|
|
||||||
|
const r = range(a, b, str)
|
||||||
|
|
||||||
|
return (
|
||||||
|
r && {
|
||||||
|
start: r[0],
|
||||||
|
end: r[1],
|
||||||
|
pre: str.slice(0, r[0]),
|
||||||
|
body: str.slice(r[0] + a.length, r[1]),
|
||||||
|
post: str.slice(r[1] + b.length)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RegExp} reg
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function maybeMatch(reg, str) {
|
||||||
|
const m = str.match(reg)
|
||||||
|
return m ? m[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
balanced.range = range
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} a
|
||||||
|
* @param {string} b
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function range(a, b, str) {
|
||||||
|
let begs, beg, left, right, result
|
||||||
|
let ai = str.indexOf(a)
|
||||||
|
let bi = str.indexOf(b, ai + 1)
|
||||||
|
let i = ai
|
||||||
|
|
||||||
|
if (ai >= 0 && bi > 0) {
|
||||||
|
if (a === b) {
|
||||||
|
return [ai, bi]
|
||||||
|
}
|
||||||
|
begs = []
|
||||||
|
left = str.length
|
||||||
|
|
||||||
|
while (i >= 0 && !result) {
|
||||||
|
if (i === ai) {
|
||||||
|
begs.push(i)
|
||||||
|
ai = str.indexOf(a, i + 1)
|
||||||
|
} else if (begs.length === 1) {
|
||||||
|
result = [begs.pop(), bi]
|
||||||
|
} else {
|
||||||
|
beg = begs.pop()
|
||||||
|
if (beg < left) {
|
||||||
|
left = beg
|
||||||
|
right = bi
|
||||||
|
}
|
||||||
|
|
||||||
|
bi = str.indexOf(b, i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
i = ai < bi && ai >= 0 ? ai : bi
|
||||||
|
}
|
||||||
|
|
||||||
|
if (begs.length) {
|
||||||
|
result = [left, right]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = balanced
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
const balanced = require('../balancedMatch');
|
||||||
|
|
||||||
|
const escSlash = '\0SLASH' + Math.random() + '\0';
|
||||||
|
const escOpen = '\0OPEN' + Math.random() + '\0';
|
||||||
|
const escClose = '\0CLOSE' + Math.random() + '\0';
|
||||||
|
const escComma = '\0COMMA' + Math.random() + '\0';
|
||||||
|
const escPeriod = '\0PERIOD' + Math.random() + '\0';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number}
|
||||||
|
*/
|
||||||
|
function numeric(str) {
|
||||||
|
return parseInt(str, 10) == str
|
||||||
|
? parseInt(str, 10)
|
||||||
|
: str.charCodeAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function escapeBraces(str) {
|
||||||
|
return str.split('\\\\').join(escSlash)
|
||||||
|
.split('\\{').join(escOpen)
|
||||||
|
.split('\\}').join(escClose)
|
||||||
|
.split('\\,').join(escComma)
|
||||||
|
.split('\\.').join(escPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function unescapeBraces(str) {
|
||||||
|
return str.split(escSlash).join('\\')
|
||||||
|
.split(escOpen).join('{')
|
||||||
|
.split(escClose).join('}')
|
||||||
|
.split(escComma).join(',')
|
||||||
|
.split(escPeriod).join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basically just str.split(","), but handling cases
|
||||||
|
* where we have nested braced sections, which should be
|
||||||
|
* treated as individual members, like {a,{b,c},d}
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function parseCommaParts(str) {
|
||||||
|
if (!str)
|
||||||
|
return [''];
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
const m = balanced('{', '}', str);
|
||||||
|
|
||||||
|
if (!m)
|
||||||
|
return str.split(',');
|
||||||
|
|
||||||
|
const { pre, body, post } = m;
|
||||||
|
const p = pre.split(',');
|
||||||
|
|
||||||
|
p[p.length - 1] += '{' + body + '}';
|
||||||
|
const postParts = parseCommaParts(post);
|
||||||
|
if (post.length) {
|
||||||
|
p[p.length - 1] += postParts.shift();
|
||||||
|
p.push.apply(p, postParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push.apply(parts, p);
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function expandTop(str) {
|
||||||
|
if (!str)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
// I don't know why Bash 4.3 does this, but it does.
|
||||||
|
// Anything starting with {} will have the first two bytes preserved
|
||||||
|
// but *only* at the top level, so {},a}b will not expand to anything,
|
||||||
|
// but a{},b}c will be expanded to [a}c,abc].
|
||||||
|
// One could argue that this is a bug in Bash, but since the goal of
|
||||||
|
// this module is to match Bash's rules, we escape a leading {}
|
||||||
|
if (str.slice(0, 2) === '{}') {
|
||||||
|
str = '\\{\\}' + str.slice(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return expand(escapeBraces(str), true).map(unescapeBraces);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
*/
|
||||||
|
function embrace(str) {
|
||||||
|
return '{' + str + '}';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} el
|
||||||
|
*/
|
||||||
|
function isPadded(el) {
|
||||||
|
return /^-?0\d/.test(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} i
|
||||||
|
* @param {number} y
|
||||||
|
*/
|
||||||
|
function lte(i, y) {
|
||||||
|
return i <= y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} i
|
||||||
|
* @param {number} y
|
||||||
|
*/
|
||||||
|
function gte(i, y) {
|
||||||
|
return i >= y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @param {boolean} [isTop]
|
||||||
|
*/
|
||||||
|
function expand(str, isTop) {
|
||||||
|
/** @type {string[]} */
|
||||||
|
const expansions = [];
|
||||||
|
|
||||||
|
const m = balanced('{', '}', str);
|
||||||
|
if (!m) return [str];
|
||||||
|
|
||||||
|
// no need to expand pre, since it is guaranteed to be free of brace-sets
|
||||||
|
const pre = m.pre;
|
||||||
|
const post = m.post.length
|
||||||
|
? expand(m.post, false)
|
||||||
|
: [''];
|
||||||
|
|
||||||
|
if (/\$$/.test(m.pre)) {
|
||||||
|
for (let k = 0; k < post.length; k++) {
|
||||||
|
const expansion = pre + '{' + m.body + '}' + post[k];
|
||||||
|
expansions.push(expansion);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
|
||||||
|
const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
|
||||||
|
const isSequence = isNumericSequence || isAlphaSequence;
|
||||||
|
const isOptions = m.body.indexOf(',') >= 0;
|
||||||
|
if (!isSequence && !isOptions) {
|
||||||
|
// {a},b}
|
||||||
|
if (m.post.match(/,.*\}/)) {
|
||||||
|
str = m.pre + '{' + m.body + escClose + m.post;
|
||||||
|
return expand(str);
|
||||||
|
}
|
||||||
|
return [str];
|
||||||
|
}
|
||||||
|
|
||||||
|
let n;
|
||||||
|
if (isSequence) {
|
||||||
|
n = m.body.split(/\.\./);
|
||||||
|
} else {
|
||||||
|
n = parseCommaParts(m.body);
|
||||||
|
if (n.length === 1) {
|
||||||
|
// x{{a,b}}y ==> x{a}y x{b}y
|
||||||
|
n = expand(n[0], false).map(embrace);
|
||||||
|
if (n.length === 1) {
|
||||||
|
return post.map(function (p) {
|
||||||
|
return m.pre + n[0] + p;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, n is the parts, and we know it's not a comma set
|
||||||
|
// with a single entry.
|
||||||
|
let N;
|
||||||
|
|
||||||
|
if (isSequence) {
|
||||||
|
const x = numeric(n[0]);
|
||||||
|
const y = numeric(n[1]);
|
||||||
|
const width = Math.max(n[0].length, n[1].length)
|
||||||
|
let incr = n.length == 3
|
||||||
|
? Math.abs(numeric(n[2]))
|
||||||
|
: 1;
|
||||||
|
let test = lte;
|
||||||
|
const reverse = y < x;
|
||||||
|
if (reverse) {
|
||||||
|
incr *= -1;
|
||||||
|
test = gte;
|
||||||
|
}
|
||||||
|
const pad = n.some(isPadded);
|
||||||
|
|
||||||
|
N = [];
|
||||||
|
|
||||||
|
for (let i = x; test(i, y); i += incr) {
|
||||||
|
let c;
|
||||||
|
if (isAlphaSequence) {
|
||||||
|
c = String.fromCharCode(i);
|
||||||
|
if (c === '\\')
|
||||||
|
c = '';
|
||||||
|
} else {
|
||||||
|
c = String(i);
|
||||||
|
if (pad) {
|
||||||
|
const need = width - c.length;
|
||||||
|
if (need > 0) {
|
||||||
|
const z = new Array(need + 1).join('0');
|
||||||
|
if (i < 0)
|
||||||
|
c = '-' + z + c.slice(1);
|
||||||
|
else
|
||||||
|
c = z + c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
N.push(c);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
N = [];
|
||||||
|
|
||||||
|
for (let j = 0; j < n.length; j++) {
|
||||||
|
N.push.apply(N, expand(n[j], false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < N.length; j++) {
|
||||||
|
for (let k = 0; k < post.length; k++) {
|
||||||
|
const expansion = pre + N[j] + post[k];
|
||||||
|
if (!isTop || isSequence || expansion)
|
||||||
|
expansions.push(expansion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expansions;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = expandTop;
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* archiver-utils
|
||||||
|
*
|
||||||
|
* Copyright (c) 2012-2014 Chris Talkington, contributors.
|
||||||
|
* Licensed under the MIT license.
|
||||||
|
* https://github.com/archiverjs/node-archiver/blob/master/LICENSE-MIT
|
||||||
|
*/
|
||||||
|
var fs = require('graceful-fs');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var flatten = require('./lodash.flatten')
|
||||||
|
var difference = require('./lodash.difference');
|
||||||
|
var union = require('./lodash.union');
|
||||||
|
var isPlainObject = require('./lodash.isplainobject');
|
||||||
|
|
||||||
|
var glob = require('./glob');
|
||||||
|
|
||||||
|
var file = module.exports = {};
|
||||||
|
|
||||||
|
var pathSeparatorRe = /[\/\\]/g;
|
||||||
|
|
||||||
|
// Process specified wildcard glob patterns or filenames against a
|
||||||
|
// callback, excluding and uniquing files in the result set.
|
||||||
|
var processPatterns = function (patterns, fn) {
|
||||||
|
// Filepaths to return.
|
||||||
|
var result = [];
|
||||||
|
// Iterate over flattened patterns array.
|
||||||
|
flatten(patterns).forEach(function (pattern) {
|
||||||
|
// If the first character is ! it should be omitted
|
||||||
|
var exclusion = pattern.indexOf('!') === 0;
|
||||||
|
// If the pattern is an exclusion, remove the !
|
||||||
|
if (exclusion) { pattern = pattern.slice(1); }
|
||||||
|
// Find all matching files for this pattern.
|
||||||
|
var matches = fn(pattern);
|
||||||
|
if (exclusion) {
|
||||||
|
// If an exclusion, remove matching files.
|
||||||
|
result = difference(result, matches);
|
||||||
|
} else {
|
||||||
|
// Otherwise add matching files.
|
||||||
|
result = union(result, matches);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// True if the file path exists.
|
||||||
|
file.exists = function () {
|
||||||
|
var filepath = path.join.apply(path, arguments);
|
||||||
|
return fs.existsSync(filepath);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return an array of all file paths that match the given wildcard patterns.
|
||||||
|
file.expand = function (...args) {
|
||||||
|
// If the first argument is an options object, save those options to pass
|
||||||
|
// into the File.prototype.glob.sync method.
|
||||||
|
var options = isPlainObject(args[0]) ? args.shift() : {};
|
||||||
|
// Use the first argument if it's an Array, otherwise convert the arguments
|
||||||
|
// object to an array and use that.
|
||||||
|
var patterns = Array.isArray(args[0]) ? args[0] : args;
|
||||||
|
// Return empty set if there are no patterns or filepaths.
|
||||||
|
if (patterns.length === 0) { return []; }
|
||||||
|
// Return all matching filepaths.
|
||||||
|
var matches = processPatterns(patterns, function (pattern) {
|
||||||
|
// Find all matching files for this pattern.
|
||||||
|
return glob.sync(pattern, options);
|
||||||
|
});
|
||||||
|
// Filter result set?
|
||||||
|
if (options.filter) {
|
||||||
|
matches = matches.filter(function (filepath) {
|
||||||
|
filepath = path.join(options.cwd || '', filepath);
|
||||||
|
try {
|
||||||
|
if (typeof options.filter === 'function') {
|
||||||
|
return options.filter(filepath);
|
||||||
|
} else {
|
||||||
|
// If the file is of the right type and exists, this should work.
|
||||||
|
return fs.statSync(filepath)[options.filter]();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Otherwise, it's probably not the right type.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build a multi task "files" object dynamically.
|
||||||
|
file.expandMapping = function (patterns, destBase, options) {
|
||||||
|
options = Object.assign({
|
||||||
|
rename: function (destBase, destPath) {
|
||||||
|
return path.join(destBase || '', destPath);
|
||||||
|
}
|
||||||
|
}, options);
|
||||||
|
var files = [];
|
||||||
|
var fileByDest = {};
|
||||||
|
// Find all files matching pattern, using passed-in options.
|
||||||
|
file.expand(options, patterns).forEach(function (src) {
|
||||||
|
var destPath = src;
|
||||||
|
// Flatten?
|
||||||
|
if (options.flatten) {
|
||||||
|
destPath = path.basename(destPath);
|
||||||
|
}
|
||||||
|
// Change the extension?
|
||||||
|
if (options.ext) {
|
||||||
|
destPath = destPath.replace(/(\.[^\/]*)?$/, options.ext);
|
||||||
|
}
|
||||||
|
// Generate destination filename.
|
||||||
|
var dest = options.rename(destBase, destPath, options);
|
||||||
|
// Prepend cwd to src path if necessary.
|
||||||
|
if (options.cwd) { src = path.join(options.cwd, src); }
|
||||||
|
// Normalize filepaths to be unix-style.
|
||||||
|
dest = dest.replace(pathSeparatorRe, '/');
|
||||||
|
src = src.replace(pathSeparatorRe, '/');
|
||||||
|
// Map correct src path to dest path.
|
||||||
|
if (fileByDest[dest]) {
|
||||||
|
// If dest already exists, push this src onto that dest's src array.
|
||||||
|
fileByDest[dest].src.push(src);
|
||||||
|
} else {
|
||||||
|
// Otherwise create a new src-dest file mapping object.
|
||||||
|
files.push({
|
||||||
|
src: [src],
|
||||||
|
dest: dest,
|
||||||
|
});
|
||||||
|
// And store a reference for later use.
|
||||||
|
fileByDest[dest] = files[files.length - 1];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
// reusing bits of grunt's multi-task source normalization
|
||||||
|
file.normalizeFilesArray = function (data) {
|
||||||
|
var files = [];
|
||||||
|
|
||||||
|
data.forEach(function (obj) {
|
||||||
|
var prop;
|
||||||
|
if ('src' in obj || 'dest' in obj) {
|
||||||
|
files.push(obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
files = _(files).chain().forEach(function (obj) {
|
||||||
|
if (!('src' in obj) || !obj.src) { return; }
|
||||||
|
// Normalize .src properties to flattened array.
|
||||||
|
if (Array.isArray(obj.src)) {
|
||||||
|
obj.src = flatten(obj.src);
|
||||||
|
} else {
|
||||||
|
obj.src = [obj.src];
|
||||||
|
}
|
||||||
|
}).map(function (obj) {
|
||||||
|
// Build options object, removing unwanted properties.
|
||||||
|
var expandOptions = Object.assign({}, obj);
|
||||||
|
delete expandOptions.src;
|
||||||
|
delete expandOptions.dest;
|
||||||
|
|
||||||
|
// Expand file mappings.
|
||||||
|
if (obj.expand) {
|
||||||
|
return file.expandMapping(obj.src, obj.dest, expandOptions).map(function (mapObj) {
|
||||||
|
// Copy obj properties to result.
|
||||||
|
var result = Object.assign({}, obj);
|
||||||
|
// Make a clone of the orig obj available.
|
||||||
|
result.orig = Object.assign({}, obj);
|
||||||
|
// Set .src and .dest, processing both as templates.
|
||||||
|
result.src = mapObj.src;
|
||||||
|
result.dest = mapObj.dest;
|
||||||
|
// Remove unwanted properties.
|
||||||
|
['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function (prop) {
|
||||||
|
delete result[prop];
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy obj properties to result, adding an .orig property.
|
||||||
|
var result = Object.assign({}, obj);
|
||||||
|
// Make a clone of the orig obj available.
|
||||||
|
result.orig = Object.assign({}, obj);
|
||||||
|
|
||||||
|
if ('src' in result) {
|
||||||
|
// Expose an expand-on-demand getter method as .src.
|
||||||
|
Object.defineProperty(result, 'src', {
|
||||||
|
enumerable: true,
|
||||||
|
get: function fn() {
|
||||||
|
var src;
|
||||||
|
if (!('result' in fn)) {
|
||||||
|
src = obj.src;
|
||||||
|
// If src is an array, flatten it. Otherwise, make it into an array.
|
||||||
|
src = Array.isArray(src) ? flatten(src) : [src];
|
||||||
|
// Expand src files, memoizing result.
|
||||||
|
fn.result = file.expand(expandOptions, src);
|
||||||
|
}
|
||||||
|
return fn.result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('dest' in result) {
|
||||||
|
result.dest = obj.dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}).flatten().value();
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
The ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2016-2022 Isaac Z. Schlueter and Contributors
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||||
|
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
This library bundles a version of the `fs.realpath` and `fs.realpathSync`
|
||||||
|
methods from Node.js v0.10 under the terms of the Node.js MIT license.
|
||||||
|
|
||||||
|
Node's license follows, also included at the header of `old.js` which contains
|
||||||
|
the licensed code:
|
||||||
|
|
||||||
|
Copyright (c) 2016-2022 Joyent, Inc. and other Node contributors.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
module.exports = realpath
|
||||||
|
realpath.realpath = realpath
|
||||||
|
realpath.sync = realpathSync
|
||||||
|
realpath.realpathSync = realpathSync
|
||||||
|
|
||||||
|
var fs = require('fs')
|
||||||
|
var origRealpath = fs.realpath
|
||||||
|
var origRealpathSync = fs.realpathSync
|
||||||
|
|
||||||
|
var version = process.version
|
||||||
|
var ok = /^v[0-5]\./.test(version)
|
||||||
|
var old = require('./old.js')
|
||||||
|
|
||||||
|
function newError(er) {
|
||||||
|
return er && er.syscall === 'realpath' && (
|
||||||
|
er.code === 'ELOOP' ||
|
||||||
|
er.code === 'ENOMEM' ||
|
||||||
|
er.code === 'ENAMETOOLONG'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function realpath(p, cache, cb) {
|
||||||
|
if (ok) {
|
||||||
|
return origRealpath(p, cache, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof cache === 'function') {
|
||||||
|
cb = cache
|
||||||
|
cache = null
|
||||||
|
}
|
||||||
|
origRealpath(p, cache, function (er, result) {
|
||||||
|
if (newError(er)) {
|
||||||
|
old.realpath(p, cache, cb)
|
||||||
|
} else {
|
||||||
|
cb(er, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function realpathSync(p, cache) {
|
||||||
|
if (ok) {
|
||||||
|
return origRealpathSync(p, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return origRealpathSync(p, cache)
|
||||||
|
} catch (er) {
|
||||||
|
if (newError(er)) {
|
||||||
|
return old.realpathSync(p, cache)
|
||||||
|
} else {
|
||||||
|
throw er
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user