diff --git a/.dockerignore b/.dockerignore index 942b75ec5..11cc0bbce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,4 +12,5 @@ dev.js test/ /client/.nuxt/ /client/dist/ -/dist/ \ No newline at end of file +/dist/ +/deploy/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 53a60bef2..8df68f625 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,7 @@ test/ /client/.nuxt/ /client/dist/ /dist/ -library/ +/deploy/ sw.* .DS_STORE diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..2fb8b48fb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "vetur.format.defaultFormatterOptions": { + "prettier": { + "semi": false, + "singleQuote": true, + "printWidth": 400, + "proseWrap": "never", + "trailingComma": "none" + }, + "prettyhtml": { + "printWidth": 400, + "singleQuote": false, + "wrapAttributes": false, + "sortAttributes": false + } + }, + "editor.formatOnSave": true, + "editor.detectIndentation": true, + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 47aced8a9..db7642bb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,9 @@ RUN npm ci && npm cache clean --force RUN npm run generate ### STAGE 1: Build server ### +FROM sandreas/tone:v0.1.2 AS tone FROM node:16-alpine + ENV NODE_ENV=production RUN apk update && \ apk add --no-cache --update \ @@ -14,6 +16,7 @@ RUN apk update && \ tzdata \ ffmpeg +COPY --from=tone /usr/local/bin/tone /usr/local/bin/ COPY --from=build /client/dist /client/dist COPY index.js package* / COPY server server diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst index 0ad58d94d..be2d8b794 100644 --- a/build/debian/DEBIAN/preinst +++ b/build/debian/DEBIAN/preinst @@ -8,8 +8,6 @@ CONFIG_PATH="/etc/default/audiobookshelf" DEFAULT_PORT=13378 DEFAULT_HOST="0.0.0.0" - - add_user() { : "${1:?'User was not defined'}" declare -r user="$1" @@ -51,7 +49,8 @@ add_group() { install_ffmpeg() { echo "Starting FFMPEG Install" - WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz" + WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz" + WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.2/tone-0.1.2-linux-x64.tar.gz --output-document=tone-0.1.2-linux-x64.tar.gz" if ! cd "$FFMPEG_INSTALL_DIR"; then echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR" @@ -64,13 +63,26 @@ install_ffmpeg() { tar xvf ffmpeg-git-amd64-static.tar.xz --strip-components=1 rm ffmpeg-git-amd64-static.tar.xz - echo "Good to go on Ffmpeg... hopefully" + # Temp downloading tone library to the ffmpeg dir + echo "Getting tone.." + $WGET_TONE + tar xvf tone-0.1.2-linux-x64.tar.gz --strip-components=1 + rm tone-0.1.2-linux-x64.tar.gz + + echo "Good to go on Ffmpeg (& tone)... hopefully" } setup_config() { if [ -f "$CONFIG_PATH" ]; then echo "Existing config found." cat $CONFIG_PATH + + # TONE_PATH variable added in 2.1.6, if it doesnt exist then add it + if ! grep -q "TONE_PATH" "$CONFIG_PATH"; then + echo "Adding TONE_PATH to existing config" + echo "TONE_PATH=$FFMPEG_INSTALL_DIR/tone" >> "$CONFIG_PATH" + fi + else if [ ! -d "$DEFAULT_DATA_DIR" ]; then @@ -83,11 +95,12 @@ setup_config() { echo "Creating default config." config_text="METADATA_PATH=$DEFAULT_DATA_DIR/metadata - CONFIG_PATH=$DEFAULT_DATA_DIR/config - FFMPEG_PATH=/usr/lib/audiobookshelf-ffmpeg/ffmpeg - FFPROBE_PATH=/usr/lib/audiobookshelf-ffmpeg/ffprobe - PORT=$DEFAULT_PORT - HOST=$DEFAULT_HOST" +CONFIG_PATH=$DEFAULT_DATA_DIR/config +FFMPEG_PATH=$FFMPEG_INSTALL_DIR/ffmpeg +FFPROBE_PATH=$FFMPEG_INSTALL_DIR/ffprobe +TONE_PATH=$FFMPEG_INSTALL_DIR/tone +PORT=$DEFAULT_PORT +HOST=$DEFAULT_HOST" echo "$config_text" diff --git a/build/debian/lib/systemd/system/audiobookshelf.service b/build/debian/lib/systemd/system/audiobookshelf.service index 528a2c30a..7dd657dad 100644 --- a/build/debian/lib/systemd/system/audiobookshelf.service +++ b/build/debian/lib/systemd/system/audiobookshelf.service @@ -10,6 +10,7 @@ ExecStart=/usr/share/audiobookshelf/audiobookshelf ExecReload=/bin/kill -HUP $MAINPID Restart=always User=audiobookshelf +Group=audiobookshelf PermissionsStartOnly=true [Install] diff --git a/client/assets/absicons.css b/client/assets/absicons.css new file mode 100644 index 000000000..b2c0f8e88 --- /dev/null +++ b/client/assets/absicons.css @@ -0,0 +1,106 @@ +@font-face { + font-family: 'absicons'; + src: url('~static/fonts/absicons/absicons.eot?2jfq33'); + src: url('~static/fonts/absicons/absicons.eot?2jfq33#iefix') format('embedded-opentype'), + url('~static/fonts/absicons/absicons.ttf?2jfq33') format('truetype'), + url('~static/fonts/absicons/absicons.woff?2jfq33') format('woff'), + url('~static/fonts/absicons/absicons.svg?2jfq33#absicons') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +.abs-icons { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'absicons' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-books-1:before { + content: "\e905"; +} + +.icon-microphone-1:before { + content: "\e902"; +} + +.icon-radio:before { + content: "\e903"; +} + +.icon-podcast:before { + content: "\e904"; +} + +.icon-audiobookshelf:before { + content: "\e900"; +} + +.icon-database:before { + content: "\e906"; +} + +.icon-microphone-2:before { + content: "\e901"; +} + +.icon-headphones:before { + content: "\e910"; +} + +.icon-music:before { + content: "\e911"; +} + +.icon-video:before { + content: "\e914"; +} + +.icon-microphone-3:before { + content: "\e91e"; +} + +.icon-book-1:before { + content: "\e91f"; +} + +.icon-books-2:before { + content: "\e920"; +} + +.icon-file-picture:before { + content: "\e927"; +} + +.icon-database-1:before { + content: "\e964"; +} + +.icon-rocket:before { + content: "\e9a5"; +} + +.icon-power:before { + content: "\e9b5"; +} + +.icon-star:before { + content: "\e9d9"; +} + +.icon-heart:before { + content: "\e9da"; +} + +.icon-rss:before { + content: "\ea9b"; +} \ No newline at end of file diff --git a/client/assets/app.css b/client/assets/app.css index e870e7664..c823af2f8 100644 --- a/client/assets/app.css +++ b/client/assets/app.css @@ -2,6 +2,7 @@ @import './transitions.css'; @import './draggable.css'; @import './defaultStyles.css'; +@import './absicons.css'; :root { --bookshelf-texture-img: url(/textures/wood_default.jpg); diff --git a/client/assets/fonts.css b/client/assets/fonts.css index 46e007aa2..f3c586e34 100644 --- a/client/assets/fonts.css +++ b/client/assets/fonts.css @@ -2,14 +2,14 @@ font-family: 'Material Icons'; font-style: normal; font-weight: 400; - src: url(/fonts/MaterialIcons.woff2) format('woff2'); + src: url(~static/fonts/MaterialIcons.woff2) format('woff2'); } @font-face { font-family: 'Material Icons Outlined'; font-style: normal; font-weight: 400; - src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2'); + src: url(~static/fonts/MaterialIconsOutlined.woff2) format('woff2'); } .material-icons { @@ -54,7 +54,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); + src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2'); 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; } @@ -64,7 +64,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); + src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2'); 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; } @@ -74,7 +74,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/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; } @@ -84,7 +84,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @@ -94,7 +94,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); unicode-range: U+1F00-1FFF; } @@ -104,7 +104,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); unicode-range: U+0370-03FF; } @@ -114,7 +114,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/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; } @@ -124,7 +124,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/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; } @@ -134,7 +134,7 @@ font-style: normal; font-weight: 300; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('truetype'); + src: url(~static/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; } @@ -144,7 +144,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf'); unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; } @@ -154,7 +154,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @@ -164,7 +164,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); unicode-range: U+1F00-1FFF; } @@ -174,7 +174,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); unicode-range: U+0370-03FF; } @@ -184,7 +184,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); + src: url(~static/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; } @@ -194,7 +194,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); + src: url(~static/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; } @@ -204,7 +204,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('truetype'); + src: url(~static/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; } @@ -214,7 +214,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/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; } @@ -224,7 +224,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @@ -234,7 +234,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); unicode-range: U+1F00-1FFF; } @@ -244,7 +244,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); unicode-range: U+0370-03FF; } @@ -254,7 +254,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/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; } @@ -264,7 +264,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/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; } @@ -274,7 +274,7 @@ font-style: normal; font-weight: 600; font-display: swap; - src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('truetype'); + src: url(~static/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; } @@ -284,7 +284,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); + src: url(~static/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; } @@ -294,7 +294,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); + src: url(~static/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } @@ -304,7 +304,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); + src: url(~static/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); unicode-range: U+1F00-1FFF; } @@ -314,7 +314,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); + src: url(~static/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); unicode-range: U+0370-03FF; } @@ -324,7 +324,7 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); + src: url(~static/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; } @@ -334,6 +334,6 @@ font-style: normal; font-weight: 400; font-display: swap; - src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('truetype'); + src: url(~static/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; } \ No newline at end of file diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index cc417739b..f39e88398 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -3,11 +3,11 @@
- + -

audiobookshelf

+

audiobookshelf logo_dev

@@ -15,7 +15,7 @@
- logo_dev +
-
-

{{ numLibraryItemsSelected }} Selected

+

{{ $getString('MessageItemsSelected', [numLibraryItemsSelected]) }}

- + + + + - + - + - + close
@@ -150,7 +152,7 @@ export default { cancelSelectionMode() { if (this.processingBatchDelete) return this.$store.commit('setSelectedLibraryItems', []) - this.$eventBus.$emit('bookshelf-clear-selection') + this.$eventBus.$emit('bookshelf_clear_selection') this.isAllSelected = false }, toggleBatchRead() { @@ -169,7 +171,7 @@ export default { this.$toast.success('Batch update success!') this.$store.commit('setProcessingBatch', false) this.$store.commit('setSelectedLibraryItems', []) - this.$eventBus.$emit('bookshelf-clear-selection') + this.$eventBus.$emit('bookshelf_clear_selection') }) .catch((error) => { this.$toast.error('Batch update failed') @@ -192,7 +194,7 @@ export default { this.processingBatchDelete = false this.$store.commit('setProcessingBatch', false) this.$store.commit('setSelectedLibraryItems', []) - this.$eventBus.$emit('bookshelf-clear-selection') + this.$eventBus.$emit('bookshelf_clear_selection') }) .catch((error) => { this.$toast.error('Batch delete failed') @@ -206,10 +208,13 @@ export default { this.$router.push('/batch') }, batchAddToCollectionClick() { - this.$store.commit('globals/setShowBatchUserCollectionsModal', true) + this.$store.commit('globals/setShowBatchCollectionsModal', true) }, setBookshelfTotalEntities(totalEntities) { this.totalEntities = totalEntities + }, + batchAutoMatchClick() { + this.$store.commit('globals/setShowBatchQuickMatchModal', true) } }, mounted() { diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index ceb792374..f17ca3658 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -16,24 +16,24 @@
@@ -54,7 +54,8 @@ export default { keywordFilterTimeout: null, scannerParseSubtitle: false, wrapperClientWidth: 0, - shelves: [] + shelves: [], + lastItemIndexSelected: -1 } }, computed: { @@ -71,7 +72,7 @@ export default { return this.$store.getters['libraries/getCurrentLibraryName'] }, isAlternativeBookshelfView() { - return this.$store.getters['getHomeBookshelfView'] === this.$constants.BookshelfView.TITLES + return this.$store.getters['getHomeBookshelfView'] === this.$constants.BookshelfView.DETAIL }, bookCoverWidth() { var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize') @@ -87,9 +88,64 @@ export default { sizeMultiplier() { var baseSize = this.isCoverSquareAspectRatio ? 192 : 120 return this.bookCoverWidth / baseSize + }, + selectedLibraryItems() { + return this.$store.state.selectedLibraryItems || [] } }, methods: { + selectEntity({ entity, shiftKey }, shelfIndex) { + const shelf = this.shelves[shelfIndex] + const entityShelfIndex = shelf.entities.findIndex((ent) => ent.id === entity.id) + const indexOf = shelf.shelfStartIndex + entityShelfIndex + + const lastLastItemIndexSelected = this.lastItemIndexSelected + if (!this.selectedLibraryItems.includes(entity.id)) { + this.lastItemIndexSelected = indexOf + } else { + this.lastItemIndexSelected = -1 + } + + if (shiftKey && lastLastItemIndexSelected >= 0) { + var loopStart = indexOf + var loopEnd = lastLastItemIndexSelected + if (indexOf > lastLastItemIndexSelected) { + loopStart = lastLastItemIndexSelected + loopEnd = indexOf + } + + const flattenedEntitiesArray = [] + this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities)) + + var isSelecting = false + // If any items in this range is not selected then select all otherwise unselect all + for (let i = loopStart; i <= loopEnd; i++) { + const thisEntity = flattenedEntitiesArray[i] + if (thisEntity) { + if (!this.selectedLibraryItems.includes(thisEntity.id)) { + isSelecting = true + break + } + } + } + if (isSelecting) this.lastItemIndexSelected = indexOf + + for (let i = loopStart; i <= loopEnd; i++) { + const thisEntity = flattenedEntitiesArray[i] + if (thisEntity) { + this.$store.commit('setLibraryItemSelected', { libraryItemId: thisEntity.id, selected: isSelecting }) + } else { + console.error('Invalid entity index', i) + } + } + } else { + this.$store.commit('toggleLibraryItemSelected', entity.id) + } + + this.$nextTick(() => { + this.$eventBus.$emit('item-selected', entity) + }) + }, async init() { this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0 @@ -110,6 +166,12 @@ export default { console.error('Failed to fetch categories', error) return [] }) + + let totalEntityCount = 0 + for (const shelf of categories) { + shelf.shelfStartIndex = totalEntityCount + totalEntityCount += shelf.entities.length + } this.shelves = categories }, async setShelvesFromSearch() { @@ -118,6 +180,7 @@ export default { shelves.push({ id: 'books', label: 'Books', + labelStringKey: 'LabelBooks', type: 'book', entities: this.results.books.map((res) => res.libraryItem) }) @@ -127,6 +190,7 @@ export default { shelves.push({ id: 'podcasts', label: 'Podcasts', + labelStringKey: 'LabelPodcasts', type: 'podcast', entities: this.results.podcasts.map((res) => res.libraryItem) }) @@ -136,6 +200,7 @@ export default { shelves.push({ id: 'series', label: 'Series', + labelStringKey: 'LabelSeries', type: 'series', entities: this.results.series.map((seriesObj) => { return { @@ -150,6 +215,7 @@ export default { shelves.push({ id: 'tags', label: 'Tags', + labelStringKey: 'LabelTags', type: 'tags', entities: this.results.tags.map((tagObj) => { return { @@ -164,6 +230,7 @@ export default { shelves.push({ id: 'authors', label: 'Authors', + labelStringKey: 'LabelAuthors', type: 'authors', entities: this.results.authors.map((a) => { return { @@ -175,7 +242,6 @@ export default { } this.shelves = shelves }, - settingsUpdated(settings) {}, scan() { this.$store .dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId }) @@ -187,6 +253,15 @@ export default { this.$toast.error('Failed to start scan') }) }, + userUpdated(user) { + if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) { + this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening) + } + if (user.mediaProgress.length) { + const mediaProgressToHide = user.mediaProgress.filter((mp) => mp.hideFromContinueListening) + this.removeItemsFromContinueListening(mediaProgressToHide) + } + }, libraryItemAdded(libraryItem) { console.log('libraryItem added', libraryItem) // TODO: Check if libraryItem would be on this shelf @@ -244,6 +319,45 @@ export default { this.libraryItemUpdated(li) }) }, + removeAllSeriesFromContinueSeries(seriesIds) { + this.shelves.forEach((shelf) => { + if (shelf.type == 'book' && shelf.id == 'continue-series') { + // Filter out series books from continue series shelf + shelf.entities = shelf.entities.filter((ent) => { + if (ent.media.metadata.series && seriesIds.includes(ent.media.metadata.series.id)) return false + return true + }) + } + }) + }, + removeItemsFromContinueListening(mediaProgressItems) { + const continueListeningShelf = this.shelves.find((s) => s.id === 'continue-listening') + if (continueListeningShelf) { + if (continueListeningShelf.type === 'book') { + continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => { + if (mediaProgressItems.some((mp) => mp.libraryItemId === ent.id)) return false + return true + }) + } else if (continueListeningShelf.type === 'episode') { + continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => { + if (!ent.recentEpisode) return true // Should always have this here + if (mediaProgressItems.some((mp) => mp.libraryItemId === ent.id && mp.episodeId === ent.recentEpisode.id)) return false + return true + }) + } + } + // this.shelves.forEach((shelf) => { + // if (shelf.id == 'continue-listening') { + // if (shelf.type == 'book') { + // // Filter out books from continue listening shelf + // shelf.entities = shelf.entities.filter((ent) => { + // if (mediaProgressItems.some(mp => mp.libraryItemId === ent.id)) return false + // return true + // }) + // } + // } + // }) + }, authorUpdated(author) { this.shelves.forEach((shelf) => { if (shelf.type == 'authors') { @@ -267,9 +381,8 @@ export default { }) }, initListeners() { - this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated }) - if (this.$root.socket) { + this.$root.socket.on('user_updated', this.userUpdated) this.$root.socket.on('author_updated', this.authorUpdated) this.$root.socket.on('author_removed', this.authorRemoved) this.$root.socket.on('item_updated', this.libraryItemUpdated) @@ -285,6 +398,7 @@ export default { this.$store.commit('user/removeSettingsListener', 'bookshelf') if (this.$root.socket) { + this.$root.socket.off('user_updated', this.userUpdated) this.$root.socket.off('author_updated', this.authorUpdated) this.$root.socket.off('author_removed', this.authorRemoved) this.$root.socket.off('item_updated', this.libraryItemUpdated) diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue index 2c2ae60ab..f35d72a25 100644 --- a/client/components/app/BookShelfRow.vue +++ b/client/components/app/BookShelfRow.vue @@ -4,12 +4,26 @@
@@ -32,7 +46,7 @@
-

{{ shelf.label }}

+

{{ $strings[shelf.labelStringKey] }}

@@ -57,7 +71,8 @@ export default { }, sizeMultiplier: Number, bookCoverWidth: Number, - bookCoverAspectRatio: Number + bookCoverAspectRatio: Number, + continueListeningShelf: Boolean }, data() { return { @@ -123,11 +138,8 @@ export default { }) } }, - selectItem(libraryItem) { - this.$store.commit('toggleLibraryItemSelected', libraryItem.id) - this.$nextTick(() => { - this.$eventBus.$emit('item-selected', libraryItem) - }) + selectItem(payload) { + this.$emit('selectEntity', payload) }, itemSelectedEvt() { this.updateSelectionMode(this.isSelectionMode) @@ -176,11 +188,11 @@ export default { } }, mounted() { - this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities) + this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities) this.$eventBus.$on('item-selected', this.itemSelectedEvt) }, beforeDestroy() { - this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities) + this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities) this.$eventBus.$off('item-selected', this.itemSelectedEvt) } } diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 035e3d3ef..3b31f26e4 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -1,75 +1,92 @@ @@ -131,6 +139,9 @@ export default { isPodcastSearchPage() { return this.$route.name === 'library-library-podcast-search' }, + isPodcastLatestPage() { + return this.$route.name === 'library-library-podcast-latest' + }, homePage() { return this.$route.name === 'library-library' }, @@ -179,9 +190,9 @@ export default { } }, methods: { - clickChangelog(){ + clickChangelog() { this.showChangelogModal = true - } + } }, mounted() {} } diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 5885e0d94..1b7669901 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -15,7 +15,7 @@

{{ author.name }}

-

Unknown

+

{{ $strings.LabelUnknown }}

@@ -159,8 +159,8 @@ export default { return i.libraryItemId === libraryItemId }) if (currentQueueIndex < 0) { - console.error('Media finished not found in queue', this.playerQueueItems) - return + console.error('Media finished not found in queue - using first in queue', this.playerQueueItems) + currentQueueIndex = -1 } if (currentQueueIndex === this.playerQueueItems.length - 1) { console.log('Finished last item in queue') diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index c8e68a290..46db9e0eb 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -93,12 +93,12 @@ export default { return null }) if (!response) { - this.$toast.error('Author not found') + this.$toast.error(`Author ${this.name} not found`) } else if (response.updated) { - if (response.author.imagePath) this.$toast.success('Author was updated') - else this.$toast.success('Author was updated (no image found)') + if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`) + else this.$toast.success(`Author ${response.author.name} was updated (no image found)`) } else { - this.$toast.info('No updates were made for Author') + this.$toast.info(`No updates were made for Author ${response.author.name}`) } this.searching = false }, diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index 3a34af685..d458664fe 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -4,6 +4,7 @@
+
@@ -12,13 +13,13 @@

{{ book.publishedYear }}

-

by {{ book.author }}

+

by {{ book.author }}

Narrated by {{ book.narrator }}

Runtime: {{ $elapsedPrettyExtended(book.duration * 60) }}

- {{ series.series }} #{{ series.volumeNumber }} + {{ series.series }} #{{ series.sequence }}

diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 16f789b3d..96616dd55 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -15,41 +15,41 @@
- +
- +
-

Directory (auto)

+

{{ $strings.LabelDirectory }} (auto)

- +
-

Directory (auto)

+

{{ $strings.LabelDirectory }} (auto)

- - - + + + -

Successfully Uploaded!

+

{{ $strings.MessageUploaderItemSuccess }}

-

Failed to upload

+

{{ $strings.MessageUploaderItemFailed }}

- +
diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index be101e820..32c41224b 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -14,7 +14,12 @@

{{ displaySortLine }}

-
{{ booksInSeries }}
+
+

#{{ seriesSequenceList }}

+
+
+

{{ booksInSeries }}

+
@@ -27,7 +32,9 @@
-

{{ titleCleaned }}

+

+ {{ titleCleaned }} +

@@ -37,6 +44,8 @@
+ +

{{ title }}

{{ author }}

{{ description }}

- Folder: {{ folderPath }} + {{ $strings.LabelFolder }}: {{ folderPath }}

diff --git a/client/components/controls/FilterSelect.vue b/client/components/controls/FilterSelect.vue index 3a92f9a11..1cb127f68 100644 --- a/client/components/controls/FilterSelect.vue +++ b/client/components/controls/FilterSelect.vue @@ -1,8 +1,8 @@