mirror of
https://github.com/jellyfin/jellyfin-vue.git
synced 2024-10-07 03:23:37 +00:00
chore: update MetadataEditor, MetadataEditorDialog, and PersonEditor to setup composition API
This commit is contained in:
parent
5fb2054336
commit
7d4278ccdf
@ -33,7 +33,7 @@
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<metadata-editor-dialog
|
||||
v-if="metadataDialog"
|
||||
v-if="item.Id"
|
||||
v-model:dialog="metadataDialog"
|
||||
:item-id="item.Id" />
|
||||
</div>
|
||||
|
@ -1,137 +1,151 @@
|
||||
<template>
|
||||
<v-card height="100%" class="d-flex flex-column metadata-editor">
|
||||
<v-card-title>{{ $t('editMetadata') }}</v-card-title>
|
||||
<v-card
|
||||
v-if="metadata"
|
||||
height="100%"
|
||||
class="d-flex flex-column metadata-editor">
|
||||
<v-card-title>{{ t('editMetadata') }}</v-card-title>
|
||||
<v-card-subtitle class="pb-3">
|
||||
{{ metadata.Path }}
|
||||
</v-card-subtitle>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0 flex-grow-1">
|
||||
<v-tabs v-model="tabName" :vertical="!$vuetify.display.mobile">
|
||||
<v-tab href="#general">{{ $t('general') }}</v-tab>
|
||||
<v-tab href="#details">{{ $t('details') }}</v-tab>
|
||||
<v-tab href="#castAndCrew">{{ $t('castAndCrew') }}</v-tab>
|
||||
<v-tab href="#images">{{ $t('images') }}</v-tab>
|
||||
<v-card-text
|
||||
class="pa-0 flex-grow-1"
|
||||
:class="{
|
||||
'd-flex': !$vuetify.display.mobile,
|
||||
'flex-row': !$vuetify.display.mobile
|
||||
}">
|
||||
<v-tabs
|
||||
v-model="tabName"
|
||||
:direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'">
|
||||
<v-tab value="general">{{ t('general') }}</v-tab>
|
||||
<v-tab value="details">{{ t('details') }}</v-tab>
|
||||
<v-tab value="castAndCrew">{{ t('castAndCrew') }}</v-tab>
|
||||
<v-tab value="images">{{ t('images') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tabName" class="pa-2 flex-fill">
|
||||
<v-window-item value="general">
|
||||
<v-text-field
|
||||
v-model="metadata.Name"
|
||||
variant="outlined"
|
||||
:label="t('metadata.title')" />
|
||||
<v-text-field
|
||||
v-model="metadata.OriginalTitle"
|
||||
variant="outlined"
|
||||
:label="t('originalTitle')" />
|
||||
<v-text-field
|
||||
v-model="metadata.ForcedSortName"
|
||||
variant="outlined"
|
||||
:label="t('sortTitle')" />
|
||||
<v-text-field
|
||||
v-model="metadata.Taglines"
|
||||
variant="outlined"
|
||||
:label="t('tagline')" />
|
||||
<v-textarea
|
||||
v-model="metadata.Overview"
|
||||
variant="outlined"
|
||||
no-resize
|
||||
rows="4"
|
||||
:label="t('overview')" />
|
||||
</v-window-item>
|
||||
<v-window-item value="details">
|
||||
<date-input
|
||||
:value="dateCreated"
|
||||
:label="t('dateAdded')"
|
||||
@update:date="
|
||||
(value) => formatAndAssignDate('DateCreated', value)
|
||||
" />
|
||||
<v-row>
|
||||
<v-col sm="6" cols="12">
|
||||
<v-text-field
|
||||
v-model="metadata.CommunityRating"
|
||||
variant="outlined"
|
||||
:label="t('communityRating')" />
|
||||
</v-col>
|
||||
<v-col sm="6" cols="12">
|
||||
<v-text-field
|
||||
v-model="metadata.CriticRating"
|
||||
variant="outlined"
|
||||
:label="t('criticRating')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-tabs v-model="tabName" class="pa-3">
|
||||
<v-tab value="general">
|
||||
<v-text-field
|
||||
v-model="metadata.Name"
|
||||
variant="outlined"
|
||||
:label="$t('metadata.title')" />
|
||||
<v-text-field
|
||||
v-model="metadata.OriginalTitle"
|
||||
variant="outlined"
|
||||
:label="$t('originalTitle')" />
|
||||
<v-text-field
|
||||
v-model="metadata.ForcedSortName"
|
||||
variant="outlined"
|
||||
:label="$t('sortTitle')" />
|
||||
<v-text-field
|
||||
v-model="metadata.Taglines"
|
||||
variant="outlined"
|
||||
:label="$t('tagline')" />
|
||||
<v-textarea
|
||||
v-model="metadata.Overview"
|
||||
variant="outlined"
|
||||
no-resize
|
||||
rows="4"
|
||||
:label="$t('overview')" />
|
||||
</v-tab>
|
||||
<v-tab value="details">
|
||||
<date-input
|
||||
:value="dateCreated"
|
||||
:label="$t('dateAdded')"
|
||||
@update:date="(value) => saveDate('DateCreated', value)" />
|
||||
<v-row>
|
||||
<v-col sm="6" cols="12">
|
||||
<v-text-field
|
||||
v-model="metadata.CommunityRating"
|
||||
variant="outlined"
|
||||
:label="$t('communityRating')" />
|
||||
</v-col>
|
||||
<v-col sm="6" cols="12">
|
||||
<v-text-field
|
||||
v-model="metadata.CriticRating"
|
||||
variant="outlined"
|
||||
:label="$t('criticRating')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<date-input
|
||||
:value="premiereDate"
|
||||
:label="$t('releaseDate')"
|
||||
@update:date="(value) => saveDate('PremiereDate', value)" />
|
||||
<v-text-field
|
||||
v-model="metadata.ProductionYear"
|
||||
variant="outlined"
|
||||
:label="$t('year')" />
|
||||
<v-text-field
|
||||
v-model="metadata.OfficialRating"
|
||||
variant="outlined"
|
||||
:label="$t('parentalRating')" />
|
||||
<v-text-field
|
||||
v-model="metadata.CustomRating"
|
||||
variant="outlined"
|
||||
:label="$t('customRating')" />
|
||||
<v-combobox
|
||||
v-model="metadata.Genres"
|
||||
v-model:search-input="search"
|
||||
:items="genres"
|
||||
:label="$t('genres')"
|
||||
hide-selected
|
||||
multiple
|
||||
variant="outlined"
|
||||
small-chips>
|
||||
<template #no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
{{ $t('metadataNoResultsMatching', { search: search }) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<v-combobox
|
||||
v-model="metadata.Tags"
|
||||
v-model:search-input="search"
|
||||
:items="genres"
|
||||
:label="$t('tags')"
|
||||
hide-selected
|
||||
multiple
|
||||
variant="outlined"
|
||||
small-chips>
|
||||
<template #no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
{{ $t('metadataNoResultsMatching', { search: search }) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-tab>
|
||||
<v-tab value="castAndCrew">
|
||||
<v-list lines="two">
|
||||
<v-list-item @click="(e) => handlePersonEdit()">
|
||||
<date-input
|
||||
:value="premiereDate"
|
||||
:label="t('releaseDate')"
|
||||
@update:date="
|
||||
(value) => formatAndAssignDate('PremiereDate', value)
|
||||
" />
|
||||
<v-text-field
|
||||
v-model="metadata.ProductionYear"
|
||||
variant="outlined"
|
||||
:label="t('year')" />
|
||||
<v-text-field
|
||||
v-model="metadata.OfficialRating"
|
||||
variant="outlined"
|
||||
:label="t('parentalRating')" />
|
||||
<v-text-field
|
||||
v-model="metadata.CustomRating"
|
||||
variant="outlined"
|
||||
:label="t('customRating')" />
|
||||
<v-combobox
|
||||
v-model="metadata.Genres"
|
||||
:items="genres"
|
||||
:label="t('genres')"
|
||||
hide-selected
|
||||
multiple
|
||||
variant="outlined"
|
||||
@update:search="(s) => (search = s)">
|
||||
<template #no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
{{ $t('addNewPerson') }}
|
||||
{{ t('metadataNoResultsMatching', { search: search }) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-action>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<v-combobox
|
||||
v-model="metadata.Tags"
|
||||
:items="genres"
|
||||
:label="t('tags')"
|
||||
hide-selected
|
||||
multiple
|
||||
variant="outlined"
|
||||
@update:search="(s) => (search = s)">
|
||||
<template #no-data>
|
||||
<v-list-item>
|
||||
<v-list-item-title>
|
||||
{{ t('metadataNoResultsMatching', { search: search }) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-window-item>
|
||||
<v-window-item value="castAndCrew">
|
||||
<v-list lines="two">
|
||||
<v-list-item :title="t('addNewPerson')" @click="onPersonAdd">
|
||||
<template #append>
|
||||
<v-avatar>
|
||||
<v-icon>
|
||||
<i-mdi-plus-circle />
|
||||
</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="(item, i) in metadata.People"
|
||||
:key="`${item.Id}-${i}`"
|
||||
@click="handlePersonEdit(item)">
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="(item, i) in metadata.People"
|
||||
:key="`${item.Id}-${i}`"
|
||||
:title="item.Name ?? undefined"
|
||||
:subtitle="(item.Role || item.Type) ?? undefined"
|
||||
@click="onPersonEdit(item)">
|
||||
<template #prepend>
|
||||
<v-avatar>
|
||||
<v-img
|
||||
v-if="item.PrimaryImageTag"
|
||||
v-if="item.Id && item.PrimaryImageTag"
|
||||
:src="
|
||||
$remote.sdk.api?.getItemImageUrl(
|
||||
item.Id || '',
|
||||
remote.sdk.api?.getItemImageUrl(
|
||||
item.Id,
|
||||
ImageType.Primary
|
||||
)
|
||||
" />
|
||||
@ -139,25 +153,25 @@
|
||||
<i-mdi-account />
|
||||
</v-icon>
|
||||
</v-avatar>
|
||||
<v-list-item-title>
|
||||
{{ item.Name }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="mt-1">
|
||||
{{ item.Role || item.Type }}
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-action @click.stop="handlePersonDel(i)">
|
||||
</template>
|
||||
<template #append>
|
||||
<v-avatar @click.stop="onPersonDel(i)">
|
||||
<v-icon>
|
||||
<i-mdi-delete />
|
||||
</v-icon>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-tab>
|
||||
<v-tab value="images">
|
||||
<image-editor :metadata="metadata" />
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
</v-tabs>
|
||||
</v-avatar>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<person-editor
|
||||
:person="person"
|
||||
@update:person="onPersonSave"
|
||||
@close="person = undefined" />
|
||||
</v-window-item>
|
||||
<v-window-item value="images">
|
||||
<image-editor :metadata="metadata" />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
@ -172,8 +186,8 @@
|
||||
width="8em"
|
||||
color="secondary"
|
||||
class="mr-1"
|
||||
@click="$emit('cancel')">
|
||||
{{ $t('cancel') }}
|
||||
@click="emit('cancel')">
|
||||
{{ t('cancel') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="flat"
|
||||
@ -181,19 +195,15 @@
|
||||
color="primary"
|
||||
:loading="loading"
|
||||
@click="saveMetadata">
|
||||
{{ $t('save') }}
|
||||
{{ t('save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
<person-editor
|
||||
v-model:dialog="dialog"
|
||||
:person="person"
|
||||
@update:person="handlePersonUpdate"
|
||||
@update:dialog="handleDialogUpdate" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { pick, set } from 'lodash-es';
|
||||
import { AxiosError } from 'axios';
|
||||
import {
|
||||
@ -206,241 +216,224 @@ import { getUserLibraryApi } from '@jellyfin/sdk/lib/utils/api/user-library-api'
|
||||
import { getGenresApi } from '@jellyfin/sdk/lib/utils/api/genres-api';
|
||||
import { getItemUpdateApi } from '@jellyfin/sdk/lib/utils/api/item-update-api';
|
||||
import { format, formatISO } from 'date-fns';
|
||||
import { useDateFns, useSnackbar } from '@/composables';
|
||||
import { useDateFns, useRemote, useSnackbar } from '@/composables';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
itemId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
forceRefresh: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
useSnackbar,
|
||||
ImageType
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
metadata: {} as BaseItemDto,
|
||||
menu: false,
|
||||
dialog: false,
|
||||
person: null as BaseItemPerson | null,
|
||||
genres: [] as BaseItemDto[] | null | undefined,
|
||||
search: '',
|
||||
loading: false,
|
||||
tabName: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
premiereDate(): string {
|
||||
if (!this.metadata.PremiereDate) {
|
||||
return '';
|
||||
}
|
||||
const props = defineProps<{ itemId: string }>();
|
||||
|
||||
return useDateFns(
|
||||
format,
|
||||
new Date(this.metadata.PremiereDate),
|
||||
'yyyy-MM-dd'
|
||||
).value;
|
||||
},
|
||||
dateCreated(): string {
|
||||
if (!this.metadata.DateCreated) {
|
||||
return '';
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: 'save'): void;
|
||||
(e: 'update:forceRefresh'): void;
|
||||
(e: 'cancel'): void;
|
||||
}>();
|
||||
|
||||
return useDateFns(
|
||||
format,
|
||||
new Date(this.metadata.DateCreated),
|
||||
'yyyy-MM-dd'
|
||||
).value;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
itemId(): void {
|
||||
this.getData();
|
||||
},
|
||||
forceRefresh(): void {
|
||||
this.getData();
|
||||
this.$emit('update:forceRefresh', false);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getData();
|
||||
},
|
||||
methods: {
|
||||
async getData(): Promise<void> {
|
||||
await this.fetchItemInfo();
|
||||
const { t } = useI18n();
|
||||
const remote = useRemote();
|
||||
|
||||
if (!this.metadata.Id) {
|
||||
return;
|
||||
}
|
||||
const metadata = ref<BaseItemDto>();
|
||||
const menu = ref(false);
|
||||
const person = ref<BaseItemPerson>();
|
||||
const genres = ref<string[]>([]);
|
||||
const search = ref('');
|
||||
const loading = ref(false);
|
||||
const tabName = ref<string>();
|
||||
|
||||
const ancestors = await this.$remote.sdk
|
||||
.newUserApi(getLibraryApi)
|
||||
.getAncestors({
|
||||
itemId: this.metadata.Id,
|
||||
userId: this.$remote.auth.currentUserId
|
||||
});
|
||||
const libraryInfo = ancestors.data.find(
|
||||
(index) => index.Type === 'CollectionFolder'
|
||||
);
|
||||
|
||||
if (!libraryInfo || !libraryInfo.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getGenres(libraryInfo.Id);
|
||||
},
|
||||
async fetchItemInfo(): Promise<void> {
|
||||
const itemInfo = (
|
||||
await this.$remote.sdk.newUserApi(getUserLibraryApi).getItem({
|
||||
userId: this.$remote.auth.currentUserId || '',
|
||||
itemId: this.itemId
|
||||
})
|
||||
).data;
|
||||
|
||||
this.$data.metadata = itemInfo;
|
||||
},
|
||||
async getGenres(parentId: string): Promise<void> {
|
||||
this.genres =
|
||||
(
|
||||
await this.$remote.sdk.newUserApi(getGenresApi).getGenres({
|
||||
parentId
|
||||
})
|
||||
).data.Items?.map((index) => index.Name).filter(
|
||||
(genre): genre is string => !!genre
|
||||
) ?? [];
|
||||
},
|
||||
async saveMetadata(): Promise<void> {
|
||||
const item = pick(this.metadata, [
|
||||
'Id',
|
||||
'Name',
|
||||
'OriginalTitle',
|
||||
'ForcedSortName',
|
||||
'CommunityRating',
|
||||
'CriticRating',
|
||||
'IndexNumber',
|
||||
'AirsBeforeSeasonNumber',
|
||||
'AirsAfterSeasonNumber',
|
||||
'AirsBeforeEpisodeNumber',
|
||||
'ParentIndexNumber',
|
||||
'DisplayOrder',
|
||||
'Album',
|
||||
'AlbumArtists',
|
||||
'ArtistItems',
|
||||
'Overview',
|
||||
'Status',
|
||||
'AirDays',
|
||||
'AirTime',
|
||||
'Genres',
|
||||
'Tags',
|
||||
'Studios',
|
||||
'PremiereDate',
|
||||
'DateCreated',
|
||||
'EndDate',
|
||||
'ProductionYear',
|
||||
'AspectRatio',
|
||||
'Video3DFormat',
|
||||
'OfficialRating',
|
||||
'CustomRating',
|
||||
'People',
|
||||
'LockData',
|
||||
'LockedFields',
|
||||
'ProviderIds',
|
||||
'PreferredMetadataLanguage',
|
||||
'PreferredMetadataCountryCode',
|
||||
'Taglines'
|
||||
]);
|
||||
|
||||
try {
|
||||
this.loading = true;
|
||||
|
||||
if (!this.metadata.Id) {
|
||||
throw new Error('Expected metadata to have id');
|
||||
}
|
||||
|
||||
await this.$remote.sdk.newUserApi(getItemUpdateApi).updateItem({
|
||||
itemId: this.metadata.Id,
|
||||
baseItemDto: item
|
||||
});
|
||||
this.$emit('save');
|
||||
this.loading = false;
|
||||
this.useSnackbar(this.$t('saved'), 'success');
|
||||
} catch (error) {
|
||||
// TODO: This whole block should be removed - we should verify that the data is correct client-side before posting to server
|
||||
// not expecting bad request messages.
|
||||
// TODO: Revise similar blocks like this through the entire codebase.
|
||||
let errorMessage = this.$t('unexpectedError');
|
||||
|
||||
if (error instanceof AxiosError && error.response?.status === 400) {
|
||||
errorMessage = this.$t('badRequest');
|
||||
}
|
||||
|
||||
this.useSnackbar(errorMessage, 'error');
|
||||
}
|
||||
},
|
||||
saveDate(key: string, date: string): void {
|
||||
this.menu = false;
|
||||
set(this.metadata, key, formatISO(new Date(date)));
|
||||
},
|
||||
handlePersonEdit(item: BaseItemPerson): void {
|
||||
this.person = item;
|
||||
this.dialog = true;
|
||||
},
|
||||
handlePersonUpdate(item: BaseItemPerson): void {
|
||||
if (!this.metadata.People) {
|
||||
this.metadata.People = [];
|
||||
}
|
||||
|
||||
const target = this.metadata.People?.find(
|
||||
(person) => person.Id === item.Id
|
||||
);
|
||||
|
||||
if (target) {
|
||||
Object.assign(target, item);
|
||||
} else {
|
||||
this.metadata.People.push(item);
|
||||
}
|
||||
},
|
||||
handleDialogUpdate(result: boolean): void {
|
||||
this.dialog = result;
|
||||
},
|
||||
handlePersonDel(index: number): void {
|
||||
(this.metadata.People ?? []).splice(index, 1);
|
||||
}
|
||||
const premiereDate = computed(() => {
|
||||
if (!metadata.value?.PremiereDate) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return useDateFns(format, new Date(metadata.value.PremiereDate), 'yyyy-MM-dd')
|
||||
.value;
|
||||
});
|
||||
|
||||
const dateCreated = computed(() => {
|
||||
if (!metadata.value?.DateCreated) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return useDateFns(format, new Date(metadata.value.DateCreated), 'yyyy-MM-dd')
|
||||
.value;
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch data ancestors for the current item
|
||||
*/
|
||||
async function getData(): Promise<void> {
|
||||
const itemInfo = (
|
||||
await remote.sdk.newUserApi(getUserLibraryApi).getItem({
|
||||
userId: remote.auth.currentUserId ?? '',
|
||||
itemId: props.itemId
|
||||
})
|
||||
).data;
|
||||
|
||||
metadata.value = itemInfo;
|
||||
|
||||
if (!metadata.value?.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ancestors = await remote.sdk.newUserApi(getLibraryApi).getAncestors({
|
||||
userId: remote.auth.currentUserId ?? '',
|
||||
itemId: metadata.value.Id
|
||||
});
|
||||
const libraryInfo = ancestors.data.find(
|
||||
(index) => index.Type === 'CollectionFolder'
|
||||
);
|
||||
|
||||
if (!libraryInfo?.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
getGenres(libraryInfo.Id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get genres associated with the current item
|
||||
*/
|
||||
async function getGenres(parentId: string): Promise<void> {
|
||||
genres.value =
|
||||
(
|
||||
await remote.sdk.newUserApi(getGenresApi).getGenres({
|
||||
parentId
|
||||
})
|
||||
).data.Items?.map((index) => index.Name).filter(
|
||||
(genre): genre is string => !!genre
|
||||
) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save metadata for the current item
|
||||
*/
|
||||
async function saveMetadata(): Promise<void> {
|
||||
if (!metadata.value || !metadata.value.Id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = pick(metadata.value, [
|
||||
'Id',
|
||||
'Name',
|
||||
'OriginalTitle',
|
||||
'ForcedSortName',
|
||||
'CommunityRating',
|
||||
'CriticRating',
|
||||
'IndexNumber',
|
||||
'AirsBeforeSeasonNumber',
|
||||
'AirsAfterSeasonNumber',
|
||||
'AirsBeforeEpisodeNumber',
|
||||
'ParentIndexNumber',
|
||||
'DisplayOrder',
|
||||
'Album',
|
||||
'AlbumArtists',
|
||||
'ArtistItems',
|
||||
'Overview',
|
||||
'Status',
|
||||
'AirDays',
|
||||
'AirTime',
|
||||
'Genres',
|
||||
'Tags',
|
||||
'Studios',
|
||||
'PremiereDate',
|
||||
'DateCreated',
|
||||
'EndDate',
|
||||
'ProductionYear',
|
||||
'AspectRatio',
|
||||
'Video3DFormat',
|
||||
'OfficialRating',
|
||||
'CustomRating',
|
||||
'People',
|
||||
'LockData',
|
||||
'LockedFields',
|
||||
'ProviderIds',
|
||||
'PreferredMetadataLanguage',
|
||||
'PreferredMetadataCountryCode',
|
||||
'Taglines'
|
||||
]);
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
if (!metadata.value.Id) {
|
||||
throw new Error('Expected metadata to have id');
|
||||
}
|
||||
|
||||
await remote.sdk.newUserApi(getItemUpdateApi).updateItem({
|
||||
itemId: metadata.value?.Id,
|
||||
baseItemDto: item
|
||||
});
|
||||
emit('save');
|
||||
useSnackbar(t('saved'), 'success');
|
||||
} catch (error) {
|
||||
// TODO: This whole block should be removed - we should verify that the data is correct client-side before posting to server
|
||||
// not expecting bad request messages.
|
||||
// TODO: Revise similar blocks like this through the entire codebase.
|
||||
let errorMessage = t('unexpectedError');
|
||||
|
||||
if (error instanceof AxiosError && error.response?.status === 400) {
|
||||
errorMessage = t('badRequest');
|
||||
}
|
||||
|
||||
useSnackbar(errorMessage, 'error');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats and updates dates
|
||||
*/
|
||||
function formatAndAssignDate(key: keyof BaseItemDto, date: string): void {
|
||||
if (!metadata.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.value = false;
|
||||
set(metadata.value, key, formatISO(new Date(date)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle adding a person
|
||||
*/
|
||||
function onPersonAdd(): void {
|
||||
onPersonEdit({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle editing a person
|
||||
*/
|
||||
function onPersonEdit(item: BaseItemPerson): void {
|
||||
person.value = item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving a person after editing
|
||||
*/
|
||||
function onPersonSave(item: BaseItemPerson): void {
|
||||
if (!metadata.value?.People) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.Id) {
|
||||
// undefined id means that the person was newly added
|
||||
metadata.value.People.push(item);
|
||||
} else {
|
||||
metadata.value.People = metadata.value.People.map((p) =>
|
||||
p.Id === item.Id ? item : p
|
||||
);
|
||||
}
|
||||
|
||||
person.value = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle deleting a person
|
||||
*/
|
||||
function onPersonDel(index: number): void {
|
||||
if (!metadata.value?.People) {
|
||||
return;
|
||||
}
|
||||
|
||||
metadata.value.People.splice(index, 1);
|
||||
}
|
||||
|
||||
watch(() => props.itemId, getData, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.person-icon {
|
||||
background-color: rgb(var(--v-secondary-darken1));
|
||||
}
|
||||
|
||||
:deep(.v-card__text) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.v-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.v-tabs-bar) {
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--v-secondary-lighten1);
|
||||
}
|
||||
|
||||
:deep(.v-tabs-items) {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
:deep(.v-card__actions) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,42 +3,24 @@
|
||||
content-class="metadata-dialog"
|
||||
:model-value="dialog"
|
||||
:fullscreen="$vuetify.display.mobile"
|
||||
width="50vw"
|
||||
@click:outside="$emit('update:dialog', false)">
|
||||
<metadata-editor
|
||||
v-model:force-refresh="forceRefresh"
|
||||
:item-id="itemId"
|
||||
@cancel="close"
|
||||
@save="close" />
|
||||
@update:model-value="close">
|
||||
<metadata-editor :item-id="itemId" @cancel="close" @save="close" />
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup lang="ts">
|
||||
defineProps<{ dialog: boolean; itemId: string }>();
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
dialog: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
itemId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
forceRefresh: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close(): void {
|
||||
this.forceRefresh = true;
|
||||
this.$emit('update:dialog', false);
|
||||
}
|
||||
}
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:dialog', isOpen: boolean): void;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* Close the dialog
|
||||
*/
|
||||
function close(): void {
|
||||
emit('update:dialog', false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<v-dialog :model-value="dialog" max-width="30%" @click:outside="handleCancel">
|
||||
<v-dialog
|
||||
max-width="30%"
|
||||
:model-value="person !== undefined"
|
||||
@update:model-value="emit('close')">
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('editPerson') }}</v-card-title>
|
||||
<v-card-title>{{ t('editPerson') }}</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-3">
|
||||
<v-row>
|
||||
<v-col cols="4">
|
||||
<v-avatar size="160" class="ml-2">
|
||||
<v-img
|
||||
v-if="person && person.PrimaryImageTag"
|
||||
v-if="person?.Id && person?.PrimaryImageTag"
|
||||
:src="
|
||||
$remote.sdk.api?.getItemImageUrl(person.Id, ImageType.Primary)
|
||||
" />
|
||||
@ -18,21 +21,23 @@
|
||||
</v-avatar>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-form @submit.prevent="handleSubmit">
|
||||
<v-form v-if="editState" @submit.prevent="onSubmit">
|
||||
<v-text-field
|
||||
v-model="editState.Name"
|
||||
variant="outlined"
|
||||
:label="$t('name')" />
|
||||
:label="t('name')" />
|
||||
<v-select
|
||||
v-model="editState.Type"
|
||||
:items="options"
|
||||
:label="$t('type')"
|
||||
:label="t('type')"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
variant="outlined" />
|
||||
<v-text-field
|
||||
v-if="editState.Type === 'Actor'"
|
||||
v-model="editState.Role"
|
||||
variant="outlined"
|
||||
:label="$t('role')" />
|
||||
:label="t('role')" />
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@ -45,74 +50,57 @@
|
||||
'justify-center': $vuetify.display.mobile
|
||||
}">
|
||||
<v-spacer />
|
||||
<v-btn variant="flat" width="8em" class="mr-1" @click="handleCancel">
|
||||
{{ $t('cancel') }}
|
||||
<v-btn variant="flat" width="8em" class="mr-1" @click="emit('close')">
|
||||
{{ t('cancel') }}
|
||||
</v-btn>
|
||||
<v-btn variant="flat" width="8em" color="primary" type="submit">
|
||||
{{ $t('save') }}
|
||||
<v-btn variant="flat" width="8em" color="primary" @click="onSubmit">
|
||||
{{ t('save') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { BaseItemPerson, ImageType } from '@jellyfin/sdk/lib/generated-client';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
person: {
|
||||
type: Object,
|
||||
default: (): BaseItemPerson => ({
|
||||
Name: '',
|
||||
Type: '',
|
||||
Role: ''
|
||||
})
|
||||
},
|
||||
dialog: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
return { ImageType };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editState: {} as BaseItemPerson,
|
||||
options: [
|
||||
{ text: this.$t('actor'), value: 'Actor' },
|
||||
{ text: this.$t('composer'), value: 'Composer' },
|
||||
{ text: this.$t('director'), value: 'Director' },
|
||||
{ text: this.$t('guestStar'), value: 'GuestStar' },
|
||||
{ text: this.$t('producer'), value: 'Producer' },
|
||||
{ text: this.$t('writer'), value: 'Writer' }
|
||||
]
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
person(value: BaseItemPerson): void {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const props = defineProps<{ person: BaseItemPerson | undefined }>();
|
||||
|
||||
this.editState = value;
|
||||
}
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:person', person: BaseItemPerson): void;
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const editState = ref<BaseItemPerson>();
|
||||
const options = computed(() => [
|
||||
{ text: t('actor'), value: 'Actor' },
|
||||
{ text: t('composer'), value: 'Composer' },
|
||||
{ text: t('director'), value: 'Director' },
|
||||
{ text: t('guestStar'), value: 'GuestStar' },
|
||||
{ text: t('producer'), value: 'Producer' },
|
||||
{ text: t('writer'), value: 'Writer' }
|
||||
]);
|
||||
|
||||
watch(
|
||||
() => props.person,
|
||||
(person) => {
|
||||
editState.value = { ...person };
|
||||
},
|
||||
methods: {
|
||||
handleSubmit(): void {
|
||||
this.$emit('update:person', this.editState);
|
||||
this.$emit('update:dialog', false);
|
||||
this.reset();
|
||||
},
|
||||
handleCancel(): void {
|
||||
this.$emit('update:dialog', false);
|
||||
this.reset();
|
||||
},
|
||||
reset(): void {
|
||||
this.editState = { Name: '', Type: '', Role: '' };
|
||||
}
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles saving person changes
|
||||
*/
|
||||
function onSubmit(): void {
|
||||
if (!editState.value) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
emit('update:person', editState.value);
|
||||
}
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user