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 23, 2023
1 parent 6fdc59f commit d8ed9a6
Show file tree
Hide file tree
Showing 18 changed files with 1,487 additions and 27 deletions.
85 changes: 70 additions & 15 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,6 +34,9 @@
"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",
Expand All @@ -58,6 +61,12 @@
"disabled": "Disabled",
"discNumber": "Disc {discNumber}",
"dislikes": "Dislikes",
"download": {
"copyStreamUrl": "Copy Stream URL",
"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 @@ -198,20 +207,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 @@ -337,9 +392,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 @@ -348,9 +403,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 @@ -465,9 +520,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 @@ -512,7 +567,6 @@
"undefined": "Undefined",
"unexpectedError": "Unexpected error",
"unhandledException": "Unhandled exception",
"unknown": "Unknown",
"units": {
"bitrate": {
"kbps": "{value} kbps",
Expand All @@ -522,6 +576,7 @@
"seconds": "{count} second | {count} seconds"
}
},
"unknown": "Unknown",
"unliked": "Unliked",
"unplayed": "Unplayed",
"upNext": "Up next",
Expand Down
139 changes: 137 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,17 +51,30 @@ 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 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';
import IMdiShuffle from 'virtual:icons/mdi/shuffle';
import IMdiReplay from 'virtual:icons/mdi/replay';
import IMdiRefresh from 'virtual:icons/mdi/refresh';
import { useRemote, useSnackbar } 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 { isEdgeUWP, isPs4, isXbox, isTv } from '@/utils/browser-detection';
import downloadFiles, { DownloadableFile } from '@/utils/file-download';
import { writeToClipboard } from '@/utils/clipboard';
type MenuOption = {
title: string;
Expand Down Expand Up @@ -90,6 +107,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 isItemRefreshing = computed(
Expand Down Expand Up @@ -224,12 +242,115 @@ function getPlaybackOptions(): MenuOption[] {
return playbackOptions;
}
/**
* Check if the browser can download the item
*/
function browserCanDownload(): boolean {
return (
!isEdgeUWP() &&
!isTv() &&
!isXbox() &&
!isPs4() &&
menuProps.item.Type !== 'Book'
);
}
/**
* Download action for the currently selected item
*/
async function downloadAction(): 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');
}
}
}
/**
* Copy action for the current selected item
*/
function getCopyDownloadOptions(): MenuOption[] {
const copyDownloadActions: MenuOption[] = [];
if (menuProps.item.CanDownload) {
copyDownloadActions.push({
title: t('download.copyStreamUrl'),
icon: IMdiContentCopy,
action: async (): Promise<void> => {
if (menuProps.item.Id) {
const downloadHref = getItemDownloadObject(menuProps.item.Id);
if (downloadHref) {
await copy(downloadHref.url);
} else {
useSnackbar(t('download.failureToGetURL'), 'error');
}
}
}
});
if (browserCanDownload()) {
copyDownloadActions.push({
title: t('download.download'),
icon: IMdiDownload,
action: downloadAction
});
if (['Season', 'Series'].includes(menuProps.item.Type || '')) {
copyDownloadActions.push({
title: t('download.downloadAll'),
icon: IMdiDownloadMultiple,
action: downloadAction
});
}
}
}
return copyDownloadActions;
}
/**
* Library options for libraries
*/
function getLibraryOptions(): MenuOption[] {
const libraryOptions: MenuOption[] = [];
if (menuProps.item.MediaSources) {
libraryOptions.push({
title: t('mediaInfo.title'),
icon: IMdiInformation,
action: (): void => {
mediaInfoDialog.value = true;
}
});
}
if (
remote.auth.currentUser?.Policy?.IsAdministrator &&
['Folder', 'CollectionFolder', 'UserView'].includes(
Expand Down Expand Up @@ -280,7 +401,12 @@ function getLibraryOptions(): MenuOption[] {
}
const options = computed(() => {
return [getQueueOptions(), getPlaybackOptions(), getLibraryOptions()];
return [
getQueueOptions(),
getPlaybackOptions(),
getCopyDownloadOptions(),
getLibraryOptions()
];
});
/**
Expand All @@ -302,6 +428,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 d8ed9a6

Please sign in to comment.