Skip to content

Commit

Permalink
feat: add Media Info, download, and copy to context menu
Browse files Browse the repository at this point in the history
Split from the following PR: #1951
  • Loading branch information
noaione committed Apr 28, 2023
1 parent dc5a6b6 commit 9338a7c
Show file tree
Hide file tree
Showing 18 changed files with 1,490 additions and 27 deletions.
87 changes: 71 additions & 16 deletions frontend/locales/en-US.json
@@ -1,5 +1,4 @@
{
"auto": "Automatic",
"3DFormat": "3D format",
"NoMediaSourcesAvailable": "No media sources available",
"actor": "Actor",
Expand All @@ -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.",
Expand All @@ -34,13 +34,17 @@
"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",
"continueListening": "Continue listening",
"continueWatching": "Continue watching",
"copyStreamURL": "Copy Stream URL",
"criticRating": "Critic rating",
"customRating": "Custom rating",
"darkModeToggle": "Toggle dark mode",
Expand All @@ -61,6 +65,11 @@
"disabled": "Disabled",
"discNumber": "Disc {discNumber}",
"dislikes": "Dislikes",
"download": {
"download": "Download",
"downloadAll": "Download all",
"failureToGetURL": "Failed to get URL for selected item"
},
"edit": "Edit",
"editMetadata": "Edit metadata",
"editPerson": "Edit person",
Expand Down Expand Up @@ -116,8 +125,8 @@
},
"images": "Images",
"incorrectUsernameOrPassword": "Incorrect username or password",
"instantMixQueued": "Instant mix added to queue",
"instantMix": "Instant mix",
"instantMixQueued": "Instant mix added to queue",
"item": {
"artist": {
"albums": "Albums",
Expand Down Expand Up @@ -201,20 +210,66 @@
"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",
Expand Down Expand Up @@ -340,9 +395,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"
},
Expand All @@ -351,9 +406,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",
Expand Down Expand Up @@ -468,9 +523,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": {
Expand Down Expand Up @@ -515,7 +570,6 @@
"undefined": "Undefined",
"unexpectedError": "Unexpected error",
"unhandledException": "Unhandled exception",
"unknown": "Unknown",
"units": {
"bitrate": {
"kbps": "{value} kbps",
Expand All @@ -525,6 +579,7 @@
"seconds": "{count} second | {count} seconds"
}
},
"unknown": "Unknown",
"unliked": "Unliked",
"unplayed": "Unplayed",
"upNext": "Up next",
Expand Down
130 changes: 128 additions & 2 deletions frontend/src/components/Item/ItemMenu.vue
Expand Up @@ -36,6 +36,10 @@
v-if="item.Id"
v-model:dialog="metadataDialog"
:item-id="item.Id" />
<media-detail-dialog
v-if="item.Id"
v-model:dialog="mediaInfoDialog"
:item-id="item.Id" />
</template>

<script setup lang="ts">
Expand All @@ -47,8 +51,12 @@ import { getItemRefreshApi } from '@jellyfin/sdk/lib/utils/api/item-refresh-api'
import IMdiPlaySpeed from 'virtual:icons/mdi/play-speed';
import IMdiArrowExpandUp from 'virtual:icons/mdi/arrow-expand-up';
import IMdiArrowExpandDown from 'virtual:icons/mdi/arrow-expand-down';
import IMdiContentCopy from 'virtual:icons/mdi/content-copy';
import IMdiDelete from 'virtual:icons/mdi/delete';
import IMdiDisc from 'virtual:icons/mdi/disc';
import IMdiDownload from 'virtual:icons/mdi/download';
import IMdiDownloadMultiple from 'virtual:icons/mdi/download-multiple';
import IMdiInformation from 'virtual:icons/mdi/information';
import IMdiPlaylistMinus from 'virtual:icons/mdi/playlist-minus';
import IMdiPlaylistPlus from 'virtual:icons/mdi/playlist-plus';
import IMdiPencilOutline from 'virtual:icons/mdi/pencil-outline';
Expand All @@ -58,9 +66,21 @@ import IMdiRefresh from 'virtual:icons/mdi/refresh';
import { getLibraryApi } from '@jellyfin/sdk/lib/utils/api/library-api';
import { useRoute, useRouter } from 'vue-router';
import { useRemote, useSnackbar, useConfirmDialog } from '@/composables';
import { canInstantMix, canResume } from '@/utils/items';
import {
canInstantMix,
canResume,
getItemDownloadObject,
getItemSeasonDownloadObjects,
getItemSeriesDownloadObjects
} from '@/utils/items';
import { TaskType } from '@/store/taskManager';
import { playbackManagerStore, taskManagerStore } from '@/store';
import {
DownloadableFile,
canBrowserDownloadItem,
downloadFiles
} from '@/utils/file-download';
import { writeToClipboard } from '@/utils/clipboard';
type MenuOption = {
title: string;
Expand Down Expand Up @@ -95,6 +115,7 @@ const show = ref(false);
const positionX = ref<number | undefined>(undefined);
const positionY = ref<number | undefined>(undefined);
const metadataDialog = ref(false);
const mediaInfoDialog = ref(false);
const playbackManager = playbackManagerStore();
const taskManager = taskManagerStore();
const errorMessage = t('errors.anErrorHappened');
Expand Down Expand Up @@ -185,6 +206,13 @@ const instantMixAction = {
/**
* Item related actions
*/
const mediaInfoAction = {
title: t('mediaInfo.title'),
icon: IMdiInformation,
action: (): void => {
mediaInfoDialog.value = true;
}
};
const refreshLibraryAction = {
title: t('refreshLibrary'),
icon: IMdiRefresh,
Expand Down Expand Up @@ -252,6 +280,65 @@ const deleteItemAction = {
);
}
};
const sharedDownloadAction = async (): Promise<void> => {
if (menuProps.item.Id && menuProps.item.Type && menuProps.item.Path) {
let downloadURLs: DownloadableFile[] = [];
switch (menuProps.item.Type) {
case 'Season': {
downloadURLs = await getItemSeasonDownloadObjects(menuProps.item.Id);
break;
}
case 'Series': {
downloadURLs = await getItemSeriesDownloadObjects(menuProps.item.Id);
break;
}
default: {
const url = getItemDownloadObject(
menuProps.item.Id,
menuProps.item.Path
);
if (url) {
downloadURLs = [url];
}
break;
}
}
if (downloadURLs) {
await downloadFiles(downloadURLs);
} else {
useSnackbar(t('download.failureToGetURL'), 'error');
}
}
};
const singleDownloadAction = {
title: t('download.download'),
icon: IMdiDownload,
action: sharedDownloadAction
};
const multiDownloadAction = {
title: t('download.downloadAll'),
icon: IMdiDownloadMultiple,
action: sharedDownloadAction
};
const copyStreamURLAction = {
title: t('copyStreamURL'),
icon: IMdiContentCopy,
action: async (): Promise<void> => {
if (menuProps.item.Id) {
const downloadHref = getItemDownloadObject(menuProps.item.Id);
if (downloadHref && downloadHref.url) {
await copy(downloadHref.url);
} else {
useSnackbar(t('download.failureToGetURL'), 'error');
}
}
}
};
/**
* == END OF ACTIONS ==
*/
Expand Down Expand Up @@ -316,12 +403,37 @@ function getPlaybackOptions(): MenuOption[] {
return playbackOptions;
}
/**
* Copy and download action for the current selected item
*/
function getCopyDownloadOptions(): MenuOption[] {
const copyDownloadActions: MenuOption[] = [];
if (menuProps.item.CanDownload) {
copyDownloadActions.push(copyStreamURLAction);
if (canBrowserDownloadItem(menuProps.item)) {
copyDownloadActions.push(singleDownloadAction);
if (['Season', 'Series'].includes(menuProps.item.Type || '')) {
copyDownloadActions.push(multiDownloadAction);
}
}
}
return copyDownloadActions;
}
/**
* Library options for libraries
*/
function getLibraryOptions(): MenuOption[] {
const libraryOptions: MenuOption[] = [];
if (menuProps.item.MediaSources) {
libraryOptions.push(mediaInfoAction);
}
if (
remote.auth.currentUser?.Policy?.IsAdministrator &&
['Folder', 'CollectionFolder', 'UserView'].includes(
Expand All @@ -343,7 +455,12 @@ function getLibraryOptions(): MenuOption[] {
}
const options = computed(() => {
return [getQueueOptions(), getPlaybackOptions(), getLibraryOptions()];
return [
getQueueOptions(),
getPlaybackOptions(),
getCopyDownloadOptions(),
getLibraryOptions()
];
});
/**
Expand All @@ -365,6 +482,15 @@ function onActivatorClick(): void {
positionY.value = undefined;
}
const copy = async (text: string): Promise<void> => {
try {
await writeToClipboard(text);
useSnackbar(t('clipboardSuccess'), 'success');
} catch {
useSnackbar(t('clipboardFail'), 'error');
}
};
onMounted(() => {
const parentHtml = parent?.subTree.el;
Expand Down

0 comments on commit 9338a7c

Please sign in to comment.