mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-28 02:51:56 +08:00
feat: unify media subscription flow
This commit is contained in:
@@ -2,28 +2,26 @@
|
||||
import noImage from '@images/no-image.jpeg'
|
||||
import { getDisplayImageUrl, getLogoUrl } from '@/utils/imageUtils'
|
||||
import api from '@/api'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { formatSeason, formatRating } from '@/@core/utils/formatters'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import type { MediaInfo, Subscribe, MediaSeason, Site } from '@/api/types'
|
||||
import { formatRating, formatSeason } from '@/@core/utils/formatters'
|
||||
import type { MediaInfo, Site, Subscribe } from '@/api/types'
|
||||
import router from '@/router'
|
||||
import { useUserStore, useGlobalSettingsStore } from '@/stores'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { mediaTypeDict } from '@/api/constants'
|
||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import {
|
||||
getMediaSubscribeId,
|
||||
getSubscribeMode,
|
||||
useMediaSubscribe,
|
||||
type SeasonSubscribeModes,
|
||||
} from '@/composables/useMediaSubscribe'
|
||||
import {
|
||||
getCachedMediaExistsStatus,
|
||||
getCachedMediaSubscribeStatus,
|
||||
setCachedMediaExistsStatus,
|
||||
setCachedMediaSubscribeStatus,
|
||||
} from '@/utils/mediaStatusCache'
|
||||
|
||||
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
|
||||
const SubscribeEditDialog = defineAsyncComponent(() => import('../dialog/SubscribeEditDialog.vue'))
|
||||
const SubscribeModeDialog = defineAsyncComponent(() => import('../dialog/SubscribeModeDialog.vue'))
|
||||
const SubscribeSeasonDialog = defineAsyncComponent(() => import('../dialog/SubscribeSeasonDialog.vue'))
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -51,10 +49,6 @@ const userPermissions = computed(() => buildUserPermissionContext(userStore.supe
|
||||
const canSearch = computed(() => hasPermission(userPermissions.value, 'search'))
|
||||
const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscribe'))
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 图片加载状态
|
||||
const isImageLoaded = ref(false)
|
||||
|
||||
@@ -67,8 +61,15 @@ const isSubscribed = ref(false)
|
||||
// 本地存在状态
|
||||
const isExists = ref(false)
|
||||
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<MediaSeason[]>([])
|
||||
// 当前媒体已订阅的季号
|
||||
const subscribedSeasons = ref<number[]>([])
|
||||
|
||||
// 当前媒体已订阅季的订阅模式
|
||||
const subscribedSeasonModes = ref<SeasonSubscribeModes>({})
|
||||
|
||||
const subscribedSeasonsLoaded = ref(false)
|
||||
|
||||
const subscribedSeasonsLoading = ref(false)
|
||||
|
||||
// 来源角标字典
|
||||
const sourceIconDict: { [key: string]: any } = {
|
||||
@@ -92,33 +93,6 @@ const selectedSites = ref<number[]>([])
|
||||
// 搜索菜单显示状态
|
||||
const searchMenuShow = ref(false)
|
||||
|
||||
// 选择的剧集组
|
||||
const episodeGroup = ref('')
|
||||
|
||||
// 打开订阅季选择弹窗,避免每个媒体卡片都持有弹窗实例。
|
||||
function openSubscribeSeasonDialog() {
|
||||
openSharedDialog(
|
||||
SubscribeSeasonDialog,
|
||||
{ media: props.media },
|
||||
{
|
||||
subscribe: subscribeSeasons,
|
||||
},
|
||||
{ closeOn: ['close', 'subscribe'] },
|
||||
)
|
||||
}
|
||||
|
||||
// 打开订阅编辑弹窗,保存、关闭或删除时释放共享弹窗实例。
|
||||
function openSubscribeEditDialog(subid: number) {
|
||||
openSharedDialog(
|
||||
SubscribeEditDialog,
|
||||
{ subid },
|
||||
{
|
||||
remove: onRemoveSubscribe,
|
||||
},
|
||||
{ closeOn: ['close', 'save', 'remove'] },
|
||||
)
|
||||
}
|
||||
|
||||
// 打开站点选择弹窗,并把选择结果交回当前媒体卡片继续搜索。
|
||||
function openSearchSiteDialog() {
|
||||
openSharedDialog(
|
||||
@@ -158,10 +132,7 @@ async function querySelectedSites() {
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
if (props.media?.tmdb_id) return `tmdb:${props.media?.tmdb_id}`
|
||||
else if (props.media?.douban_id) return `douban:${props.media?.douban_id}`
|
||||
else if (props.media?.bangumi_id) return `bangumi:${props.media?.bangumi_id}`
|
||||
else return `${props.media?.mediaid_prefix}:${props.media?.media_id}`
|
||||
return getMediaSubscribeId(props.media)
|
||||
}
|
||||
|
||||
function getSubscribeStatusKey(season: number | null = props.media?.season ?? null) {
|
||||
@@ -180,6 +151,23 @@ function getExistsStatusKey() {
|
||||
].join('::')
|
||||
}
|
||||
|
||||
const subscribedSeasonText = computed(() => {
|
||||
if (props.media?.type !== '电视剧') return ''
|
||||
if (subscribedSeasonsLoading.value) return t('common.loadingText')
|
||||
if (!subscribedSeasons.value.length) return t('media.status.subscribed')
|
||||
|
||||
return subscribedSeasons.value.map(season => formatSeason(season.toString())).join('、')
|
||||
})
|
||||
|
||||
function isSameSubscribeMedia(subscribe: Subscribe) {
|
||||
if (props.media?.tmdb_id && subscribe.tmdbid) return props.media.tmdb_id === subscribe.tmdbid
|
||||
if (props.media?.douban_id && subscribe.doubanid) return props.media.douban_id === subscribe.doubanid
|
||||
if (props.media?.bangumi_id && subscribe.bangumiid) return props.media.bangumi_id === subscribe.bangumiid
|
||||
|
||||
const mediaId = props.media?.media_id ? `${props.media.mediaid_prefix}:${props.media.media_id}` : ''
|
||||
return Boolean(mediaId && subscribe.mediaid === mediaId)
|
||||
}
|
||||
|
||||
// 角标颜色
|
||||
function getChipColor(type: string) {
|
||||
if (type === '电影') return 'border-blue-500 bg-blue-600'
|
||||
@@ -187,122 +175,6 @@ function getChipColor(type: string) {
|
||||
else return 'border-purple-600 bg-purple-600'
|
||||
}
|
||||
|
||||
// 添加订阅处理
|
||||
async function handleAddSubscribe() {
|
||||
if (props.media?.type === '电视剧') {
|
||||
// 弹出季选择列表,支持多选
|
||||
seasonsSelected.value = []
|
||||
openSubscribeSeasonDialog()
|
||||
} else {
|
||||
if (isExists.value) {
|
||||
openSharedDialog(
|
||||
SubscribeModeDialog,
|
||||
{ modes: ['normal', 'best_version'], type: props.media?.type },
|
||||
{
|
||||
choose: (mode: string) =>
|
||||
addSubscribe(null, {
|
||||
best_version: mode === 'normal' ? 0 : 1,
|
||||
best_version_full: 0,
|
||||
}),
|
||||
},
|
||||
{ closeOn: ['close', 'choose'] },
|
||||
)
|
||||
} else {
|
||||
addSubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API添加订阅,电视剧的话需要指定季
|
||||
async function addSubscribe(season: number | null = null, payload: { best_version?: number; best_version_full?: number } = {}) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', {
|
||||
name: props.media?.title,
|
||||
type: props.media?.type,
|
||||
year: props.media?.year,
|
||||
tmdbid: props.media?.tmdb_id,
|
||||
doubanid: props.media?.douban_id,
|
||||
bangumiid: props.media?.bangumi_id,
|
||||
mediaid: props.media?.media_id ? `${props.media?.mediaid_prefix}:${props.media?.media_id}` : '',
|
||||
season: props.media?.type === '电影' ? null : season,
|
||||
...payload,
|
||||
episode_group: episodeGroup.value,
|
||||
})
|
||||
|
||||
// 订阅状态
|
||||
if (result.success) {
|
||||
// 订阅成功
|
||||
isSubscribed.value = true
|
||||
setCachedMediaSubscribeStatus(getSubscribeStatusKey(season), true)
|
||||
}
|
||||
|
||||
// 提示
|
||||
showSubscribeAddToast(result.success, props.media?.title ?? '', season, result.message, payload.best_version ?? 0)
|
||||
|
||||
// 弹出订阅编辑弹窗
|
||||
if (result.success && seasonsSelected.value.length <= 1) {
|
||||
const show_edit_dialog = await queryDefaultSubscribeConfig()
|
||||
if (show_edit_dialog) {
|
||||
openSubscribeEditDialog(result.data.id)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number | null, message: string, best_version: number) {
|
||||
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
let subname = t('subscribe.normalSub')
|
||||
if (best_version > 0) subname = t('subscribe.versionSub')
|
||||
|
||||
if (result) $toast.success(`${title} ${t('subscribe.addSuccess', { name: subname })}`)
|
||||
else if (!result) $toast.error(`${title} ${t('subscribe.addFailed', { name: subname, message: message })}`)
|
||||
}
|
||||
|
||||
// 调用API取消订阅
|
||||
async function removeSubscribe() {
|
||||
const confirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('dialog.subscribeEdit.cancelSubscribeConfirm'),
|
||||
})
|
||||
if (!confirmed) return
|
||||
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season: props.media?.season,
|
||||
},
|
||||
})
|
||||
let title = props.media?.title ?? ''
|
||||
if (props.media?.season !== null && props.media?.season !== undefined)
|
||||
title = `${title} ${formatSeason(props.media.season.toString())}`
|
||||
|
||||
if (result.success) {
|
||||
isSubscribed.value = false
|
||||
setCachedMediaSubscribeStatus(getSubscribeStatusKey(props.media?.season ?? null), false)
|
||||
$toast.success(`${title} ${t('subscribe.cancelSuccess')}`)
|
||||
} else {
|
||||
$toast.error(`${title} ${t('subscribe.cancelFailed', { message: result.message })}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已订阅
|
||||
async function handleCheckSubscribe() {
|
||||
try {
|
||||
@@ -315,6 +187,31 @@ async function handleCheckSubscribe() {
|
||||
}
|
||||
}
|
||||
|
||||
async function querySubscribedSeasons() {
|
||||
if (props.media?.type !== '电视剧' || !isSubscribed.value || subscribedSeasonsLoaded.value || subscribedSeasonsLoading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
subscribedSeasonsLoading.value = true
|
||||
try {
|
||||
const subscribes: Subscribe[] = await api.get('subscribe/')
|
||||
const mediaSubscribes = subscribes.filter(
|
||||
item => item.type === '电视剧' && item.season !== undefined && isSameSubscribeMedia(item),
|
||||
)
|
||||
|
||||
subscribedSeasons.value = mediaSubscribes.map(item => item.season as number).sort((a, b) => a - b)
|
||||
subscribedSeasonModes.value = mediaSubscribes.reduce<SeasonSubscribeModes>((modes, item) => {
|
||||
if (item.season !== undefined) modes[item.season] = getSubscribeMode(item)
|
||||
return modes
|
||||
}, {})
|
||||
subscribedSeasonsLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
subscribedSeasonsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查询当前媒体是否已入库
|
||||
async function handleCheckExists() {
|
||||
try {
|
||||
@@ -341,75 +238,13 @@ async function handleCheckExists() {
|
||||
|
||||
// 调用API检查是否已订阅,电视剧需要指定季
|
||||
async function checkSubscribe(season: number | null) {
|
||||
try {
|
||||
// AbortController 现在由全局请求优化器自动管理
|
||||
const mediaid = getMediaId()
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
title: props.media?.title,
|
||||
},
|
||||
})
|
||||
|
||||
return Boolean(result.id)
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status === 404) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 查询订阅弹窗规则
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
if (!canSubscribe.value) return false
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (props.media?.type === '电影') subscribe_config_url = 'system/setting/public/DefaultMovieSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
if (result.data?.value) return result.data.value.show_edit_dialog
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
return subscribeActions.checkSubscribe(season)
|
||||
}
|
||||
|
||||
// 爱心订阅按钮响应
|
||||
function handleSubscribe() {
|
||||
if (isSubscribed.value) removeSubscribe()
|
||||
else handleAddSubscribe()
|
||||
}
|
||||
|
||||
// 订阅多季
|
||||
function subscribeSeasons(seasons: MediaSeason[], seasonNoExists: { [key: number]: number }, groudId: string) {
|
||||
episodeGroup.value = groudId
|
||||
seasonsSelected.value = seasons || []
|
||||
if (seasonsSelected.value.length === 1) {
|
||||
const seasonNumber = seasonsSelected.value[0]?.season_number ?? null
|
||||
if (seasonNumber !== null && !seasonNoExists[seasonNumber]) {
|
||||
openSharedDialog(
|
||||
SubscribeModeDialog,
|
||||
{ modes: ['normal', 'best_version', 'best_version_full'], type: props.media?.type },
|
||||
{
|
||||
choose: (mode: string) =>
|
||||
addSubscribe(seasonNumber, {
|
||||
best_version: mode === 'normal' ? 0 : 1,
|
||||
best_version_full: mode === 'best_version_full' ? 1 : 0,
|
||||
}),
|
||||
},
|
||||
{ closeOn: ['close', 'choose'] },
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
seasonsSelected.value.forEach(season => {
|
||||
const seasonNumber = season.season_number ?? null
|
||||
const payload =
|
||||
seasonNumber !== null && !seasonNoExists[seasonNumber] ? { best_version: 1, best_version_full: 1 } : {}
|
||||
addSubscribe(seasonNumber, payload)
|
||||
})
|
||||
async function handleSubscribe() {
|
||||
await querySubscribedSeasons()
|
||||
subscribeActions.handleSubscribe()
|
||||
}
|
||||
|
||||
// 打开详情页
|
||||
@@ -510,17 +345,40 @@ const getImgUrl: Ref<string> = computed(() => {
|
||||
return getDisplayImageUrl(url, globalSettings.GLOBAL_IMAGE_CACHE)
|
||||
})
|
||||
|
||||
// 移除订阅
|
||||
function onRemoveSubscribe() {
|
||||
isSubscribed.value = false
|
||||
}
|
||||
|
||||
// 获取媒体类型文本
|
||||
function getMediaTypeText(type: string | undefined) {
|
||||
if (!type) return ''
|
||||
return mediaTypeDict[type]
|
||||
}
|
||||
|
||||
const subscribeActions = useMediaSubscribe({
|
||||
media: () => props.media,
|
||||
canSubscribe: () => canSubscribe.value,
|
||||
isSubscribed,
|
||||
isExists: () => isExists.value,
|
||||
subscribedSeasons,
|
||||
subscribedSeasonModes,
|
||||
primarySeason: () => props.media?.season ?? null,
|
||||
getSubscribeStatusKey,
|
||||
})
|
||||
|
||||
watch(isSubscribed, subscribed => {
|
||||
subscribedSeasonsLoaded.value = false
|
||||
if (!subscribed) {
|
||||
subscribedSeasons.value = []
|
||||
subscribedSeasonModes.value = {}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.media,
|
||||
() => {
|
||||
subscribedSeasons.value = []
|
||||
subscribedSeasonModes.value = {}
|
||||
subscribedSeasonsLoaded.value = false
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
setupIntersectionObserver()
|
||||
})
|
||||
@@ -535,7 +393,12 @@ onBeforeUnmount(() => {
|
||||
<VHover>
|
||||
<template #default="hover">
|
||||
<!-- Hover 命中区域保持静止,避免卡片上浮后底边反复触发 mouseleave。 -->
|
||||
<div ref="mediaCardRef" v-bind="hover.props" class="media-card-hover-area">
|
||||
<div
|
||||
ref="mediaCardRef"
|
||||
v-bind="hover.props"
|
||||
class="media-card-hover-area"
|
||||
@mouseenter="querySubscribedSeasons"
|
||||
>
|
||||
<VCard
|
||||
:height="props.height"
|
||||
:width="props.width"
|
||||
@@ -574,6 +437,12 @@ onBeforeUnmount(() => {
|
||||
<p class="media-card-overview line-clamp-3 overflow-hidden text-ellipsis ...">
|
||||
{{ props.media?.overview }}
|
||||
</p>
|
||||
<div v-if="isSubscribed" class="media-card-subscribe-summary mb-2">
|
||||
<VIcon icon="mdi-heart" color="error" size="small" />
|
||||
<span>
|
||||
{{ props.media?.type === '电视剧' ? subscribedSeasonText : t('media.status.subscribed') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="props.media?.collection_id" class="mb-3" @click.stop=""></div>
|
||||
<div v-else class="flex align-center justify-between">
|
||||
<IconBtn
|
||||
@@ -586,7 +455,7 @@ onBeforeUnmount(() => {
|
||||
<VSpacer />
|
||||
<IconBtn
|
||||
v-if="canSubscribe"
|
||||
icon="mdi-heart"
|
||||
:icon="isSubscribed ? 'mdi-heart' : 'mdi-heart-outline'"
|
||||
:color="isSubscribed ? 'error' : 'white'"
|
||||
size="small"
|
||||
@click.stop="handleSubscribe"
|
||||
@@ -645,4 +514,20 @@ onBeforeUnmount(() => {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.media-card-subscribe-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
min-block-size: 1.25rem;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.media-card-subscribe-summary span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,20 @@ import { PropType } from 'vue'
|
||||
import NoDataFound from '@/components/states/NoDataFound.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import type { SeasonSubscribeModes, SubscribeMode } from '@/composables/useMediaSubscribe'
|
||||
|
||||
type SubscribeModeOption = {
|
||||
icon: string
|
||||
title: string
|
||||
value: SubscribeMode
|
||||
}
|
||||
|
||||
type EpisodeGroupOption = {
|
||||
title: string
|
||||
subtitle: string
|
||||
value: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
@@ -15,6 +29,9 @@ const emit = defineEmits(['subscribe', 'close'])
|
||||
// 定义输入
|
||||
const props = defineProps({
|
||||
media: Object as PropType<MediaInfo>,
|
||||
selectedSeason: Number,
|
||||
subscribedSeasons: Array as PropType<number[]>,
|
||||
subscribedSeasonModes: Object as PropType<SeasonSubscribeModes>,
|
||||
})
|
||||
|
||||
// 从 provide 中获取全局设置
|
||||
@@ -25,8 +42,11 @@ const globalSettings = globalSettingsStore.globalSettings
|
||||
// 季详情
|
||||
const seasonInfos = ref<MediaSeason[]>([])
|
||||
|
||||
// 选中的订阅季
|
||||
const seasonsSelected = ref<MediaSeason[]>([])
|
||||
// 选中的订阅季号
|
||||
const seasonsSelected = ref<number[]>([])
|
||||
|
||||
// 各季订阅方式
|
||||
const seasonModes = ref<Record<number, SubscribeMode>>({})
|
||||
|
||||
// 各季缺失状态:0-已入库 1-部分缺失 2-全部缺失,没有数据也是已入库
|
||||
const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
@@ -40,17 +60,70 @@ const episodeGroups = ref<{ [key: string]: any }[]>([])
|
||||
// 当前选择剧集组
|
||||
const episodeGroup = ref('')
|
||||
|
||||
// 剧集组选项属性
|
||||
function episodeGroupItemProps(item: { title: string; subtitle: string }) {
|
||||
return {
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
}
|
||||
const subscribeModeOptions = computed<SubscribeModeOption[]>(() => [
|
||||
{
|
||||
title: t('dialog.subscribeMode.normal'),
|
||||
value: 'normal',
|
||||
icon: 'mdi-plus-circle-outline',
|
||||
},
|
||||
{
|
||||
title: t('dialog.subscribeMode.bestVersionEpisode'),
|
||||
value: 'best_version',
|
||||
icon: 'mdi-refresh',
|
||||
},
|
||||
{
|
||||
title: t('dialog.subscribeMode.bestVersionFull'),
|
||||
value: 'best_version_full',
|
||||
icon: 'mdi-shimmer',
|
||||
},
|
||||
])
|
||||
|
||||
function getSubscribeModeColor(mode: SubscribeMode) {
|
||||
if (mode === 'normal') return 'primary'
|
||||
if (mode === 'best_version') return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
function isSubscribeMode(value: unknown): value is SubscribeMode {
|
||||
return value === 'normal' || value === 'best_version' || value === 'best_version_full'
|
||||
}
|
||||
|
||||
const subscribedSeasonSet = computed(() => new Set(props.subscribedSeasons ?? []))
|
||||
|
||||
const selectedSeasonSet = computed(() => new Set(seasonsSelected.value))
|
||||
|
||||
const visibleSeasonNumbers = computed(() =>
|
||||
seasonInfos.value
|
||||
.map(item => item.season_number)
|
||||
.filter((season): season is number => season !== null && season !== undefined),
|
||||
)
|
||||
|
||||
const hasSelectionChanges = computed(() => {
|
||||
const visibleSeasons = new Set(visibleSeasonNumbers.value)
|
||||
|
||||
for (const season of visibleSeasons) {
|
||||
if (subscribedSeasonSet.value.has(season) !== selectedSeasonSet.value.has(season)) return true
|
||||
if (
|
||||
subscribedSeasonSet.value.has(season) &&
|
||||
selectedSeasonSet.value.has(season) &&
|
||||
(props.subscribedSeasonModes?.[season] ?? 'normal') !== (seasonModes.value[season] ?? 'normal')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const submitButtonText = computed(() => {
|
||||
if (!hasSelectionChanges.value && seasonsSelected.value.length === 0) return t('dialog.subscribeSeason.selectSeasons')
|
||||
|
||||
return t('dialog.subscribeSeason.submit')
|
||||
})
|
||||
|
||||
// 剧集组选项
|
||||
const episodeGroupOptions = computed(() => {
|
||||
let options = (episodeGroups.value as { id: string; name: string; group_count: number; episode_count: number }[]).map(
|
||||
const episodeGroupOptions = computed<EpisodeGroupOption[]>(() => {
|
||||
const options = (episodeGroups.value as { id: string; name: string; group_count: number; episode_count: number }[]).map(
|
||||
item => {
|
||||
return {
|
||||
title: item.name,
|
||||
@@ -59,6 +132,7 @@ const episodeGroupOptions = computed(() => {
|
||||
{ count: item.episode_count },
|
||||
)}`,
|
||||
value: item.id,
|
||||
icon: 'mdi-folder-play-outline',
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -67,6 +141,7 @@ const episodeGroupOptions = computed(() => {
|
||||
title: t('dialog.subscribeSeason.defaultGroup'),
|
||||
subtitle: t('dialog.subscribeSeason.seasonCount', { count: seasonInfos.value.length }),
|
||||
value: '',
|
||||
icon: 'mdi-layers-outline',
|
||||
})
|
||||
return options
|
||||
})
|
||||
@@ -125,9 +200,10 @@ async function getGroupSeasons() {
|
||||
async function checkSeasonsNotExists() {
|
||||
// 开始处理
|
||||
try {
|
||||
let tmpMedia = props.media ?? { episode_group: '' }
|
||||
if (episodeGroup.value) tmpMedia.episode_group = episodeGroup.value
|
||||
else tmpMedia.episode_group = ''
|
||||
const tmpMedia = {
|
||||
...(props.media ?? {}),
|
||||
episode_group: episodeGroup.value || '',
|
||||
}
|
||||
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', tmpMedia)
|
||||
if (result) {
|
||||
result.forEach(item => {
|
||||
@@ -183,8 +259,89 @@ function getYear(airDate: string) {
|
||||
return date.getFullYear()
|
||||
}
|
||||
|
||||
function setEpisodeGroup(value: string) {
|
||||
if (episodeGroup.value === value) return
|
||||
|
||||
seasonsNotExisted.value = {}
|
||||
seasonInfos.value = []
|
||||
episodeGroup.value = value
|
||||
}
|
||||
|
||||
function subscribeSeasons() {
|
||||
emit('subscribe', seasonsSelected.value, seasonsNotExisted.value, episodeGroup.value)
|
||||
const selectedSeasons = seasonInfos.value.filter(item => {
|
||||
const seasonNumber = item.season_number ?? null
|
||||
return seasonNumber !== null && selectedSeasonSet.value.has(seasonNumber)
|
||||
})
|
||||
|
||||
emit(
|
||||
'subscribe',
|
||||
selectedSeasons,
|
||||
seasonsNotExisted.value,
|
||||
episodeGroup.value,
|
||||
{ ...seasonModes.value },
|
||||
visibleSeasonNumbers.value,
|
||||
)
|
||||
}
|
||||
|
||||
function setSeasonMode(season: number, mode: SubscribeMode) {
|
||||
seasonModes.value = {
|
||||
...seasonModes.value,
|
||||
[season]: mode,
|
||||
}
|
||||
}
|
||||
|
||||
function updateSeasonMode(season: number, mode: unknown) {
|
||||
if (!isSubscribeMode(mode)) return
|
||||
setSeasonMode(season, mode)
|
||||
}
|
||||
|
||||
function ensureSeasonMode(season: number) {
|
||||
if (!seasonModes.value[season]) setSeasonMode(season, props.subscribedSeasonModes?.[season] ?? 'normal')
|
||||
}
|
||||
|
||||
function isSeasonSubscribed(season: number) {
|
||||
return subscribedSeasonSet.value.has(season)
|
||||
}
|
||||
|
||||
function isSeasonSelected(season: number) {
|
||||
return selectedSeasonSet.value.has(season)
|
||||
}
|
||||
|
||||
function setSeasonSelected(season: number, selected: boolean | null) {
|
||||
const nextSeasons = new Set(seasonsSelected.value)
|
||||
if (selected) {
|
||||
nextSeasons.add(season)
|
||||
ensureSeasonMode(season)
|
||||
} else {
|
||||
nextSeasons.delete(season)
|
||||
}
|
||||
seasonsSelected.value = [...nextSeasons].sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
function toggleSeasonSelected(season: number) {
|
||||
setSeasonSelected(season, !isSeasonSelected(season))
|
||||
}
|
||||
|
||||
function syncSelectedSeason() {
|
||||
if (!seasonInfos.value.length) return
|
||||
|
||||
const seasonNumbers = new Set<number>()
|
||||
props.subscribedSeasons?.forEach(season => seasonNumbers.add(season))
|
||||
if (props.selectedSeason !== undefined) seasonNumbers.add(props.selectedSeason)
|
||||
|
||||
const validSeasonNumbers = new Set(
|
||||
seasonInfos.value
|
||||
.map(item => item.season_number)
|
||||
.filter((season): season is number => season !== null && season !== undefined),
|
||||
)
|
||||
|
||||
seasonsSelected.value = [...seasonNumbers].filter(season => validSeasonNumbers.has(season)).sort((a, b) => a - b)
|
||||
seasonsSelected.value.forEach(ensureSeasonMode)
|
||||
Object.entries(props.subscribedSeasonModes ?? {}).forEach(([season, mode]) => {
|
||||
const seasonNumber = Number(season)
|
||||
if (!validSeasonNumbers.has(seasonNumber)) return
|
||||
setSeasonMode(seasonNumber, mode)
|
||||
})
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
@@ -193,6 +350,23 @@ watchEffect(() => {
|
||||
checkSeasonsNotExists()
|
||||
})
|
||||
|
||||
watch(seasonInfos, syncSelectedSeason)
|
||||
|
||||
watch(
|
||||
() => props.selectedSeason,
|
||||
syncSelectedSeason,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.subscribedSeasons,
|
||||
syncSelectedSeason,
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.subscribedSeasonModes,
|
||||
syncSelectedSeason,
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
getMediaSeasons()
|
||||
getEpisodeGroups()
|
||||
@@ -201,25 +375,46 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VBottomSheet inset scrollable>
|
||||
<VCard>
|
||||
<VBottomSheet inset scrollable content-class="subscribe-season-sheet">
|
||||
<VCard class="subscribe-season-dialog">
|
||||
<VDialogCloseBtn @click="emit('close')" />
|
||||
<VCardItem>
|
||||
<VCardTitle class="pe-10"> {{ t('dialog.subscribeSeason.title', { title: props.media?.title }) }} </VCardTitle>
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
<VCardText>
|
||||
<VSelect
|
||||
v-model="episodeGroup"
|
||||
:items="episodeGroupOptions"
|
||||
:item-props="episodeGroupItemProps"
|
||||
:label="t('dialog.subscribeSeason.selectGroup')"
|
||||
persistent-hint
|
||||
/>
|
||||
<div class="subscribe-season-group-selector">
|
||||
<div class="subscribe-season-group-label">
|
||||
{{ t('dialog.subscribeSeason.selectGroup') }}
|
||||
</div>
|
||||
<div class="subscribe-season-group-options">
|
||||
<button
|
||||
v-for="group in episodeGroupOptions"
|
||||
:key="group.value || 'default'"
|
||||
type="button"
|
||||
class="subscribe-season-group-option"
|
||||
:class="{ 'subscribe-season-group-option--active': episodeGroup === group.value }"
|
||||
@click="setEpisodeGroup(group.value)"
|
||||
>
|
||||
<VIcon :icon="group.icon" size="small" />
|
||||
<span class="subscribe-season-group-text">
|
||||
<span class="subscribe-season-group-title">{{ group.title }}</span>
|
||||
<span class="subscribe-season-group-subtitle">{{ group.subtitle }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<LoadingBanner v-if="!isRefreshed" class="mt-5" />
|
||||
<div v-else-if="seasonInfos.length > 0">
|
||||
<VList v-model:selected="seasonsSelected" lines="three" select-strategy="classic">
|
||||
<VListItem v-for="(item, i) in seasonInfos" :key="i" :value="item">
|
||||
<VList lines="three" class="subscribe-season-list">
|
||||
<VListItem
|
||||
v-for="(item, i) in seasonInfos"
|
||||
:key="i"
|
||||
:active="isSeasonSelected(item.season_number || 0)"
|
||||
rounded="lg"
|
||||
class="subscribe-season-list-item"
|
||||
@click="toggleSeasonSelected(item.season_number || 0)"
|
||||
>
|
||||
<template #prepend>
|
||||
<VImg
|
||||
height="90"
|
||||
@@ -258,10 +453,46 @@ onMounted(async () => {
|
||||
>
|
||||
{{ getExistText(item.season_number || 0) }}
|
||||
</VChip>
|
||||
<VChip
|
||||
v-if="isSeasonSubscribed(item.season_number || 0)"
|
||||
class="mt-2 ms-2"
|
||||
size="small"
|
||||
color="error"
|
||||
>
|
||||
{{ t('media.status.subscribed') }}
|
||||
</VChip>
|
||||
</VListItemSubtitle>
|
||||
<template #append="{ isSelected }">
|
||||
<VListItemAction start>
|
||||
<VSwitch :model-value="isSelected" />
|
||||
<template #append>
|
||||
<VListItemAction start class="subscribe-season-actions">
|
||||
<VSwitch
|
||||
:model-value="isSeasonSelected(item.season_number || 0)"
|
||||
hide-details
|
||||
@click.stop
|
||||
@update:model-value="setSeasonSelected(item.season_number || 0, $event)"
|
||||
/>
|
||||
<VBtnToggle
|
||||
v-if="isSeasonSelected(item.season_number || 0)"
|
||||
:model-value="seasonModes[item.season_number || 0] || 'normal'"
|
||||
density="compact"
|
||||
divided
|
||||
mandatory
|
||||
variant="outlined"
|
||||
class="subscribe-season-mode-toggle"
|
||||
@click.stop
|
||||
@update:model-value="updateSeasonMode(item.season_number || 0, $event)"
|
||||
>
|
||||
<VBtn
|
||||
v-for="mode in subscribeModeOptions"
|
||||
:key="mode.value"
|
||||
:value="mode.value"
|
||||
:color="getSubscribeModeColor(mode.value)"
|
||||
size="small"
|
||||
class="subscribe-season-mode-button"
|
||||
>
|
||||
<VIcon :icon="mode.icon" size="small" />
|
||||
<span>{{ mode.title }}</span>
|
||||
</VBtn>
|
||||
</VBtnToggle>
|
||||
</VListItemAction>
|
||||
</template>
|
||||
</VListItem>
|
||||
@@ -269,15 +500,186 @@ onMounted(async () => {
|
||||
</div>
|
||||
<NoDataFound v-else errorTitle="出错啦!" :errorDescription="`${props.media?.title} 未查询到季集信息`" />
|
||||
</VCardText>
|
||||
<div class="my-2 text-center">
|
||||
<VBtn :disabled="seasonsSelected.length === 0" width="30%" @click="subscribeSeasons">
|
||||
{{
|
||||
seasonsSelected.length === 0
|
||||
? t('dialog.subscribeSeason.selectSeasons')
|
||||
: t('dialog.subscribeSeason.submit')
|
||||
}}
|
||||
<VCardActions class="justify-center py-3">
|
||||
<VBtn
|
||||
:disabled="!hasSelectionChanges"
|
||||
width="30%"
|
||||
min-width="8rem"
|
||||
color="primary"
|
||||
variant="elevated"
|
||||
class="subscribe-season-submit"
|
||||
@click="subscribeSeasons"
|
||||
>
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-check" />
|
||||
</template>
|
||||
{{ submitButtonText }}
|
||||
</VBtn>
|
||||
</div>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VBottomSheet>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.subscribe-season-dialog {
|
||||
inline-size: min(46rem, calc(100vw - 2rem));
|
||||
max-block-size: min(42rem, calc(100vh - 4rem));
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.subscribe-season-actions {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
justify-items: end;
|
||||
align-content: end;
|
||||
align-self: stretch;
|
||||
block-size: 100%;
|
||||
min-inline-size: 18rem;
|
||||
}
|
||||
|
||||
.subscribe-season-group-selector {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
||||
.subscribe-season-group-label {
|
||||
color: rgba(var(--v-theme-on-surface), 0.72);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.subscribe-season-group-options {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
padding-block: 0.125rem 0.375rem;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.subscribe-season-group-option {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 0 0 auto;
|
||||
min-inline-size: 11rem;
|
||||
max-inline-size: 16rem;
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
border-radius: 8px;
|
||||
background: rgba(var(--v-theme-surface), 0.72);
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: start;
|
||||
transition:
|
||||
border-color 0.16s ease,
|
||||
background-color 0.16s ease,
|
||||
color 0.16s ease;
|
||||
}
|
||||
|
||||
.subscribe-season-group-option:hover {
|
||||
border-color: rgba(var(--v-theme-primary), 0.45);
|
||||
background: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
.subscribe-season-group-option--active {
|
||||
border-color: rgb(var(--v-theme-primary));
|
||||
background: rgba(var(--v-theme-primary), 0.14);
|
||||
color: rgb(var(--v-theme-primary));
|
||||
}
|
||||
|
||||
.subscribe-season-group-text {
|
||||
display: grid;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.subscribe-season-group-title,
|
||||
.subscribe-season-group-subtitle {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subscribe-season-group-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.125rem;
|
||||
}
|
||||
|
||||
.subscribe-season-group-subtitle {
|
||||
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.subscribe-season-group-option--active .subscribe-season-group-subtitle {
|
||||
color: rgba(var(--v-theme-primary), 0.82);
|
||||
}
|
||||
|
||||
.subscribe-season-list {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.subscribe-season-list-item {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.subscribe-season-list-item :deep(.v-list-item__append) {
|
||||
align-self: stretch;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.subscribe-season-mode-toggle {
|
||||
block-size: 2rem;
|
||||
max-inline-size: 16rem;
|
||||
}
|
||||
|
||||
.subscribe-season-mode-button {
|
||||
min-inline-size: 0;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.subscribe-season-mode-button span {
|
||||
margin-inline-start: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.subscribe-season-actions {
|
||||
gap: 0.375rem;
|
||||
min-inline-size: 6.75rem;
|
||||
}
|
||||
|
||||
.subscribe-season-mode-toggle {
|
||||
inline-size: 6.75rem;
|
||||
max-inline-size: 6.75rem;
|
||||
}
|
||||
|
||||
.subscribe-season-mode-button {
|
||||
flex: 1 1 0;
|
||||
padding-inline: 0.25rem;
|
||||
}
|
||||
|
||||
.subscribe-season-mode-button span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.subscribe-season-submit {
|
||||
inline-size: 100% !important;
|
||||
}
|
||||
|
||||
.subscribe-season-list-item :deep(.v-list-item__content) {
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.subscribe-season-group-option {
|
||||
min-inline-size: 9.5rem;
|
||||
max-inline-size: 12rem;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
462
src/composables/useMediaSubscribe.ts
Normal file
462
src/composables/useMediaSubscribe.ts
Normal file
@@ -0,0 +1,462 @@
|
||||
import { defineAsyncComponent, ref, type Ref } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import api from '@/api'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import type { MediaInfo, MediaSeason, Subscribe } from '@/api/types'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { setCachedMediaSubscribeStatus } from '@/utils/mediaStatusCache'
|
||||
|
||||
export type SubscribeMode = 'normal' | 'best_version' | 'best_version_full'
|
||||
|
||||
interface SubscribePayload {
|
||||
best_version?: number
|
||||
best_version_full?: number
|
||||
}
|
||||
|
||||
interface AddSubscribeOptions {
|
||||
openEditDialog?: boolean
|
||||
}
|
||||
|
||||
interface RemoveSubscribeOptions {
|
||||
confirm?: boolean
|
||||
}
|
||||
|
||||
interface UseMediaSubscribeOptions {
|
||||
media: () => MediaInfo | undefined
|
||||
canSubscribe: () => boolean
|
||||
isSubscribed?: Ref<boolean>
|
||||
isExists?: () => boolean
|
||||
seasonsSubscribed?: Ref<{ [key: number]: boolean }>
|
||||
subscribedSeasons?: Ref<number[]>
|
||||
subscribedSeasonModes?: Ref<SeasonSubscribeModes>
|
||||
primarySeason?: () => number | null
|
||||
getSubscribeStatusKey?: (season: number | null) => string
|
||||
onEditRemove?: () => void
|
||||
}
|
||||
|
||||
const SubscribeEditDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeEditDialog.vue'))
|
||||
const SubscribeModeDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeModeDialog.vue'))
|
||||
const SubscribeSeasonDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeSeasonDialog.vue'))
|
||||
|
||||
export type SeasonSubscribeModes = Record<number, SubscribeMode>
|
||||
|
||||
export function getMediaSubscribeId(media?: MediaInfo) {
|
||||
if (media?.tmdb_id) return `tmdb:${media.tmdb_id}`
|
||||
if (media?.douban_id) return `douban:${media.douban_id}`
|
||||
if (media?.bangumi_id) return `bangumi:${media.bangumi_id}`
|
||||
return `${media?.mediaid_prefix}:${media?.media_id}`
|
||||
}
|
||||
|
||||
function getSubscribePayload(mode: SubscribeMode): SubscribePayload {
|
||||
return {
|
||||
best_version: mode === 'normal' ? 0 : 1,
|
||||
best_version_full: mode === 'best_version_full' ? 1 : 0,
|
||||
}
|
||||
}
|
||||
|
||||
function isEnabledFlag(value: unknown) {
|
||||
return value === true || value === 1 || value === '1'
|
||||
}
|
||||
|
||||
export function getSubscribeMode(subscribe: { best_version?: unknown; best_version_full?: unknown }): SubscribeMode {
|
||||
if (!isEnabledFlag(subscribe.best_version)) return 'normal'
|
||||
|
||||
return isEnabledFlag(subscribe.best_version_full) ? 'best_version_full' : 'best_version'
|
||||
}
|
||||
|
||||
function getModeName(t: ReturnType<typeof useI18n>['t'], mode: SubscribeMode) {
|
||||
if (mode === 'normal') return t('dialog.subscribeMode.normal')
|
||||
if (mode === 'best_version') return t('dialog.subscribeMode.bestVersionEpisode')
|
||||
return t('dialog.subscribeMode.bestVersionFull')
|
||||
}
|
||||
|
||||
export function useMediaSubscribe(options: UseMediaSubscribeOptions) {
|
||||
const { t } = useI18n()
|
||||
const $toast = useToast()
|
||||
const createConfirm = useConfirm()
|
||||
const episodeGroup = ref('')
|
||||
|
||||
function currentMedia() {
|
||||
return options.media()
|
||||
}
|
||||
|
||||
function getMediaId() {
|
||||
return getMediaSubscribeId(currentMedia())
|
||||
}
|
||||
|
||||
function getPrimarySeason() {
|
||||
return options.primarySeason?.() ?? currentMedia()?.season ?? null
|
||||
}
|
||||
|
||||
function updateSubscribeStatus(season: number | null, subscribed: boolean, mode: SubscribeMode = 'normal') {
|
||||
const media = currentMedia()
|
||||
|
||||
if (media?.type === '电影' || season === null) {
|
||||
if (options.isSubscribed) options.isSubscribed.value = subscribed
|
||||
} else {
|
||||
if (options.seasonsSubscribed) options.seasonsSubscribed.value[season] = subscribed
|
||||
else if (options.subscribedSeasons) {
|
||||
const nextSeasons = new Set(options.subscribedSeasons.value)
|
||||
if (subscribed) nextSeasons.add(season)
|
||||
else nextSeasons.delete(season)
|
||||
options.subscribedSeasons.value = [...nextSeasons].sort((a, b) => a - b)
|
||||
}
|
||||
|
||||
if (options.isSubscribed) {
|
||||
if (subscribed) {
|
||||
options.isSubscribed.value = true
|
||||
} else if (options.seasonsSubscribed) {
|
||||
options.isSubscribed.value = Object.values(options.seasonsSubscribed.value).some(Boolean)
|
||||
} else if (options.subscribedSeasons) {
|
||||
options.isSubscribed.value = options.subscribedSeasons.value.length > 0
|
||||
} else {
|
||||
options.isSubscribed.value = false
|
||||
}
|
||||
}
|
||||
|
||||
if (options.subscribedSeasonModes) {
|
||||
const nextModes = { ...options.subscribedSeasonModes.value }
|
||||
if (subscribed) nextModes[season] = mode
|
||||
else delete nextModes[season]
|
||||
options.subscribedSeasonModes.value = nextModes
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getSubscribeStatusKey) {
|
||||
setCachedMediaSubscribeStatus(options.getSubscribeStatusKey(season), subscribed)
|
||||
}
|
||||
}
|
||||
|
||||
function openSubscribeEditDialog(subid: number) {
|
||||
openSharedDialog(
|
||||
SubscribeEditDialog,
|
||||
{ subid },
|
||||
{
|
||||
remove: () => {
|
||||
if (options.onEditRemove) {
|
||||
options.onEditRemove()
|
||||
} else if (options.isSubscribed) {
|
||||
options.isSubscribed.value = false
|
||||
}
|
||||
},
|
||||
},
|
||||
{ closeOn: ['close', 'save', 'remove'] },
|
||||
)
|
||||
}
|
||||
|
||||
function openSubscribeModeDialog(
|
||||
modes: SubscribeMode[],
|
||||
choose: (payload: SubscribePayload, mode: SubscribeMode) => void,
|
||||
) {
|
||||
openSharedDialog(
|
||||
SubscribeModeDialog,
|
||||
{ modes, type: currentMedia()?.type },
|
||||
{
|
||||
choose: (mode: SubscribeMode) => choose(getSubscribePayload(mode), mode),
|
||||
},
|
||||
{ closeOn: ['close', 'choose'] },
|
||||
)
|
||||
}
|
||||
|
||||
function openSubscribeSeasonDialog(selectedSeason?: number | null) {
|
||||
const media = currentMedia()
|
||||
if (!media) return
|
||||
|
||||
openSharedDialog(
|
||||
SubscribeSeasonDialog,
|
||||
{
|
||||
media,
|
||||
selectedSeason,
|
||||
subscribedSeasons: options.subscribedSeasons?.value ?? [],
|
||||
subscribedSeasonModes: options.subscribedSeasonModes?.value ?? {},
|
||||
},
|
||||
{
|
||||
subscribe: subscribeSeasons,
|
||||
},
|
||||
{ closeOn: ['close', 'subscribe'] },
|
||||
)
|
||||
}
|
||||
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
if (!options.canSubscribe()) return false
|
||||
|
||||
try {
|
||||
const media = currentMedia()
|
||||
const subscribeConfigUrl =
|
||||
media?.type === '电影'
|
||||
? 'system/setting/public/DefaultMovieSubscribeConfig'
|
||||
: 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
const result: { [key: string]: any } = await api.get(subscribeConfigUrl)
|
||||
|
||||
if (result.data?.value) return result.data.value.show_edit_dialog
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function showSubscribeAddToast(
|
||||
result: boolean,
|
||||
title: string,
|
||||
season: number | null,
|
||||
message: string,
|
||||
bestVersion: number,
|
||||
) {
|
||||
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
const subname = bestVersion > 0 ? t('subscribe.versionSub') : t('subscribe.normalSub')
|
||||
|
||||
if (result) $toast.success(`${title} ${t('subscribe.addSuccess', { name: subname })}`)
|
||||
else $toast.error(`${title} ${t('subscribe.addFailed', { name: subname, message })}`)
|
||||
}
|
||||
|
||||
async function addSubscribe(
|
||||
season: number | null = null,
|
||||
payload: SubscribePayload = {},
|
||||
addOptions: AddSubscribeOptions = {},
|
||||
) {
|
||||
const media = currentMedia()
|
||||
if (!media) return
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', {
|
||||
name: media.title,
|
||||
type: media.type,
|
||||
year: media.year,
|
||||
tmdbid: media.tmdb_id,
|
||||
doubanid: media.douban_id,
|
||||
bangumiid: media.bangumi_id,
|
||||
mediaid: media.media_id ? `${media.mediaid_prefix}:${media.media_id}` : '',
|
||||
season: media.type === '电影' ? null : season,
|
||||
...payload,
|
||||
episode_group: episodeGroup.value,
|
||||
})
|
||||
|
||||
if (result.success) updateSubscribeStatus(media.type === '电影' ? null : season, true, getSubscribeMode(payload))
|
||||
|
||||
showSubscribeAddToast(result.success, media.title ?? '', season, result.message, payload.best_version ?? 0)
|
||||
|
||||
if (result.success && (addOptions.openEditDialog ?? true)) {
|
||||
const showEditDialog = await queryDefaultSubscribeConfig()
|
||||
if (showEditDialog) openSubscribeEditDialog(result.data.id)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSubscribe(season: number | null = null, removeOptions: RemoveSubscribeOptions = {}) {
|
||||
if (removeOptions.confirm ?? true) {
|
||||
const confirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('dialog.subscribeEdit.cancelSubscribeConfirm'),
|
||||
})
|
||||
if (!confirmed) return
|
||||
}
|
||||
|
||||
const media = currentMedia()
|
||||
if (!media) return
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/media/${getMediaId()}`, {
|
||||
params: {
|
||||
season: media.type === '电影' ? null : season,
|
||||
},
|
||||
})
|
||||
let title = media.title ?? ''
|
||||
if (media.type !== '电影' && season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
if (result.success) {
|
||||
updateSubscribeStatus(media.type === '电影' ? null : season, false)
|
||||
$toast.success(`${title} ${t('subscribe.cancelSuccess')}`)
|
||||
} else {
|
||||
$toast.error(`${title} ${t('subscribe.cancelFailed', { message: result.message })}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSubscribe(season: number | null = null) {
|
||||
try {
|
||||
const result: Subscribe = await api.get(`subscribe/media/${getMediaId()}`, {
|
||||
params: {
|
||||
season,
|
||||
title: currentMedia()?.title,
|
||||
},
|
||||
})
|
||||
|
||||
return Boolean(result.id)
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status === 404) return false
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function querySubscribe(season: number | null = null) {
|
||||
try {
|
||||
const result: Subscribe = await api.get(`subscribe/media/${getMediaId()}`, {
|
||||
params: {
|
||||
season,
|
||||
title: currentMedia()?.title,
|
||||
},
|
||||
})
|
||||
|
||||
return result.id ? result : null
|
||||
} catch (error: any) {
|
||||
if (error?.response?.status === 404) return null
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSubscribeMode(season: number, mode: SubscribeMode) {
|
||||
const media = currentMedia()
|
||||
if (!media) return
|
||||
|
||||
startNProgress()
|
||||
try {
|
||||
const subscribe = await querySubscribe(season)
|
||||
if (!subscribe?.id) {
|
||||
$toast.error(`${media.title ?? ''} ${formatSeason(season.toString())} ${t('subscribe.notFound')}`)
|
||||
return
|
||||
}
|
||||
|
||||
const payload = getSubscribePayload(mode)
|
||||
const result: { [key: string]: any } = await api.put('subscribe/', {
|
||||
...subscribe,
|
||||
...payload,
|
||||
})
|
||||
const title = `${media.title ?? ''} ${formatSeason(season.toString())}`
|
||||
|
||||
if (result.success) {
|
||||
updateSubscribeStatus(season, true, mode)
|
||||
$toast.success(`${title} ${t('subscribe.modeUpdateSuccess', { mode: getModeName(t, mode) })}`)
|
||||
} else {
|
||||
$toast.error(`${title} ${t('subscribe.addFailed', { name: getModeName(t, mode), message: result.message })}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
doneNProgress()
|
||||
}
|
||||
}
|
||||
|
||||
function handleSeasonSubscribe(season: number) {
|
||||
if (options.seasonsSubscribed?.value[season]) {
|
||||
removeSubscribe(season)
|
||||
return
|
||||
}
|
||||
|
||||
openSubscribeSeasonDialog(season)
|
||||
}
|
||||
|
||||
function handlePrimarySubscribe() {
|
||||
const media = currentMedia()
|
||||
if (!media) return
|
||||
|
||||
const season = media.type === '电影' ? null : getPrimarySeason()
|
||||
|
||||
if (media.type === '电视剧') {
|
||||
openSubscribeSeasonDialog()
|
||||
return
|
||||
}
|
||||
|
||||
if (options.isSubscribed?.value) {
|
||||
removeSubscribe(season)
|
||||
return
|
||||
}
|
||||
|
||||
if (options.isExists?.()) {
|
||||
openSubscribeModeDialog(['normal', 'best_version'], payload => addSubscribe(null, payload))
|
||||
return
|
||||
}
|
||||
|
||||
addSubscribe(null)
|
||||
}
|
||||
|
||||
function handleSubscribe(season?: number | null) {
|
||||
if (season !== undefined && season !== null) {
|
||||
handleSeasonSubscribe(season)
|
||||
return
|
||||
}
|
||||
|
||||
handlePrimarySubscribe()
|
||||
}
|
||||
|
||||
function subscribeSeasons(
|
||||
seasons: MediaSeason[] = [],
|
||||
seasonExistsStates: { [key: number]: number } = {},
|
||||
groupId = '',
|
||||
seasonModes: SubscribeMode | SeasonSubscribeModes = 'normal',
|
||||
visibleSeasonNumbers: number[] = [],
|
||||
) {
|
||||
episodeGroup.value = groupId
|
||||
const subscribedSeasonSet = new Set(options.subscribedSeasons?.value ?? [])
|
||||
const selectedSeasonSet = new Set(
|
||||
seasons.map(season => season.season_number).filter((season): season is number => season !== null && season !== undefined),
|
||||
)
|
||||
const visibleSeasonSet = new Set(visibleSeasonNumbers)
|
||||
const seasonsToSubscribe = seasons.filter(season => {
|
||||
const seasonNumber = season.season_number ?? null
|
||||
return seasonNumber !== null && !subscribedSeasonSet.has(seasonNumber)
|
||||
})
|
||||
const seasonsToUnsubscribe = [...subscribedSeasonSet].filter(
|
||||
season => visibleSeasonSet.has(season) && !selectedSeasonSet.has(season),
|
||||
)
|
||||
const seasonsToUpdateMode = seasons.filter(season => {
|
||||
const seasonNumber = season.season_number ?? null
|
||||
if (seasonNumber === null || !subscribedSeasonSet.has(seasonNumber)) return false
|
||||
|
||||
const nextMode = typeof seasonModes === 'string' ? seasonModes : seasonModes[seasonNumber] ?? 'normal'
|
||||
return (options.subscribedSeasonModes?.value[seasonNumber] ?? 'normal') !== nextMode
|
||||
})
|
||||
|
||||
seasonsToUnsubscribe.forEach(season => {
|
||||
removeSubscribe(season, { confirm: false })
|
||||
})
|
||||
|
||||
seasonsToUpdateMode.forEach(season => {
|
||||
const seasonNumber = season.season_number ?? null
|
||||
if (seasonNumber === null) return
|
||||
|
||||
const mode = typeof seasonModes === 'string' ? seasonModes : seasonModes[seasonNumber] ?? 'normal'
|
||||
updateSubscribeMode(seasonNumber, mode)
|
||||
})
|
||||
|
||||
seasonsToSubscribe.forEach(season => {
|
||||
const seasonNumber = season.season_number ?? null
|
||||
if (seasonNumber === null) return
|
||||
|
||||
const mode = typeof seasonModes === 'string' ? seasonModes : seasonModes[seasonNumber] ?? 'normal'
|
||||
const payload = getSubscribePayload(mode)
|
||||
addSubscribe(
|
||||
seasonNumber,
|
||||
payload,
|
||||
{
|
||||
openEditDialog: seasonsToSubscribe.length === 1 && seasonsToUnsubscribe.length === 0,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
addSubscribe,
|
||||
checkSubscribe,
|
||||
handleSubscribe,
|
||||
openSubscribeSeasonDialog,
|
||||
removeSubscribe,
|
||||
subscribeSeasons,
|
||||
}
|
||||
}
|
||||
@@ -990,6 +990,8 @@ export default {
|
||||
missing: 'Missing',
|
||||
partiallyMissing: 'Partially Missing',
|
||||
subscribed: 'Subscribed',
|
||||
seasonsSubscribed: '{count} Seasons Subscribed',
|
||||
allSeasonsSubscribed: 'All Seasons Subscribed',
|
||||
},
|
||||
minutes: 'minutes',
|
||||
overview: 'Overview',
|
||||
@@ -1049,8 +1051,10 @@ export default {
|
||||
versionSub: 'Version Upgrade Subscribe',
|
||||
addSuccess: 'Added {name} successfully!',
|
||||
addFailed: 'Failed to add {name}: {message}!',
|
||||
modeUpdateSuccess: 'Updated to {mode}!',
|
||||
cancelSuccess: 'Subscription cancelled!',
|
||||
cancelFailed: 'Failed to cancel subscription: {message}!',
|
||||
notFound: 'Subscription not found!',
|
||||
filterSubscriptions: 'Filter Subscriptions',
|
||||
name: 'Name',
|
||||
searchShares: 'Search Subscription Shares',
|
||||
@@ -2290,6 +2294,7 @@ export default {
|
||||
},
|
||||
subscribeSeason: {
|
||||
title: 'Subscribe - {title}',
|
||||
selectMode: 'Subscription Type',
|
||||
selectGroup: 'Select Episode Group',
|
||||
defaultGroup: 'Default',
|
||||
seasonCount: '{count} Seasons',
|
||||
|
||||
@@ -986,6 +986,8 @@ export default {
|
||||
missing: '缺失',
|
||||
partiallyMissing: '部分缺失',
|
||||
subscribed: '已订阅',
|
||||
seasonsSubscribed: '已订阅 {count} 季',
|
||||
allSeasonsSubscribed: '已全部订阅',
|
||||
},
|
||||
minutes: '分钟',
|
||||
overview: '简介',
|
||||
@@ -1045,8 +1047,10 @@ export default {
|
||||
versionSub: '洗版订阅',
|
||||
addSuccess: '添加{name}成功!',
|
||||
addFailed: '添加{name}失败:{message}!',
|
||||
modeUpdateSuccess: '已更新为{mode}!',
|
||||
cancelSuccess: '已取消订阅!',
|
||||
cancelFailed: '取消订阅失败:{message}!',
|
||||
notFound: '订阅不存在!',
|
||||
filterSubscriptions: '筛选订阅',
|
||||
name: '名称',
|
||||
searchShares: '搜索订阅分享',
|
||||
@@ -2245,6 +2249,7 @@ export default {
|
||||
},
|
||||
subscribeSeason: {
|
||||
title: '订阅 - {title}',
|
||||
selectMode: '选择订阅方式',
|
||||
selectGroup: '选择剧集组',
|
||||
defaultGroup: '默认',
|
||||
seasonCount: '{count} 季',
|
||||
|
||||
@@ -986,6 +986,8 @@ export default {
|
||||
missing: '缺失',
|
||||
partiallyMissing: '部分缺失',
|
||||
subscribed: '已訂閱',
|
||||
seasonsSubscribed: '已訂閱 {count} 季',
|
||||
allSeasonsSubscribed: '已全部訂閱',
|
||||
},
|
||||
minutes: '分鐘',
|
||||
overview: '簡介',
|
||||
@@ -1045,8 +1047,10 @@ export default {
|
||||
versionSub: '洗版訂閱',
|
||||
addSuccess: '添加{name}成功!',
|
||||
addFailed: '添加{name}失敗:{message}!',
|
||||
modeUpdateSuccess: '已更新為{mode}!',
|
||||
cancelSuccess: '已取消訂閱!',
|
||||
cancelFailed: '取消訂閱失敗:{message}!',
|
||||
notFound: '訂閱不存在!',
|
||||
filterSubscriptions: '篩選訂閱',
|
||||
name: '名稱',
|
||||
searchShares: '搜索訂閱分享',
|
||||
@@ -2246,6 +2250,7 @@ export default {
|
||||
},
|
||||
subscribeSeason: {
|
||||
title: '訂閱 - {title}',
|
||||
selectMode: '選擇訂閱方式',
|
||||
selectGroup: '選擇劇集組',
|
||||
defaultGroup: '默認',
|
||||
seasonCount: '{count} 季',
|
||||
|
||||
@@ -5,8 +5,6 @@ import MediaCardSlideView from './MediaCardSlideView.vue'
|
||||
import api from '@/api'
|
||||
import type { MediaInfo, NotExistMediaInfo, Site, Subscribe, TmdbEpisode } from '@/api/types'
|
||||
import NoDataFound from '@/components/states/NoDataFound.vue'
|
||||
import { doneNProgress, startNProgress } from '@/api/nprogress'
|
||||
import { formatSeason } from '@/@core/utils/formatters'
|
||||
import { formatSeasonLabel } from '@/@core/utils/season'
|
||||
import router from '@/router'
|
||||
import { isNullOrEmptyObject } from '@/@core/utils'
|
||||
@@ -14,19 +12,24 @@ import { useUserStore } from '@/stores'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildUserPermissionContext, hasPermission } from '@/utils/permission'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { useGlobalSettingsStore } from '@/stores'
|
||||
import { openMediaServerItem, openDoubanApp } from '@/utils/appDeepLink'
|
||||
import { openSharedDialog } from '@/composables/useSharedDialog'
|
||||
import { getDisplayImageUrl } from '@/utils/imageUtils'
|
||||
import {
|
||||
getMediaSubscribeId,
|
||||
getSubscribeMode,
|
||||
useMediaSubscribe,
|
||||
type SeasonSubscribeModes,
|
||||
} from '@/composables/useMediaSubscribe'
|
||||
|
||||
const SearchSiteDialog = defineAsyncComponent(() => import('@/components/dialog/SearchSiteDialog.vue'))
|
||||
const SubscribeEditDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeEditDialog.vue'))
|
||||
const SubscribeModeDialog = defineAsyncComponent(() => import('@/components/dialog/SubscribeModeDialog.vue'))
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const $toast = useToast()
|
||||
|
||||
// 输入参数
|
||||
const mediaProps = defineProps({
|
||||
mediaid: String,
|
||||
@@ -46,10 +49,6 @@ const userPermissions = computed(() => buildUserPermissionContext(userStore.supe
|
||||
const canSearch = computed(() => hasPermission(userPermissions.value, 'search'))
|
||||
const canSubscribe = computed(() => hasPermission(userPermissions.value, 'subscribe'))
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
const createConfirm = useConfirm()
|
||||
|
||||
// 获取主题信息
|
||||
const theme = useTheme()
|
||||
|
||||
@@ -77,6 +76,9 @@ const seasonsNotExisted = ref<{ [key: number]: number }>({})
|
||||
// 各季的订阅状态
|
||||
const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
||||
|
||||
// 各季的订阅模式
|
||||
const subscribedSeasonModes = ref<SeasonSubscribeModes>({})
|
||||
|
||||
// 所有站点
|
||||
const allSites = ref<Site[]>([])
|
||||
|
||||
@@ -102,18 +104,6 @@ const isTransparentTheme = computed(() => {
|
||||
return theme.name.value === 'transparent'
|
||||
})
|
||||
|
||||
// 打开订阅编辑弹窗,关闭和保存时由共享 Host 自动释放实例。
|
||||
function openSubscribeEditDialog(subid: number) {
|
||||
openSharedDialog(
|
||||
SubscribeEditDialog,
|
||||
{ subid },
|
||||
{
|
||||
remove: onSubscribeEditRemove,
|
||||
},
|
||||
{ closeOn: ['close', 'save', 'remove'] },
|
||||
)
|
||||
}
|
||||
|
||||
// 打开站点选择弹窗,并把站点选择结果交回详情页执行搜索。
|
||||
function openSearchSiteDialog() {
|
||||
openSharedDialog(
|
||||
@@ -154,10 +144,11 @@ async function querySelectedSites() {
|
||||
|
||||
// 获得mediaid
|
||||
function getMediaId() {
|
||||
if (mediaDetail.value?.tmdb_id) return `tmdb:${mediaDetail.value?.tmdb_id}`
|
||||
else if (mediaDetail.value?.douban_id) return `douban:${mediaDetail.value?.douban_id}`
|
||||
else if (mediaDetail.value?.bangumi_id) return `bangumi:${mediaDetail.value?.bangumi_id}`
|
||||
else return `${mediaDetail.value?.mediaid_prefix}:${mediaDetail.value?.media_id}`
|
||||
return getMediaSubscribeId(mediaDetail.value)
|
||||
}
|
||||
|
||||
function getSubscribeStatusKey(season: number | null = mediaDetail.value?.season ?? null) {
|
||||
return `${getMediaId()}::${season ?? 'all'}`
|
||||
}
|
||||
|
||||
// 调用API查询详情
|
||||
@@ -231,16 +222,7 @@ async function checkExists() {
|
||||
// 查询当前媒体是否已订阅
|
||||
async function checkSubscribe(season: number | null = null) {
|
||||
try {
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: Subscribe = await api.get(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
title: mediaDetail.value.title,
|
||||
},
|
||||
})
|
||||
|
||||
if (result.id) return true
|
||||
return await subscribeActions.checkSubscribe(season)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
@@ -248,6 +230,17 @@ async function checkSubscribe(season: number | null = null) {
|
||||
return false
|
||||
}
|
||||
|
||||
function isSameSubscribeMedia(subscribe: Subscribe) {
|
||||
if (mediaDetail.value?.tmdb_id && subscribe.tmdbid) return mediaDetail.value.tmdb_id === subscribe.tmdbid
|
||||
if (mediaDetail.value?.douban_id && subscribe.doubanid) return mediaDetail.value.douban_id === subscribe.doubanid
|
||||
if (mediaDetail.value?.bangumi_id && subscribe.bangumiid) return mediaDetail.value.bangumi_id === subscribe.bangumiid
|
||||
|
||||
const mediaId = mediaDetail.value?.media_id
|
||||
? `${mediaDetail.value.mediaid_prefix}:${mediaDetail.value.media_id}`
|
||||
: ''
|
||||
return Boolean(mediaId && subscribe.mediaid === mediaId)
|
||||
}
|
||||
|
||||
// 检查所有季的缺失状态
|
||||
async function checkSeasonsNotExists() {
|
||||
if (mediaDetail.value.type !== '电视剧') return
|
||||
@@ -287,149 +280,50 @@ const getMediaSeasons = computed(() => {
|
||||
async function checkSeasonsSubscribed() {
|
||||
if (mediaDetail.value.type !== '电视剧') return
|
||||
try {
|
||||
mediaDetail.value?.season_info?.forEach(async item => {
|
||||
seasonsSubscribed.value[item.season_number ?? 0] = await checkSubscribe(item.season_number ?? null)
|
||||
const subscribes: Subscribe[] = await api.get('subscribe/')
|
||||
const mediaSubscribes = subscribes.filter(
|
||||
item => item.type === '电视剧' && item.season !== undefined && isSameSubscribeMedia(item),
|
||||
)
|
||||
const nextSubscribed: { [key: number]: boolean } = {}
|
||||
const nextModes: SeasonSubscribeModes = {}
|
||||
|
||||
mediaDetail.value?.season_info?.forEach(item => {
|
||||
const season = item.season_number ?? 0
|
||||
nextSubscribed[season] = false
|
||||
})
|
||||
|
||||
mediaSubscribes.forEach(item => {
|
||||
const season = item.season as number
|
||||
nextSubscribed[season] = true
|
||||
nextModes[season] = getSubscribeMode(item)
|
||||
})
|
||||
|
||||
seasonsSubscribed.value = nextSubscribed
|
||||
subscribedSeasonModes.value = nextModes
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用API添加订阅,电视剧的话需要指定季
|
||||
async function addSubscribe(season: number | null, payload: { best_version?: number; best_version_full?: number } = {}) {
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
// 请求API
|
||||
const result: { [key: string]: any } = await api.post('subscribe/', {
|
||||
name: mediaDetail.value?.title,
|
||||
type: mediaDetail.value?.type,
|
||||
year: mediaDetail.value?.year,
|
||||
tmdbid: mediaDetail.value?.tmdb_id,
|
||||
doubanid: mediaDetail.value?.douban_id,
|
||||
bangumiid: mediaDetail.value?.bangumi_id,
|
||||
season: mediaDetail.value?.type === '电影' ? null : season,
|
||||
...payload,
|
||||
})
|
||||
const subscribedSeasonNumbers = computed(() =>
|
||||
Object.entries(seasonsSubscribed.value)
|
||||
.filter(([, subscribed]) => subscribed)
|
||||
.map(([season]) => Number(season))
|
||||
.sort((a, b) => a - b),
|
||||
)
|
||||
|
||||
// 订阅状态
|
||||
if (result.success) {
|
||||
// 订阅成功
|
||||
isSubscribed.value = true
|
||||
if (season !== null) seasonsSubscribed.value[season] = true
|
||||
}
|
||||
const subscribeSeasonTotal = computed(() => getMediaSeasons.value.length)
|
||||
|
||||
// 提示
|
||||
showSubscribeAddToast(result.success, mediaDetail.value?.title ?? '', season, result.message, payload.best_version ?? 0)
|
||||
|
||||
// 显示编辑弹窗
|
||||
if (result.success) {
|
||||
const show_edit_dialog = await queryDefaultSubscribeConfig()
|
||||
if (show_edit_dialog) {
|
||||
openSubscribeEditDialog(result.data.id)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
|
||||
// 弹出添加订阅提示
|
||||
function showSubscribeAddToast(result: boolean, title: string, season: number | null, message: string, best_version: number) {
|
||||
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
let subname = t('subscribe.normalSub')
|
||||
if (best_version > 0) subname = t('subscribe.versionSub')
|
||||
|
||||
if (result) $toast.success(`${title} ${t('subscribe.addSuccess', { name: subname })}`)
|
||||
else $toast.error(`${title} ${t('media.subscribe.addFailed', { reason: message })}`)
|
||||
}
|
||||
|
||||
// 调用API取消订阅
|
||||
async function removeSubscribe(season: number | null) {
|
||||
const confirmed = await createConfirm({
|
||||
title: t('common.confirm'),
|
||||
content: t('dialog.subscribeEdit.cancelSubscribeConfirm'),
|
||||
})
|
||||
if (!confirmed) return
|
||||
|
||||
// 开始处理
|
||||
startNProgress()
|
||||
try {
|
||||
const mediaid = getMediaId()
|
||||
|
||||
const result: { [key: string]: any } = await api.delete(`subscribe/media/${mediaid}`, {
|
||||
params: {
|
||||
season,
|
||||
},
|
||||
})
|
||||
let title = mediaDetail.value?.title ?? ''
|
||||
if (season !== null) title = `${title} ${formatSeason(season.toString())}`
|
||||
|
||||
if (result.success) {
|
||||
isSubscribed.value = false
|
||||
if (season !== null) seasonsSubscribed.value[season] = false
|
||||
$toast.success(`${title} ${t('media.subscribe.canceled')}`)
|
||||
} else {
|
||||
$toast.error(`${title} ${t('media.subscribe.cancelFailed', { reason: result.message })}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
doneNProgress()
|
||||
}
|
||||
const isAllSeasonsSubscribed = computed(
|
||||
() =>
|
||||
mediaDetail.value.type === '电视剧' &&
|
||||
subscribeSeasonTotal.value > 0 &&
|
||||
subscribedSeasonNumbers.value.length >= subscribeSeasonTotal.value,
|
||||
)
|
||||
|
||||
// 订阅按钮响应
|
||||
function handleSubscribe(season: number | null = null) {
|
||||
if (season !== null) {
|
||||
if (seasonsSubscribed.value[season]) {
|
||||
removeSubscribe(season)
|
||||
return
|
||||
}
|
||||
|
||||
if (!seasonsNotExisted.value[season]) {
|
||||
openSharedDialog(
|
||||
SubscribeModeDialog,
|
||||
{ modes: ['normal', 'best_version', 'best_version_full'], type: mediaDetail.value?.type },
|
||||
{
|
||||
choose: (mode: string) =>
|
||||
addSubscribe(season, {
|
||||
best_version: mode === 'normal' ? 0 : 1,
|
||||
best_version_full: mode === 'best_version_full' ? 1 : 0,
|
||||
}),
|
||||
},
|
||||
{ closeOn: ['close', 'choose'] },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
addSubscribe(season)
|
||||
return
|
||||
}
|
||||
|
||||
if (isSubscribed.value) {
|
||||
removeSubscribe(season)
|
||||
return
|
||||
}
|
||||
|
||||
if (mediaDetail.value?.type === '电影' && existsItemId.value) {
|
||||
openSharedDialog(
|
||||
SubscribeModeDialog,
|
||||
{ modes: ['normal', 'best_version'], type: mediaDetail.value?.type },
|
||||
{
|
||||
choose: (mode: string) =>
|
||||
addSubscribe(season, {
|
||||
best_version: mode === 'normal' ? 0 : 1,
|
||||
best_version_full: 0,
|
||||
}),
|
||||
},
|
||||
{ closeOn: ['close', 'choose'] },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
addSubscribe(season)
|
||||
subscribeActions.handleSubscribe(season)
|
||||
}
|
||||
|
||||
// 从genres中获取name,使用、分隔
|
||||
@@ -543,16 +437,33 @@ function getExistText(season: number) {
|
||||
|
||||
// 计算订阅图标
|
||||
const getSubscribeIcon = computed(() => {
|
||||
if (mediaDetail.value.type === '电视剧') return subscribedSeasonNumbers.value.length > 0 ? 'mdi-heart' : 'mdi-heart-outline'
|
||||
if (isSubscribed.value) return 'mdi-heart'
|
||||
else return 'mdi-heart-outline'
|
||||
})
|
||||
|
||||
// 计算订阅按钮颜色
|
||||
const getSubscribeColor = computed(() => {
|
||||
if (mediaDetail.value.type === '电视剧') {
|
||||
if (isAllSeasonsSubscribed.value) return 'error'
|
||||
if (subscribedSeasonNumbers.value.length > 0) return 'warning'
|
||||
return 'warning'
|
||||
}
|
||||
if (isSubscribed.value) return 'error'
|
||||
else return 'warning'
|
||||
})
|
||||
|
||||
const getSubscribeText = computed(() => {
|
||||
if (mediaDetail.value.type === '电视剧') {
|
||||
if (isAllSeasonsSubscribed.value) return t('media.status.allSeasonsSubscribed')
|
||||
if (subscribedSeasonNumbers.value.length > 0) {
|
||||
return t('media.status.seasonsSubscribed', { count: subscribedSeasonNumbers.value.length })
|
||||
}
|
||||
return t('media.actions.subscribe')
|
||||
}
|
||||
return isSubscribed.value ? t('media.status.subscribed') : t('media.actions.subscribe')
|
||||
})
|
||||
|
||||
// 使用、拼装数组为字符串
|
||||
function joinArray(arr: string[]) {
|
||||
return arr.join('、')
|
||||
@@ -600,28 +511,25 @@ async function handlePlay() {
|
||||
}
|
||||
}
|
||||
|
||||
async function queryDefaultSubscribeConfig() {
|
||||
if (!canSubscribe.value) return false
|
||||
try {
|
||||
let subscribe_config_url = ''
|
||||
if (mediaProps.type === '电影') subscribe_config_url = 'system/setting/public/DefaultMovieSubscribeConfig'
|
||||
else subscribe_config_url = 'system/setting/public/DefaultTvSubscribeConfig'
|
||||
|
||||
const result: { [key: string]: any } = await api.get(subscribe_config_url)
|
||||
|
||||
if (result.data?.value) return result.data.value.show_edit_dialog
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 删除订阅处理
|
||||
function onSubscribeEditRemove() {
|
||||
if (mediaDetail.value.type === '电影') checkMovieSubscribed()
|
||||
else checkSeasonsSubscribed()
|
||||
}
|
||||
|
||||
const subscribeActions = useMediaSubscribe({
|
||||
media: () => mediaDetail.value,
|
||||
canSubscribe: () => canSubscribe.value,
|
||||
isSubscribed,
|
||||
isExists: () => Boolean(existsItemId.value),
|
||||
seasonsSubscribed,
|
||||
subscribedSeasons: subscribedSeasonNumbers,
|
||||
subscribedSeasonModes,
|
||||
primarySeason: () => mediaDetail.value?.season ?? null,
|
||||
getSubscribeStatusKey,
|
||||
onEditRemove: onSubscribeEditRemove,
|
||||
})
|
||||
|
||||
// 搜索前弹出站点选择框,确认后执行资源或字幕搜索。
|
||||
async function clickSearch(type: string, resultType: 'torrent' | 'subtitle' = 'torrent', options: MediaSearchOptions = {}) {
|
||||
searchType.value = type
|
||||
@@ -722,7 +630,7 @@ onBeforeMount(() => {
|
||||
canSearch
|
||||
"
|
||||
variant="tonal"
|
||||
color="info"
|
||||
color="primary"
|
||||
class="mb-2"
|
||||
>
|
||||
<template #prepend>
|
||||
@@ -756,7 +664,7 @@ onBeforeMount(() => {
|
||||
{{ t('media.actions.searchSubtitle') }}
|
||||
</VBtn>
|
||||
<VBtn
|
||||
v-if="canSubscribe && (mediaDetail.type === '电影' || mediaDetail.douban_id || mediaDetail.bangumi_id)"
|
||||
v-if="canSubscribe && (mediaDetail.type === '电影' || mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id)"
|
||||
class="ms-2 mb-2"
|
||||
:color="getSubscribeColor"
|
||||
variant="tonal"
|
||||
@@ -765,7 +673,7 @@ onBeforeMount(() => {
|
||||
<template #prepend>
|
||||
<VIcon :icon="getSubscribeIcon" />
|
||||
</template>
|
||||
{{ isSubscribed ? t('media.status.subscribed') : t('media.actions.subscribe') }}
|
||||
{{ getSubscribeText }}
|
||||
</VBtn>
|
||||
<VBtn v-if="existsItemId" class="ms-2 mb-2" variant="tonal" @click="handlePlay()">
|
||||
<template #prepend>
|
||||
|
||||
Reference in New Issue
Block a user