mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-01 05:29:41 +00:00
Add consolidate badge
This commit is contained in:
parent
5f599a9980
commit
b3cdd880e1
5 changed files with 72 additions and 9 deletions
|
|
@ -238,6 +238,9 @@ Artifact specifications should serve as a source of truth for the feature's life
|
|||
|
||||
### Best Practices
|
||||
|
||||
- **Use for Every Feature**: For *every* new feature or significant development, start by creating a specification file in today's dated folder.
|
||||
- **Initialization**: Always run `make` (or `make today`) in the `artifacts/` directory before starting work on a new specification to ensure the correct dated folder exists.
|
||||
- **Relevant Naming**: Name the specification file according to the task/feature (e.g., `feature_name_specification.md`).
|
||||
- **Update as you go**: The artifact should be updated during implementation if the plan changes.
|
||||
- **Be Specific**: Avoid vague descriptions. If a function is moved to a controller, name the function and the controller.
|
||||
- **Use Tables**: Tables are great for listing files or comparing before/after states.
|
||||
|
|
|
|||
35
artifacts/2026-02-15/consolidation_badge.md
Normal file
35
artifacts/2026-02-15/consolidation_badge.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Not Consolidated Badge Specification
|
||||
|
||||
## Overview
|
||||
Add a visual indicator (badge) to the book thumbnail card in listings to identify books that are not "consolidated". Consolidation means the book's folder name matches the standard `Author - Title` format.
|
||||
|
||||
## Requirements
|
||||
- The badge should only appear if the folder name does not match the sanitized `Author - Title` format.
|
||||
- Only applicable to Books (not Podcasts).
|
||||
- Only applicable to library items in folders (not single files).
|
||||
- The badge should have a descriptive tooltip ("Not Consolidated").
|
||||
- The badge should be clearly visible but not obstructive.
|
||||
- The badge position should account for other status indicators (RSS, shared icon) to avoid overlap.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Backend (Server)
|
||||
- **Model**: `LibraryItem` (`server/models/LibraryItem.js`)
|
||||
- **Logic**: Added `checkIsNotConsolidated()` which:
|
||||
1. Checks if the item is a book folder.
|
||||
2. Sanitizes the `Author - Title` name using `sanitizeFilename`.
|
||||
3. Compares the sanitized name with the folder's name (`Path.basename(this.path)`).
|
||||
- **API**: The flag `isNotConsolidated` is included in the JSON response for library items.
|
||||
|
||||
### Frontend (Client)
|
||||
- **Component**: `LazyBookCard` (`client/components/cards/LazyBookCard.vue`)
|
||||
- **UI**: Added a folder icon badge with a yellow background (`bg-warning`).
|
||||
- **Logic**: Toggles visibility based on the `libraryItem.isNotConsolidated` flag.
|
||||
- **Positioning**: Absolute positioning on the bottom-left side (`bottom: 0.375em`, `left: 0`).
|
||||
|
||||
### View Book Page
|
||||
- **Component**: `client/pages/item/_id/index.vue`
|
||||
- **UI (Badge)**: Badge added next to the book title when `isNotConsolidated` is true.
|
||||
- **UI (Button)**: "Consolidate" button added to the primary action row (after Edit and Mark as Finished).
|
||||
- **Behavior**: The "Consolidate" button is disabled if the book is already consolidated.
|
||||
- **Cleanup**: The "Consolidate" option has been removed from the context menu on this page.
|
||||
|
|
@ -115,6 +115,13 @@
|
|||
<div cy-id="numEpisodesIncomplete" v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||
<p :style="{ fontSize: 0.8 + 'em' }">{{ numEpisodesIncomplete }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Not Consolidated Badge -->
|
||||
<ui-tooltip v-if="isNotConsolidated && !isSelectionMode && !isHovering" text="Not Consolidated" direction="top" class="absolute left-0 z-10" :style="{ padding: 0.375 + 'em', bottom: 0.375 + 'em' }">
|
||||
<div class="rounded-full bg-warning flex items-center justify-center border border-black/20 shadow-sm" :style="{ width: 1.25 + 'em', height: 1.25 + 'em' }">
|
||||
<span class="material-symbols text-black" :style="{ fontSize: 0.9 + 'em' }">folder_open</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -447,6 +454,9 @@ export default {
|
|||
isInvalid() {
|
||||
return this._libraryItem.isInvalid
|
||||
},
|
||||
isNotConsolidated() {
|
||||
return !!this._libraryItem.isNotConsolidated
|
||||
},
|
||||
errorText() {
|
||||
if (this.isMissing) return 'Item directory is missing!'
|
||||
else if (this.isInvalid) {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@
|
|||
{{ title }}
|
||||
<widgets-explicit-indicator v-if="isExplicit" />
|
||||
<widgets-abridged-indicator v-if="isAbridged" />
|
||||
<ui-tooltip v-if="isNotConsolidated" text="Not Consolidated" direction="bottom" class="ml-2">
|
||||
<div class="rounded-full bg-warning flex items-center justify-center border border-black/20 shadow-sm w-6 h-6">
|
||||
<span class="material-symbols text-black text-sm">folder_open</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
|
|
@ -108,6 +113,10 @@
|
|||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="userCanUpdate && !isFile && !isPodcast" :text="isNotConsolidated ? 'Consolidate' : 'Already Consolidated'" direction="top">
|
||||
<ui-icon-btn icon="folder_open" class="mx-0.5" :class="isNotConsolidated ? 'text-warning' : 'opacity-50'" :disabled="!isNotConsolidated" @click="consolidate" />
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Only admin or root user can download new episodes -->
|
||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" :text="$strings.LabelFindEpisodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :aria-label="$strings.LabelFindEpisodes" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
|
|
@ -222,6 +231,9 @@ export default {
|
|||
isInvalid() {
|
||||
return this.libraryItem.isInvalid
|
||||
},
|
||||
isNotConsolidated() {
|
||||
return !!this.libraryItem.isNotConsolidated
|
||||
},
|
||||
isExplicit() {
|
||||
return !!this.mediaMetadata.explicit
|
||||
},
|
||||
|
|
@ -428,12 +440,6 @@ export default {
|
|||
text: this.$strings.ButtonReScan,
|
||||
action: 'rescan'
|
||||
})
|
||||
if (!this.isFile && !this.isPodcast) {
|
||||
items.push({
|
||||
text: 'Consolidate',
|
||||
action: 'consolidate'
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
text: this.$strings.ButtonMoveToLibrary,
|
||||
action: 'move'
|
||||
|
|
@ -837,8 +843,6 @@ export default {
|
|||
} else if (action === 'move') {
|
||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||
this.$store.commit('globals/setShowMoveToLibraryModal', true)
|
||||
} else if (action === 'consolidate') {
|
||||
this.consolidate()
|
||||
} else if (action === 'sendToDevice') {
|
||||
this.sendToDevice(data)
|
||||
} else if (action === 'share') {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const { DataTypes, Model } = require('sequelize')
|
|||
const fsExtra = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
|
||||
const { filePathToPOSIX, getFileTimestampsWithIno, sanitizeFilename } = require('../utils/fileUtils')
|
||||
const LibraryFile = require('../objects/files/LibraryFile')
|
||||
const Book = require('./Book')
|
||||
const Podcast = require('./Podcast')
|
||||
|
|
@ -916,6 +916,14 @@ class LibraryItem extends Model {
|
|||
return this.libraryFiles.map((lf) => new LibraryFile(lf).toJSON())
|
||||
}
|
||||
|
||||
checkIsNotConsolidated() {
|
||||
if (this.isFile || this.mediaType !== 'book' || !this.media) return false
|
||||
const author = this.media.authors?.[0]?.name || 'Unknown Author'
|
||||
const title = this.media.title || 'Unknown Title'
|
||||
const folderName = sanitizeFilename(`${author} - ${title}`)
|
||||
return Path.basename(this.path) !== folderName
|
||||
}
|
||||
|
||||
toOldJSON() {
|
||||
if (!this.media) {
|
||||
throw new Error(`[LibraryItem] Cannot convert to old JSON without media for library item "${this.id}"`)
|
||||
|
|
@ -939,6 +947,7 @@ class LibraryItem extends Model {
|
|||
scanVersion: this.lastScanVersion,
|
||||
isMissing: !!this.isMissing,
|
||||
isInvalid: !!this.isInvalid,
|
||||
isNotConsolidated: this.checkIsNotConsolidated(),
|
||||
mediaType: this.mediaType,
|
||||
media: this.media.toOldJSON(this.id),
|
||||
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||
|
|
@ -967,6 +976,7 @@ class LibraryItem extends Model {
|
|||
updatedAt: this.updatedAt.valueOf(),
|
||||
isMissing: !!this.isMissing,
|
||||
isInvalid: !!this.isInvalid,
|
||||
isNotConsolidated: this.checkIsNotConsolidated(),
|
||||
mediaType: this.mediaType,
|
||||
media: this.media.toOldJSONMinified(),
|
||||
numFiles: this.libraryFiles.length,
|
||||
|
|
@ -993,6 +1003,7 @@ class LibraryItem extends Model {
|
|||
scanVersion: this.lastScanVersion,
|
||||
isMissing: !!this.isMissing,
|
||||
isInvalid: !!this.isInvalid,
|
||||
isNotConsolidated: this.checkIsNotConsolidated(),
|
||||
mediaType: this.mediaType,
|
||||
media: this.media.toOldJSONExpanded(this.id),
|
||||
// LibraryFile JSON includes a fileType property that may not be saved in libraryFiles column in the database
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue