Skip to content

Commit

Permalink
refactor: address review comments about downloads
Browse files Browse the repository at this point in the history
Given initiating a download is browser-specific and we already have the "Copy stream URL"
option, we can just leave it alone and let the user fire downloads manually
to avoid extra browser shenanigans and checks
  • Loading branch information
ferferga committed Aug 16, 2023
1 parent 4e2c5b0 commit c644d39
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 268 deletions.
2 changes: 2 additions & 0 deletions frontend/locales/en-US.json
Expand Up @@ -2,6 +2,7 @@
"3DFormat": "3D format",
"NoMediaSourcesAvailable": "No media sources available",
"NoMediaStreamsAvailable": "No media streams available for the selected source",
"accept": "Accept",
"actor": "Actor",
"actors": "Actors",
"addNewPerson": "Add a new person",
Expand Down Expand Up @@ -46,6 +47,7 @@
"connect": "Connect",
"continueListening": "Continue listening",
"continueWatching": "Continue watching",
"copyPrompt": "Copy the following text into the clipboard?",
"copyStreamURL": "Copy Stream URL",
"criticRating": "Critic rating",
"customRating": "Custom rating",
Expand Down
23 changes: 14 additions & 9 deletions frontend/src/components/Dialogs/ConfirmDialog.vue
Expand Up @@ -9,10 +9,11 @@
</v-card-subtitle>

<v-divider />

<v-card-text class="d-flex text-center align-center justify-center">
{{ state.text }}
</v-card-text>
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
<v-card-text
class="d-flex text-center align-center justify-center"
v-html="innerHtml" />
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
<v-card-actions class="align-center justify-center">
<v-btn variant="elevated" color="secondary" width="8em" @click="cancel">
{{ t('cancel') }}
Expand All @@ -31,20 +32,22 @@
<script lang="ts">
import { reactive, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useConfirmDialog as vUseConfirmDialog } from '@vueuse/core';
import { sanitizeHtml } from '@/utils/html';
interface ConfirmDialogState {
title: string;
text: string;
confirmText: string;
confirmText?: string;
subtitle?: string;
confirmColor?: string;
}
const state = reactive<ConfirmDialogState>({
title: '',
text: '',
confirmText: '',
confirmText: undefined,
subtitle: undefined,
confirmColor: undefined
});
Expand Down Expand Up @@ -80,7 +83,7 @@ export async function useConfirmDialog<T>(
state.title = params.title || '';
state.subtitle = params.subtitle;
state.text = params.text || '';
state.confirmText = params.confirmText || '';
state.confirmText = params.confirmText;
state.confirmColor = params.confirmColor;
const { isCanceled } = await reveal();
Expand All @@ -96,7 +99,9 @@ export async function useConfirmDialog<T>(
</script>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const innerHtml = computed(() =>
state.text ? sanitizeHtml(state.text, true) : sanitizeHtml(t('accept'), true)
);
</script>
157 changes: 64 additions & 93 deletions frontend/src/components/Item/ItemMenu.vue
Expand Up @@ -64,8 +64,6 @@ import IMdiCloudSearch from 'virtual:icons/mdi/cloud-search-outline';
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';
Expand All @@ -82,16 +80,11 @@ import {
canInstantMix,
canRefreshMetadata,
canResume,
getItemDownloadObject,
getItemSeasonDownloadObjects,
getItemSeriesDownloadObjects
getItemDownloadUrl,
getItemSeasonDownloadMap,
getItemSeriesDownloadMap
} from '@/utils/items';
import { playbackManagerStore, taskManagerStore } from '@/store';
import {
DownloadableFile,
canBrowserDownloadItem,
downloadFiles
} from '@/utils/file-download';
type MenuOption = {
title: string;
Expand Down Expand Up @@ -299,79 +292,61 @@ const identifyItemAction = {
identifyItemDialog.value = true;
}
};
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) {
try {
await downloadFiles(downloadURLs);
} catch (error) {
console.error(error);
useSnackbar(errorMessage, 'error');
}
} else {
console.error(
'Unable to get download URL for selected item/series/season'
);
useSnackbar(errorMessage, 'error');
}
}
};
const singleDownloadAction = {
title: t('downloadItem', 1),
icon: IMdiDownload,
action: sharedDownloadAction
};
const multiDownloadAction = {
title: t('downloadItem', 2),
icon: IMdiDownloadMultiple,
action: sharedDownloadAction
};
const copyStreamURLAction = {
const copyDownloadURLAction = {
title: t('copyStreamURL'),
icon: IMdiContentCopy,
action: async (): Promise<void> => {
if (menuProps.item.Id) {
const downloadHref = getItemDownloadObject(menuProps.item.Id);
const clipboard = useClipboard();
const clipboard = useClipboard();
let streamUrls: Map<string, string> | string | undefined;
try {
if (clipboard.isSupported.value) {
if (downloadHref?.url) {
await clipboard.copy(downloadHref.url);
}
} else {
throw new ReferenceError('Unsupported clipboard operation');
if (!clipboard.isSupported.value) {
useSnackbar(t('clipboardUnsupported'), 'error');
return;
}
if (menuProps.item.Id) {
switch (menuProps.item.Type) {
case 'Season': {
streamUrls = await getItemSeasonDownloadMap(menuProps.item.Id);
break;
}
case 'Series': {
streamUrls = await getItemSeriesDownloadMap(menuProps.item.Id);
break;
}
default: {
streamUrls = getItemDownloadUrl(menuProps.item.Id);
break;
}
} catch (error) {
error instanceof ReferenceError
? useSnackbar(t('clipboardUnsupported'), 'error')
: useSnackbar(errorMessage, 'error');
}
/**
* The Map is mapped to an string like: EpisodeName: DownloadUrl
*/
const text = streamUrls
? typeof streamUrls === 'string'
? streamUrls
: [...streamUrls.entries()]
.map(([k, v]) => `(${k}) - ${v}`)
.join('\n')
: undefined;
const copyAction = async (txt: string): Promise<void> => {
await clipboard.copy(txt);
useSnackbar(t('clipboardSuccess'), 'success');
};
if (text) {
await (typeof streamUrls === 'string'
? copyAction(text)
: useConfirmDialog(async () => await copyAction(text), {
title: t('copyPrompt'),
text: text,
confirmText: t('accept')
}));
} else {
useSnackbar(errorMessage, 'error');
}
}
}
Expand Down Expand Up @@ -443,22 +418,15 @@ function getPlaybackOptions(): MenuOption[] {
/**
* 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);
function getCopyOptions(): MenuOption[] {
const copyActions: MenuOption[] = [];
const remote = useRemote();
if (['Season', 'Series'].includes(menuProps.item.Type || '')) {
copyDownloadActions.push(multiDownloadAction);
}
}
if (remote.auth.currentUser?.Policy?.EnableContentDownloading) {
copyActions.push(copyDownloadURLAction);
}
return copyDownloadActions;
return copyActions;
}
/**
Expand All @@ -483,7 +451,10 @@ function getLibraryOptions(): MenuOption[] {
}
}
if (menuProps.item.CanDelete) {
if (
remote.auth.currentUser?.Policy?.EnableContentDeletion ||
remote.auth.currentUser?.Policy?.EnableContentDeletionFromFolders
) {
libraryOptions.push(deleteItemAction);
}
Expand All @@ -494,7 +465,7 @@ const options = computed(() => {
return [
getQueueOptions(),
getPlaybackOptions(),
getCopyDownloadOptions(),
getCopyOptions(),
getLibraryOptions()
];
});
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/components/Playback/TrackList.vue
Expand Up @@ -113,11 +113,7 @@ async function fetch(): Promise<void> {
parentId: props.item.Id,
sortBy: ['SortName'],
sortOrder: [SortOrder.Ascending],
fields: [
ItemFields.MediaSources,
ItemFields.CanDelete,
ItemFields.CanDownload
]
fields: [ItemFields.MediaSources]
})
).data.Items;
}
Expand Down
9 changes: 7 additions & 2 deletions frontend/src/pages/library/_itemId/index.vue
Expand Up @@ -58,7 +58,11 @@
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client';
import {
BaseItemDto,
BaseItemKind,
ItemFields
} from '@jellyfin/sdk/lib/generated-client';
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api';
Expand Down Expand Up @@ -283,7 +287,8 @@ async function refreshItems(): Promise<void> {
filters.value.features.includes('HasThemeVideo') || undefined,
isHd: filters.value.types.includes('isHD') || undefined,
is4K: filters.value.types.includes('is4K') || undefined,
is3D: filters.value.types.includes('is3D') || undefined
is3D: filters.value.types.includes('is3D') || undefined,
fields: Object.values(ItemFields)
})
).data;
break;
Expand Down
11 changes: 0 additions & 11 deletions frontend/src/store/userLibraries.ts
Expand Up @@ -149,8 +149,6 @@ class UserLibrariesStore {
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.MediaSources,
ItemFields.CanDelete,
ItemFields.CanDownload,
ItemFields.ProviderIds
],
imageTypeLimit: 1,
Expand Down Expand Up @@ -180,8 +178,6 @@ class UserLibrariesStore {
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.MediaSources,
ItemFields.CanDelete,
ItemFields.CanDownload,
ItemFields.ProviderIds
],
imageTypeLimit: 1,
Expand Down Expand Up @@ -211,8 +207,6 @@ class UserLibrariesStore {
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.MediaSources,
ItemFields.CanDelete,
ItemFields.CanDownload,
ItemFields.ProviderIds
],
imageTypeLimit: 1,
Expand Down Expand Up @@ -244,9 +238,6 @@ class UserLibrariesStore {
fields: [
ItemFields.PrimaryImageAspectRatio,
ItemFields.MediaSources,

ItemFields.CanDelete,
ItemFields.CanDownload,
ItemFields.ProviderIds
],
imageTypeLimit: 1,
Expand Down Expand Up @@ -276,8 +267,6 @@ class UserLibrariesStore {
ItemFields.Overview,
ItemFields.PrimaryImageAspectRatio,
ItemFields.MediaSources,
ItemFields.CanDelete,
ItemFields.CanDownload,
ItemFields.ProviderIds
],
enableImageTypes: [ImageType.Backdrop, ImageType.Logo],
Expand Down

0 comments on commit c644d39

Please sign in to comment.