diff --git a/frontend/locales/en-US.json b/frontend/locales/en-US.json index 065f6a62864..a85226ede0f 100644 --- a/frontend/locales/en-US.json +++ b/frontend/locales/en-US.json @@ -1,5 +1,4 @@ { - "auto": "Automatic", "3DFormat": "3D format", "NoMediaSourcesAvailable": "No media sources available", "actor": "Actor", @@ -25,6 +24,7 @@ "aspectRatio": "Aspect ratio", "audio": "Audio", "audioCodecNotSupported": "The audio codec is not supported", + "auto": "Automatic", "badRequest": "Bad request. Try again", "books": "Books", "browserNotSupported": "Your browser is not supported for playing this file.", @@ -34,17 +34,24 @@ "byArtist": "By {artist}", "cancel": "Cancel", "castAndCrew": "Cast & crew", + "clipboardFail": "Failed to copy to clipboard", + "clipboardSuccess": "Copied to clipboard", + "close": "Close", "collectionEmpty": "This collection is empty", "collections": "Collections", "communityRating": "Community rating", "confirm": "Confirm", "connect": "Connect", + "contentType": "Content type", "continueListening": "Continue listening", "continueWatching": "Continue watching", "criticRating": "Critic rating", "customRating": "Custom rating", "darkModeToggle": "Toggle dark mode", "dateAdded": "Date added", + "delete": "Delete", + "deleteItem": "Delete media", + "deleteItemDescription": "Deleting this item will delete it from both the file system and your media library. Are you sure you wish to continue?", "details": "Details", "dialog": { "upNext": { @@ -58,6 +65,11 @@ "disabled": "Disabled", "discNumber": "Disc {discNumber}", "dislikes": "Dislikes", + "download": { + "copyStreamUrl": "Copy Stream URL", + "download": "Download", + "downloadAll": "Download all" + }, "edit": "Edit", "editMetadata": "Edit metadata", "editPerson": "Edit person", @@ -79,6 +91,8 @@ }, "failedRetrievingDisplayPreferences": "Unable to get display preferences. Using last known settings.", "failedSettingDisplayPreferences": "Unable to update display preferences.", + "failedToDeleteItem": "Failed to delete item", + "failedToGetDownloadUrl": "Failed to get URL for selected Item", "failedToRefreshItems": "Failed to refresh items", "favorite": "Favorite", "features": "Features", @@ -98,6 +112,12 @@ "recentlyAdded": "Recently added" } }, + "identify": { + "applyError": "Failed to apply selected search result", + "instructResult": "You can click on one of these cards to apply the result", + "searchError": "Failed to search to remote providers", + "title": "Identify" + }, "imageType": { "art": "Art", "backdrop": "Backdrop", @@ -196,24 +216,79 @@ "name": "Audio channels:" }, "audioCodec": { - "name": "Audio codec:" - }, - "bitrate": { - "name": "Bitrate:" - }, - "container": { - "name": "Container:" + "channels": "Channels:", + "layout": "Layout:", + "name": "Audio codec:", + "sampleRate": "Sample rate:", + "titles": "Audio | Audio {0}" + }, + "embeddedImageCodec": { + "name": "Image codec:", + "titles": "Image | Image {0}" + }, + "generic": { + "bitrate": "Bitrate:", + "codec": "Codec:", + "codecTag": "Codec tag:", + "container": "Container:", + "default": "Default:", + "external": "External:", + "forced": "Forced:", + "language": "Language:", + "path": "Path:", + "profile": "Profile:", + "size": "Size:", + "title": "Title:" }, "name": "Media", "subtitleCodec": { - "name": "Subtitle codec:" + "name": "Subtitle codec:", + "titles": "Subtitle | Subtitle {0}" }, + "title": "Media Info", "videoCodec": { - "name": "Video codec:" + "DoVi": { + "blPresent": "DV bl preset flag:", + "blSignalCompatibilityId": "DV bl signal compatibility ID:", + "elPresent": "DV el preset flag:", + "level": "DV level:", + "majorVersion": "DV version major:", + "minorVersion": "DV version minor:", + "profile": "DV profile:", + "rpuPresent": "DV rpu preset flag:", + "title": "DV title:" + }, + "aspectRatio": "Aspect ratio:", + "bitdepth": "Bit depth:", + "colorPrimaries": "Color primaries:", + "colorRange": "Color range:", + "colorSpace": "Color space:", + "colorTransfer": "Color transfer:", + "frameRate": "Framerate:", + "isAnamorphic": "Anamorphic:", + "isAvc": "AVC:", + "isInterlaced": "Interlaced:", + "level": "Level:", + "name": "Video codec:", + "pixelFormat": "Pixel format:", + "refFrames": "Ref frames:", + "resolution": "Resolution:", + "titles": "Video | Video {0}", + "videoRange": "Video range:", + "videoRangeType": "Video range type:" } }, "menu": "Menu", "metadata": { + "refresh": { + "all": "Replace all metadata", + "methodHint": "Metadata is refreshed based on settings and internet services that are enabled in the Dashboard.", + "missing": "Search for missing metadata", + "replaceImage": "Replace existing images", + "scan": "Scan for new and updated files", + "success": "Metadata refresh enqueued", + "title": "Refresh metadata" + }, "source": "Source", "sourceAll": "All", "title": "Title", @@ -247,6 +322,7 @@ "playback": { "addToQueue": "Add to queue", "clearQueue": "Clear queue and stop playback", + "instantMix": "Instant mix", "playAll": "Play all", "playNext": "Play next", "playbackSource": { @@ -303,6 +379,7 @@ "quality": "Quality", "queue": "Queue", "rating": "Rating", + "refresh": "Refresh", "refreshLibrary": "Refresh library", "releaseDate": "Release date", "remoteDevices": "Remote devices", @@ -313,6 +390,8 @@ "saved": "Saved", "search": { "name": "Search", + "noResults": "No results found", + "results": "Results", "topResults": "Top results" }, "seasonEpisode": "Season {seasonNumber}, Episode {episodeNumber}", @@ -335,9 +414,9 @@ "refreshKeysFailure": "Error refreshing API keys", "revoke": "Revoke", "revokeAll": "Revoke all API keys", - "revokeConfirm": "Confirm API key revocation", "revokeAllFailure": "Error revoking all API keys", "revokeAllSuccess": "Successfully revoked all API keys", + "revokeConfirm": "Confirm API key revocation", "revokeFailure": "Error revoking API key", "revokeSuccess": "Successfully revoked API key" }, @@ -346,9 +425,9 @@ "appVersion": "App version", "delete": "Delete", "deleteAll": "Delete all", - "deleteConfirm": "Confirm device deletion", "deleteAllDevicesError": "Error deleting all devices", "deleteAllDevicesSuccess": "All devices deleted successfully", + "deleteConfirm": "Confirm device deletion", "deleteDeviceError": "Error deleting device", "deleteDeviceSuccess": "Device deleted successfully", "deviceName": "Device name", @@ -442,6 +521,8 @@ "signIn": "Sign in", "snackbar": { "addedToQueue": "Added to queue", + "instantMixFailed": "Failed to add Instant mix to queue", + "instantMixQueued": "Instant mix added to queue", "playNext": "The selected item will be played after the current one", "routeValidationError": "The specified routeId in route params is not correct" }, @@ -463,9 +544,9 @@ "themeVideo": "Theme Video", "tooltips": { "changeLanguage": "Language", + "switchToAuto": "Follow system theme", "switchToDarkMode": "Switch to dark mode", - "switchToLightMode": "Switch to light mode", - "switchToAuto": "Follow system theme" + "switchToLightMode": "Switch to light mode" }, "trailer": "Trailer", "transcodingInfo": { diff --git a/frontend/package.json b/frontend/package.json index 3eb31f85071..ea6f0465294 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -98,4 +98,4 @@ "npm": ">=8.19.2", "yarn": "Yarn is not supported. Please use NPM." } -} +} \ No newline at end of file diff --git a/frontend/src/components/Item/ConfirmDialog.vue b/frontend/src/components/Item/ConfirmDialog.vue new file mode 100644 index 00000000000..8d650bd36df --- /dev/null +++ b/frontend/src/components/Item/ConfirmDialog.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/frontend/src/components/Item/Identify/IdentifyDialog.vue b/frontend/src/components/Item/Identify/IdentifyDialog.vue new file mode 100644 index 00000000000..f343f1926ca --- /dev/null +++ b/frontend/src/components/Item/Identify/IdentifyDialog.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/components/Item/Identify/IdentifyResult.vue b/frontend/src/components/Item/Identify/IdentifyResult.vue new file mode 100644 index 00000000000..80d31da0697 --- /dev/null +++ b/frontend/src/components/Item/Identify/IdentifyResult.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/frontend/src/components/Item/Identify/IdentifyTab.vue b/frontend/src/components/Item/Identify/IdentifyTab.vue new file mode 100644 index 00000000000..47cd45ee6ba --- /dev/null +++ b/frontend/src/components/Item/Identify/IdentifyTab.vue @@ -0,0 +1,334 @@ + + + diff --git a/frontend/src/components/Item/Identify/PosterBlock.vue b/frontend/src/components/Item/Identify/PosterBlock.vue new file mode 100644 index 00000000000..7e546dc2035 --- /dev/null +++ b/frontend/src/components/Item/Identify/PosterBlock.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/frontend/src/components/Item/ItemMenu.vue b/frontend/src/components/Item/ItemMenu.vue index 21504cd6ea0..c68417431b5 100644 --- a/frontend/src/components/Item/ItemMenu.vue +++ b/frontend/src/components/Item/ItemMenu.vue @@ -35,28 +35,72 @@ + :item-id="item.Id" + :media-source-index="mediaSourceIndex" /> + + + + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailAttr.vue b/frontend/src/components/Item/MediaDetail/MediaDetailAttr.vue new file mode 100644 index 00000000000..5228e036d0f --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailAttr.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailColorSpace.vue b/frontend/src/components/Item/MediaDetail/MediaDetailColorSpace.vue new file mode 100644 index 00000000000..c2678e04ec1 --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailColorSpace.vue @@ -0,0 +1,31 @@ + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailContent.vue b/frontend/src/components/Item/MediaDetail/MediaDetailContent.vue new file mode 100644 index 00000000000..066315279f4 --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailContent.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailCopy.vue b/frontend/src/components/Item/MediaDetail/MediaDetailCopy.vue new file mode 100644 index 00000000000..9e6a6976dba --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailCopy.vue @@ -0,0 +1,26 @@ + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailDialog.vue b/frontend/src/components/Item/MediaDetail/MediaDetailDialog.vue new file mode 100644 index 00000000000..bd263e8fac0 --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailDialog.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailExtras.vue b/frontend/src/components/Item/MediaDetail/MediaDetailExtras.vue new file mode 100644 index 00000000000..db4737a10a1 --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailExtras.vue @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/components/Item/MediaDetail/MediaDetailGeneric.vue b/frontend/src/components/Item/MediaDetail/MediaDetailGeneric.vue new file mode 100644 index 00000000000..4993a8db719 --- /dev/null +++ b/frontend/src/components/Item/MediaDetail/MediaDetailGeneric.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/components/Item/Metadata/MetadataEditor.vue b/frontend/src/components/Item/Metadata/MetadataEditor.vue index e92eb9e977b..5f78e919892 100644 --- a/frontend/src/components/Item/Metadata/MetadataEditor.vue +++ b/frontend/src/components/Item/Metadata/MetadataEditor.vue @@ -26,6 +26,14 @@ + (); +const props = defineProps<{ itemId: string; mediaSourceIndex?: number }>(); const emit = defineEmits<{ (e: 'save'): void; @@ -226,6 +235,11 @@ const emit = defineEmits<{ (e: 'cancel'): void; }>(); +interface ContentOption { + value: string; + key: string; +} + const { t } = useI18n(); const remote = useRemote(); @@ -236,6 +250,9 @@ const genres = ref([]); const search = ref(''); const loading = ref(false); const tabName = ref(); +const contentOptions = ref([]); +const contentOption = ref(); +const contentType = ref(); const premiereDate = computed(() => { if (!metadata.value?.PremiereDate) { @@ -259,13 +276,49 @@ const dateCreated = computed(() => { * Fetch data ancestors for the current item */ async function getData(): Promise { - const itemInfo = ( + let itemInfo = ( await remote.sdk.newUserApi(getUserLibraryApi).getItem({ userId: remote.auth.currentUserId ?? '', itemId: props.itemId }) ).data; + const sourceId = getItemIdFromSourceIndex(itemInfo, props.mediaSourceIndex); + + if (sourceId !== props.itemId) { + // This is another version of the same item, fetch the actual metadata for this. + itemInfo = ( + await remote.sdk.newUserApi(getUserLibraryApi).getItem({ + userId: remote.auth.currentUserId ?? '', + itemId: sourceId + }) + ).data; + + // Fetch metadata editor info for ContentTypeOptions + + const options = ( + await remote.sdk.newUserApi(getItemUpdateApi).getMetadataEditorInfo({ + itemId: sourceId + }) + ).data; + + contentOptions.value = + options?.ContentTypeOptions?.map((r) => { + if (r.Name) { + return { + // the option name + key: r.Name, + // the one that will be sent + value: r.Value ?? '' + }; + } + }).filter((r): r is ContentOption => r !== undefined) ?? []; + contentOption.value = + contentOptions.value.find((r) => r.value === options.ContentType) ?? + contentOptions.value[0]; + contentType.value = options.ContentType ?? contentOption.value.value; + } + metadata.value = itemInfo; if (!metadata.value?.Id) { @@ -301,6 +354,26 @@ async function getGenres(parentId: string): Promise { ) ?? []; } +/** + * Save metadata content type for the current item + */ +async function saveContentType(): Promise { + if (!contentOption.value) { + return; + } + + if (!metadata.value?.Id) { + return; + } + + if (contentOption.value.value !== contentType.value) { + await remote.sdk.newUserApi(getItemUpdateApi).updateItemContentType({ + itemId: metadata.value.Id, + contentType: contentOption.value.value + }); + } +} + /** * Save metadata for the current item */ @@ -360,6 +433,7 @@ async function saveMetadata(): Promise { itemId: metadata.value?.Id, baseItemDto: item }); + await saveContentType(); emit('save'); useSnackbar(t('saved'), 'success'); } catch (error) { diff --git a/frontend/src/components/Item/Metadata/MetadataEditorDialog.vue b/frontend/src/components/Item/Metadata/MetadataEditorDialog.vue index f79c5949fd9..94f45be916f 100644 --- a/frontend/src/components/Item/Metadata/MetadataEditorDialog.vue +++ b/frontend/src/components/Item/Metadata/MetadataEditorDialog.vue @@ -4,12 +4,16 @@ :model-value="dialog" :fullscreen="$vuetify.display.mobile" @update:model-value="close"> - + diff --git a/frontend/src/components/Item/Metadata/RefreshMetadataDialog.vue b/frontend/src/components/Item/Metadata/RefreshMetadataDialog.vue new file mode 100644 index 00000000000..571305265ed --- /dev/null +++ b/frontend/src/components/Item/Metadata/RefreshMetadataDialog.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/frontend/src/components/Layout/AudioControls.vue b/frontend/src/components/Layout/AudioControls.vue index 06856d6699d..81eebfffc96 100644 --- a/frontend/src/components/Layout/AudioControls.vue +++ b/frontend/src/components/Layout/AudioControls.vue @@ -71,7 +71,10 @@
- + diff --git a/frontend/src/components/Playback/TrackList.vue b/frontend/src/components/Playback/TrackList.vue index e3b044eb22c..0619464e21a 100644 --- a/frontend/src/components/Playback/TrackList.vue +++ b/frontend/src/components/Playback/TrackList.vue @@ -69,7 +69,7 @@ - + @@ -86,7 +86,11 @@