mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-02-03 02:04:06 +08:00
chore: bump version to 3.2.0-beta.9
Fix TypeScript error in ab-search-bar.vue and include pending changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "auto-bangumi"
|
||||
version = "3.2.0-beta.8"
|
||||
version = "3.2.0-beta.9"
|
||||
description = "AutoBangumi - Automated anime download manager"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
|
||||
@@ -59,6 +59,12 @@ class Checker:
|
||||
@staticmethod
|
||||
async def check_downloader() -> bool:
|
||||
from module.downloader import DownloadClient
|
||||
|
||||
# Mock downloader always succeeds
|
||||
if settings.downloader.type == "mock":
|
||||
logger.info("[Checker] Using MockDownloader - skipping connection check")
|
||||
return True
|
||||
|
||||
try:
|
||||
url = (
|
||||
f"http://{settings.downloader.host}"
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import json
|
||||
import logging
|
||||
from typing import TypeAlias
|
||||
|
||||
from module.models import Bangumi, RSSItem, Torrent
|
||||
from module.network import RequestContent
|
||||
from module.parser.analyser.tmdb_parser import tmdb_parser
|
||||
from module.rss import RSSAnalyser
|
||||
|
||||
from .provider import search_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SEARCH_KEY = [
|
||||
"group_name",
|
||||
"title_raw",
|
||||
@@ -18,13 +22,32 @@ SEARCH_KEY = [
|
||||
|
||||
BangumiJSON: TypeAlias = str
|
||||
|
||||
# Cache for TMDB poster lookups by official_title
|
||||
_poster_cache: dict[str, str | None] = {}
|
||||
|
||||
|
||||
class SearchTorrent(RequestContent, RSSAnalyser):
|
||||
async def search_torrents(self, rss_item: RSSItem) -> list[Torrent]:
|
||||
return await self.get_torrents(rss_item.url)
|
||||
|
||||
async def _fetch_tmdb_poster(self, title: str) -> str | None:
|
||||
"""Fetch poster from TMDB if not in cache."""
|
||||
if title in _poster_cache:
|
||||
return _poster_cache[title]
|
||||
|
||||
try:
|
||||
tmdb_info = await tmdb_parser(title, "zh", test=True)
|
||||
if tmdb_info and tmdb_info.poster_link:
|
||||
_poster_cache[title] = tmdb_info.poster_link
|
||||
return tmdb_info.poster_link
|
||||
except Exception as e:
|
||||
logger.debug(f"[Searcher] Failed to fetch TMDB poster for {title}: {e}")
|
||||
|
||||
_poster_cache[title] = None
|
||||
return None
|
||||
|
||||
async def analyse_keyword(
|
||||
self, keywords: list[str], site: str = "mikan", limit: int = 5
|
||||
self, keywords: list[str], site: str = "mikan", limit: int = 100
|
||||
):
|
||||
rss_item = search_url(site, keywords)
|
||||
torrents = await self.search_torrents(rss_item)
|
||||
@@ -39,6 +62,11 @@ class SearchTorrent(RequestContent, RSSAnalyser):
|
||||
if special_link not in exist_list:
|
||||
bangumi.rss_link = special_link
|
||||
exist_list.append(special_link)
|
||||
# Fetch poster from TMDB if missing
|
||||
if not bangumi.poster_link and bangumi.official_title:
|
||||
tmdb_poster = await self._fetch_tmdb_poster(bangumi.official_title)
|
||||
if tmdb_poster:
|
||||
bangumi.poster_link = tmdb_poster
|
||||
yield json.dumps(bangumi.dict(), separators=(",", ":"))
|
||||
|
||||
@staticmethod
|
||||
|
||||
2
backend/uv.lock
generated
2
backend/uv.lock
generated
@@ -54,7 +54,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "auto-bangumi"
|
||||
version = "3.2.0b4"
|
||||
version = "3.2.0b7"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
|
||||
@@ -25,7 +25,7 @@ export const apiDownloader = {
|
||||
return data!;
|
||||
},
|
||||
|
||||
async deleteTorrents(hashes: string[], deleteFiles: boolean = false) {
|
||||
async deleteTorrents(hashes: string[], deleteFiles = false) {
|
||||
const { data } = await axios.post<ApiSuccess>(
|
||||
'api/v1/downloader/torrents/delete',
|
||||
{ hashes, delete_files: deleteFiles }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { CheckOne, Close, Copy, Down, ErrorPicture, Right } from '@icon-park/vue-next';
|
||||
import { NDynamicTags, NSpin, useMessage } from 'naive-ui';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -20,6 +22,23 @@ const rule = defineModel<BangumiRule>('rule', {
|
||||
required: true,
|
||||
});
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// Local deep copy for editing (prevents mutation of original)
|
||||
const localRule = ref<BangumiRule>(JSON.parse(JSON.stringify(rule.value)));
|
||||
|
||||
// Sync when rule changes (e.g., opening different item)
|
||||
watch(rule, (newVal) => {
|
||||
localRule.value = JSON.parse(JSON.stringify(newVal));
|
||||
}, { deep: true });
|
||||
|
||||
const posterSrc = computed(() => resolvePosterUrl(localRule.value.poster_link));
|
||||
const showAdvanced = ref(true);
|
||||
const copied = ref(false);
|
||||
const offsetLoading = ref(false);
|
||||
const offsetReason = ref('');
|
||||
|
||||
// Delete file dialog state
|
||||
const deleteFileDialog = reactive<{
|
||||
show: boolean;
|
||||
type: 'disable' | 'delete';
|
||||
@@ -27,24 +46,77 @@ const deleteFileDialog = reactive<{
|
||||
show: false,
|
||||
type: 'disable',
|
||||
});
|
||||
|
||||
watch(show, (val) => {
|
||||
if (!val) {
|
||||
deleteFileDialog.show = false;
|
||||
showAdvanced.value = false;
|
||||
offsetReason.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
function showDeleteFileDialog(type: string) {
|
||||
deleteFileDialog.show = true;
|
||||
if (type === 'disable' || type === '禁用') {
|
||||
deleteFileDialog.type = 'disable';
|
||||
} else {
|
||||
deleteFileDialog.type = 'delete';
|
||||
// Info tags for display
|
||||
const infoTags = computed(() => {
|
||||
const tags: { value: string; type: string }[] = [];
|
||||
const { season, season_raw, dpi, subtitle, group_name } = localRule.value;
|
||||
|
||||
if (season || season_raw) {
|
||||
const seasonDisplay = season_raw || (season ? `S${season}` : '');
|
||||
tags.push({ value: seasonDisplay, type: 'season' });
|
||||
}
|
||||
|
||||
if (dpi) {
|
||||
tags.push({ value: dpi, type: 'resolution' });
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
tags.push({ value: subtitle, type: 'subtitle' });
|
||||
}
|
||||
|
||||
if (group_name) {
|
||||
tags.push({ value: group_name, type: 'group' });
|
||||
}
|
||||
|
||||
return tags;
|
||||
});
|
||||
|
||||
// Copy RSS link
|
||||
async function copyRssLink() {
|
||||
const rssLink = localRule.value.rss_link?.[0] || '';
|
||||
if (rssLink) {
|
||||
await navigator.clipboard.writeText(rssLink);
|
||||
copied.value = true;
|
||||
setTimeout(() => {
|
||||
copied.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto detect offset
|
||||
async function autoDetectOffset() {
|
||||
if (!localRule.value.id) return;
|
||||
offsetLoading.value = true;
|
||||
offsetReason.value = '';
|
||||
try {
|
||||
const result = await apiBangumi.suggestOffset(localRule.value.id);
|
||||
localRule.value.offset = result.suggested_offset;
|
||||
offsetReason.value = result.reason;
|
||||
} catch (e) {
|
||||
console.error('Failed to detect offset:', e);
|
||||
message.error('Failed to detect offset');
|
||||
} finally {
|
||||
offsetLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => (show.value = false);
|
||||
|
||||
function emitdeleteFile(deleteFile: boolean) {
|
||||
function showDeleteFileDialog() {
|
||||
deleteFileDialog.show = true;
|
||||
deleteFileDialog.type = 'delete';
|
||||
}
|
||||
|
||||
function emitDeleteFile(deleteFile: boolean) {
|
||||
emit('deleteFile', deleteFileDialog.type, {
|
||||
id: rule.value.id,
|
||||
deleteFile,
|
||||
@@ -52,6 +124,8 @@ function emitdeleteFile(deleteFile: boolean) {
|
||||
}
|
||||
|
||||
function emitApply() {
|
||||
// Copy local changes back to rule before emitting
|
||||
Object.assign(rule.value, localRule.value);
|
||||
emit('apply', rule.value);
|
||||
}
|
||||
|
||||
@@ -66,109 +140,771 @@ function emitArchive() {
|
||||
function emitUnarchive() {
|
||||
emit('unarchive', rule.value.id);
|
||||
}
|
||||
|
||||
const popupTitle = computed(() => {
|
||||
if (rule.value.deleted) {
|
||||
return t('homepage.rule.enable_rule');
|
||||
} else {
|
||||
return t('homepage.rule.edit_rule');
|
||||
}
|
||||
});
|
||||
|
||||
const boxSize = computed(() => {
|
||||
if (rule.value.deleted) {
|
||||
return 'w-300';
|
||||
} else {
|
||||
return 'w-460';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Enable deleted rule dialog -->
|
||||
<ab-popup
|
||||
v-if="rule.deleted"
|
||||
v-model:show="show"
|
||||
:title="popupTitle"
|
||||
:css="`${boxSize} max-w-[90vw]`"
|
||||
:title="$t('homepage.rule.enable_rule')"
|
||||
css="w-300 max-w-[90vw]"
|
||||
>
|
||||
<div v-if="rule.deleted">
|
||||
<div>{{ $t('homepage.rule.enable_hit') }}</div>
|
||||
|
||||
<div line my-8></div>
|
||||
|
||||
<div f-cer gap-x-10>
|
||||
<ab-button size="small" type="warn" @click="() => emitEnable()">
|
||||
{{ $t('homepage.rule.yes_btn') }}
|
||||
</ab-button>
|
||||
<ab-button size="small" @click="() => close()">
|
||||
{{ $t('homepage.rule.no_btn') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
<div>{{ $t('homepage.rule.enable_hit') }}</div>
|
||||
<div line my-8></div>
|
||||
<div f-cer gap-x-10>
|
||||
<ab-button size="small" type="warn" @click="emitEnable">
|
||||
{{ $t('homepage.rule.yes_btn') }}
|
||||
</ab-button>
|
||||
<ab-button size="small" @click="close">
|
||||
{{ $t('homepage.rule.no_btn') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="edit-rule-content">
|
||||
<ab-rule v-model:rule="rule"></ab-rule>
|
||||
|
||||
<div class="edit-rule-actions">
|
||||
<ab-button
|
||||
v-if="rule.archived"
|
||||
size="small"
|
||||
@click="emitUnarchive"
|
||||
>
|
||||
{{ $t('homepage.rule.unarchive') }}
|
||||
</ab-button>
|
||||
<ab-button
|
||||
v-else
|
||||
size="small"
|
||||
@click="emitArchive"
|
||||
>
|
||||
{{ $t('homepage.rule.archive') }}
|
||||
</ab-button>
|
||||
<ab-button-multi
|
||||
size="small"
|
||||
type="warn"
|
||||
:selections="[t('homepage.rule.delete'), t('homepage.rule.disable')]"
|
||||
@click="showDeleteFileDialog"
|
||||
/>
|
||||
<ab-button size="small" @click="emitApply">
|
||||
{{ $t('homepage.rule.apply') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ab-popup
|
||||
v-model:show="deleteFileDialog.show"
|
||||
:title="$t('homepage.rule.delete')"
|
||||
>
|
||||
<div>{{ $t('homepage.rule.delete_hit') }}</div>
|
||||
<div line my-8></div>
|
||||
|
||||
<div f-cer gap-x-10>
|
||||
<ab-button size="small" type="warn" @click="() => emitdeleteFile(true)">
|
||||
{{ $t('homepage.rule.yes_btn') }}
|
||||
</ab-button>
|
||||
<ab-button size="small" @click="() => emitdeleteFile(false)">
|
||||
{{ $t('homepage.rule.no_btn') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
</ab-popup>
|
||||
</ab-popup>
|
||||
|
||||
<!-- Main edit modal -->
|
||||
<Teleport v-else to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="show" class="edit-backdrop" @click.self="close">
|
||||
<div class="edit-modal" role="dialog" aria-modal="true">
|
||||
<!-- Header -->
|
||||
<header class="edit-header">
|
||||
<h2 class="edit-title">{{ $t('homepage.rule.edit_rule') }}</h2>
|
||||
<button class="close-btn" aria-label="Close" @click="close">
|
||||
<Close theme="outline" size="18" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="edit-content">
|
||||
<!-- Bangumi Info -->
|
||||
<div class="bangumi-info">
|
||||
<div class="bangumi-poster">
|
||||
<template v-if="localRule.poster_link">
|
||||
<img :src="posterSrc" :alt="localRule.official_title" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="poster-placeholder">
|
||||
<ErrorPicture theme="outline" size="32" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="bangumi-meta">
|
||||
<input
|
||||
v-model="localRule.official_title"
|
||||
type="text"
|
||||
class="title-input"
|
||||
:placeholder="$t('homepage.rule.official_title')"
|
||||
/>
|
||||
<p v-if="localRule.title_raw" class="bangumi-subtitle">{{ localRule.title_raw }}</p>
|
||||
<div class="meta-row">
|
||||
<input
|
||||
:value="localRule.year ?? ''"
|
||||
type="text"
|
||||
class="year-input"
|
||||
:class="{ 'year-input--empty': !localRule.year }"
|
||||
:placeholder="$t('homepage.rule.year')"
|
||||
@input="(e) => localRule.year = (e.target as HTMLInputElement).value || null"
|
||||
/>
|
||||
<span class="meta-separator">·</span>
|
||||
<label class="season-label">S</label>
|
||||
<input
|
||||
v-model.number="localRule.season"
|
||||
type="number"
|
||||
class="season-input"
|
||||
min="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Tags -->
|
||||
<div v-if="infoTags.length > 0" class="info-tags">
|
||||
<div
|
||||
v-for="tag in infoTags"
|
||||
:key="tag.type"
|
||||
class="info-tag"
|
||||
:class="`info-tag--${tag.type}`"
|
||||
>
|
||||
{{ tag.value }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RSS Link -->
|
||||
<div v-if="localRule.rss_link?.[0]" class="rss-section">
|
||||
<div class="info-row">
|
||||
<span class="info-label">{{ $t('search.confirm.rss') }}:</span>
|
||||
<span class="info-value info-value--link">
|
||||
{{ localRule.rss_link?.[0] || '-' }}
|
||||
</span>
|
||||
<button class="copy-btn" :class="{ copied }" @click="copyRssLink">
|
||||
<CheckOne v-if="copied" theme="outline" size="14" />
|
||||
<Copy v-else theme="outline" size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced settings -->
|
||||
<div class="advanced-section">
|
||||
<button class="advanced-toggle" @click="showAdvanced = !showAdvanced">
|
||||
<component :is="showAdvanced ? Down : Right" theme="outline" size="14" />
|
||||
{{ $t('search.confirm.advanced') }}
|
||||
</button>
|
||||
|
||||
<Transition name="expand">
|
||||
<div v-show="showAdvanced" class="advanced-content">
|
||||
<!-- Filter rules row -->
|
||||
<div class="advanced-row advanced-row--tags">
|
||||
<label class="advanced-label">{{ $t('homepage.rule.filter') }}</label>
|
||||
<div class="advanced-control filter-tags">
|
||||
<NDynamicTags v-model:value="localRule.filter" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offset row -->
|
||||
<div class="advanced-row">
|
||||
<label class="advanced-label">{{ $t('homepage.rule.offset') }}</label>
|
||||
<div class="advanced-control offset-controls">
|
||||
<input
|
||||
v-model.number="localRule.offset"
|
||||
type="number"
|
||||
ab-input
|
||||
class="offset-input"
|
||||
/>
|
||||
<button
|
||||
class="detect-btn"
|
||||
:disabled="offsetLoading"
|
||||
@click="autoDetectOffset"
|
||||
>
|
||||
<NSpin v-if="offsetLoading" :size="14" />
|
||||
<span v-else>{{ $t('homepage.rule.auto_detect') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="offsetReason" class="offset-reason">{{ offsetReason }}</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="edit-footer">
|
||||
<div class="footer-left">
|
||||
<ab-button
|
||||
v-if="localRule.archived"
|
||||
size="small"
|
||||
@click="emitUnarchive"
|
||||
>
|
||||
{{ $t('homepage.rule.unarchive') }}
|
||||
</ab-button>
|
||||
<ab-button
|
||||
v-else
|
||||
size="small"
|
||||
@click="emitArchive"
|
||||
>
|
||||
{{ $t('homepage.rule.archive') }}
|
||||
</ab-button>
|
||||
<ab-button
|
||||
size="small"
|
||||
type="warn"
|
||||
@click="showDeleteFileDialog"
|
||||
>
|
||||
{{ $t('homepage.rule.delete') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<ab-button size="small" @click="emitApply">
|
||||
{{ $t('homepage.rule.apply') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirmation dialog -->
|
||||
<Transition name="modal">
|
||||
<div v-if="deleteFileDialog.show" class="delete-dialog-backdrop" @click.self="deleteFileDialog.show = false">
|
||||
<div class="delete-dialog">
|
||||
<h3 class="delete-title">{{ $t('homepage.rule.delete') }}</h3>
|
||||
<p class="delete-message">{{ $t('homepage.rule.delete_hit') }}</p>
|
||||
<div class="delete-actions">
|
||||
<ab-button size="small" type="secondary" @click="emitDeleteFile(false)">
|
||||
{{ $t('homepage.rule.no_btn') }}
|
||||
</ab-button>
|
||||
<ab-button size="small" type="warn" @click="emitDeleteFile(true)">
|
||||
{{ $t('homepage.rule.yes_btn') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-rule-content {
|
||||
.edit-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-overlay);
|
||||
z-index: var(--z-modal);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.edit-rule-actions {
|
||||
.edit-modal {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.edit-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-hover);
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
// Bangumi info section
|
||||
.bangumi-info {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bangumi-poster {
|
||||
width: 80px;
|
||||
height: 112px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface-hover);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.poster-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.bangumi-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.title-input {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 4px 0;
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-border);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.bangumi-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.year-input {
|
||||
width: 60px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 2px 0;
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast), background-color var(--transition-fast);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-border);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
&--empty {
|
||||
background: color-mix(in srgb, var(--color-warning) 15%, transparent);
|
||||
border-bottom-color: var(--color-warning);
|
||||
border-radius: var(--radius-xs) var(--radius-xs) 0 0;
|
||||
padding: 2px 4px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.meta-separator {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.season-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.season-input {
|
||||
width: 40px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
padding: 2px 0;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
transition: border-color var(--transition-fast);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-border);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
// Hide spinner buttons
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
// Info Tags
|
||||
.info-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
&--season {
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&--resolution {
|
||||
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
&--subtitle {
|
||||
background: color-mix(in srgb, var(--color-success) 12%, transparent);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
&--group {
|
||||
background: color-mix(in srgb, var(--color-warning) 12%, transparent);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
}
|
||||
|
||||
// RSS section
|
||||
.rss-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--color-surface-hover);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
flex-shrink: 0;
|
||||
width: 70px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
word-break: break-all;
|
||||
|
||||
&--link {
|
||||
color: var(--color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
color: var(--color-text-muted);
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&.copied {
|
||||
background: var(--color-success);
|
||||
border-color: var(--color-success);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// Advanced section
|
||||
.advanced-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.advanced-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: var(--color-surface-hover);
|
||||
border-radius: var(--radius-md);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.advanced-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 32px;
|
||||
|
||||
&--tags {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.advanced-control {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
:deep(.n-dynamic-tags) {
|
||||
justify-content: flex-end;
|
||||
min-height: 32px;
|
||||
|
||||
.n-tag {
|
||||
height: 28px;
|
||||
margin: 2px 0 2px 6px !important;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.offset-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding-top: 4px;
|
||||
gap: 8px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.offset-input {
|
||||
width: 70px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detect-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 80px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: var(--color-primary);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: wait;
|
||||
}
|
||||
}
|
||||
|
||||
.offset-reason {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
// Expand transition
|
||||
.expand-enter-active,
|
||||
.expand-leave-active {
|
||||
transition: all var(--transition-normal);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.expand-enter-from,
|
||||
.expand-leave-to {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
// Footer
|
||||
.edit-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// Delete confirmation dialog
|
||||
.delete-dialog-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-overlay);
|
||||
z-index: calc(var(--z-modal) + 10);
|
||||
}
|
||||
|
||||
.delete-dialog {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 24px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.delete-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.delete-message {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
.delete-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// Modal transition
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
.edit-modal,
|
||||
.delete-dialog {
|
||||
transition: transform 200ms ease, opacity 200ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.edit-modal,
|
||||
.delete-dialog {
|
||||
transform: scale(0.95) translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 480px) {
|
||||
.edit-footer {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,46 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import AbSearchModal from './search/ab-search-modal.vue';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
defineEmits<{
|
||||
(e: 'add-bangumi', bangumiRule: BangumiRule): void;
|
||||
}>();
|
||||
|
||||
const { showModal, provider, loading, inputValue } = storeToRefs(useSearchStore());
|
||||
const { toggleModal, onSearch, getProviders } = useSearchStore();
|
||||
const { showModal, provider, loading } = storeToRefs(useSearchStore());
|
||||
const { toggleModal, getProviders } = useSearchStore();
|
||||
|
||||
onMounted(() => {
|
||||
getProviders();
|
||||
});
|
||||
|
||||
// Handle click on search input - toggle modal
|
||||
function handleSearchClick() {
|
||||
toggleModal();
|
||||
}
|
||||
|
||||
// Handle search trigger from input
|
||||
function handleSearch() {
|
||||
if (!showModal.value) {
|
||||
toggleModal();
|
||||
}
|
||||
onSearch();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Compact search trigger -->
|
||||
<!-- Search trigger button -->
|
||||
<ab-search
|
||||
v-model:input-value="inputValue"
|
||||
:provider="provider"
|
||||
:loading="loading"
|
||||
@search="handleSearch"
|
||||
@select="handleSearchClick"
|
||||
@click="handleSearchClick"
|
||||
@click="toggleModal"
|
||||
/>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<AbSearchModal
|
||||
@close="toggleModal"
|
||||
@add-bangumi="(bangumi) => $emit('add-bangumi', bangumi)"
|
||||
/>
|
||||
<AbSearchModal @close="toggleModal" />
|
||||
</template>
|
||||
|
||||
@@ -25,42 +25,49 @@ const showSelections = ref<boolean>(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="btn-multi" :class="[`btn-multi--${size}`, `btn-multi--${type}`]">
|
||||
<Component
|
||||
:is="link !== null ? 'a' : 'button'"
|
||||
:href="link"
|
||||
class="btn-multi-main"
|
||||
@click="$emit('click', selected)"
|
||||
>
|
||||
<NSpin :show="loading" :size="size === 'big' ? 'large' : 'small'">
|
||||
<div class="btn-multi-label">{{ selected }}</div>
|
||||
</NSpin>
|
||||
</Component>
|
||||
<div
|
||||
class="btn-multi-arrow"
|
||||
@click="() => (showSelections = !showSelections)"
|
||||
>
|
||||
<Down fill="white" />
|
||||
<div class="btn-multi-container">
|
||||
<div class="btn-multi" :class="[`btn-multi--${size}`, `btn-multi--${type}`]">
|
||||
<Component
|
||||
:is="link !== null ? 'a' : 'button'"
|
||||
:href="link"
|
||||
class="btn-multi-main"
|
||||
@click="$emit('click', selected)"
|
||||
>
|
||||
<NSpin :show="loading" :size="size === 'big' ? 'large' : 'small'">
|
||||
<div class="btn-multi-label">{{ selected }}</div>
|
||||
</NSpin>
|
||||
</Component>
|
||||
<div
|
||||
class="btn-multi-arrow"
|
||||
@click="() => (showSelections = !showSelections)"
|
||||
>
|
||||
<Down fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showSelections"
|
||||
class="btn-multi-dropdown"
|
||||
:class="[`btn-multi--${size}`, `btn-multi--${type}`]"
|
||||
>
|
||||
<div
|
||||
v-for="selection in selections"
|
||||
:key="selection"
|
||||
class="btn-multi-option"
|
||||
@click="() => { selected = selection; showSelections = false; }"
|
||||
v-if="showSelections"
|
||||
class="btn-multi-dropdown"
|
||||
:class="[`btn-multi--${size}`, `btn-multi--${type}`]"
|
||||
>
|
||||
{{ selection }}
|
||||
<div
|
||||
v-for="selection in selections"
|
||||
:key="selection"
|
||||
class="btn-multi-option"
|
||||
@click="() => { selected = selection; showSelections = false; }"
|
||||
>
|
||||
{{ selection }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.btn-multi-container {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.btn-multi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -90,26 +97,22 @@ const showSelections = ref<boolean>(false);
|
||||
|
||||
&--primary {
|
||||
.btn-multi-main,
|
||||
.btn-multi-arrow,
|
||||
.btn-multi-option {
|
||||
.btn-multi-arrow {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
.btn-multi-main:hover,
|
||||
.btn-multi-arrow:hover,
|
||||
.btn-multi-option:hover {
|
||||
.btn-multi-arrow:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&--warn {
|
||||
.btn-multi-main,
|
||||
.btn-multi-arrow,
|
||||
.btn-multi-option {
|
||||
.btn-multi-arrow {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
.btn-multi-main:hover,
|
||||
.btn-multi-arrow:hover,
|
||||
.btn-multi-option:hover {
|
||||
.btn-multi-arrow:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
@@ -146,11 +149,32 @@ const showSelections = ref<boolean>(false);
|
||||
|
||||
.btn-multi-dropdown {
|
||||
position: absolute;
|
||||
z-index: 70;
|
||||
left: 0;
|
||||
bottom: 100%;
|
||||
margin-bottom: 4px;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
transform: translateY(80%) translateX(-111%);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-width: 100%;
|
||||
|
||||
&.btn-multi--primary {
|
||||
.btn-multi-option {
|
||||
background: var(--color-primary);
|
||||
&:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-multi--warn {
|
||||
.btn-multi-option {
|
||||
background: var(--color-danger);
|
||||
&:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-multi-option {
|
||||
@@ -158,11 +182,12 @@ const showSelections = ref<boolean>(false);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #fff;
|
||||
font-size: inherit;
|
||||
transition: background-color var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
export interface DataListColumn {
|
||||
key: string;
|
||||
@@ -8,7 +8,7 @@ export interface DataListColumn {
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
type DataItem = Record<string, any>;
|
||||
|
||||
const props = withDefaults(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { Down, Search } from '@icon-park/vue-next';
|
||||
import { Search } from '@icon-park/vue-next';
|
||||
import { NSpin } from 'naive-ui';
|
||||
|
||||
withDefaults(
|
||||
@@ -13,123 +13,94 @@ withDefaults(
|
||||
}
|
||||
);
|
||||
|
||||
defineEmits<{ select: []; search: []; click: [] }>();
|
||||
|
||||
const inputValue = defineModel<string>('inputValue');
|
||||
defineEmits<{ click: [] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-input" role="search">
|
||||
<button
|
||||
v-if="!loading"
|
||||
class="search-icon-btn"
|
||||
aria-label="Search"
|
||||
@click="$emit('search')"
|
||||
>
|
||||
<Search theme="outline" size="20" class="search-icon" />
|
||||
</button>
|
||||
<NSpin v-else :size="18" />
|
||||
<button
|
||||
class="search-trigger"
|
||||
role="search"
|
||||
aria-label="Open search"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<NSpin v-if="loading" :size="16" class="search-spinner" />
|
||||
<Search v-else theme="outline" size="18" class="search-icon" />
|
||||
|
||||
<input
|
||||
v-model="inputValue"
|
||||
type="text"
|
||||
:placeholder="$t('topbar.search.placeholder')"
|
||||
class="search-field"
|
||||
aria-label="Search anime"
|
||||
@click="$emit('click')"
|
||||
@keyup.enter="$emit('search')"
|
||||
/>
|
||||
<span class="search-placeholder">{{ $t('topbar.search.placeholder') }}</span>
|
||||
|
||||
<button
|
||||
class="search-provider"
|
||||
aria-label="Select search provider"
|
||||
@click="$emit('select')"
|
||||
>
|
||||
<div class="search-provider-label">{{ provider }}</div>
|
||||
<Down :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
<span class="search-provider">{{ provider }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-input {
|
||||
.search-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding-left: 12px;
|
||||
padding: 0 6px 0 12px;
|
||||
gap: 10px;
|
||||
width: 360px;
|
||||
min-width: 240px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: border-color var(--transition-fast),
|
||||
background-color var(--transition-normal);
|
||||
background-color var(--transition-normal),
|
||||
box-shadow var(--transition-fast);
|
||||
|
||||
&:focus-within {
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-alpha);
|
||||
}
|
||||
|
||||
@include forDesktop {
|
||||
min-width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-muted);
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
.search-icon-btn:hover & {
|
||||
.search-trigger:hover & {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.search-field {
|
||||
.search-spinner {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.search-provider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
min-width: 80px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.search-provider-label {
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.search-provider {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
.search-trigger:hover & {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue';
|
||||
import { nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -91,8 +91,8 @@ defineExpose({ goTo });
|
||||
:key="i"
|
||||
class="ab-swipe-container__dot"
|
||||
:class="{ 'ab-swipe-container__dot--active': currentIndex === i - 1 }"
|
||||
@click="goTo(i - 1)"
|
||||
:aria-label="`Go to slide ${i}`"
|
||||
@click="goTo(i - 1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
Refresh,
|
||||
} from '@icon-park/vue-next';
|
||||
import { ruleTemplate } from '#/bangumi';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const { t, changeLocale } = useMyI18n();
|
||||
const { running, onUpdate, offUpdate } = useAppInfo();
|
||||
|
||||
const showAccount = ref(false);
|
||||
const showAddRSS = ref(false);
|
||||
const searchRule = ref<BangumiRule>();
|
||||
const rssRule = ref(ruleTemplate);
|
||||
|
||||
const { start, pause, shutdown, restart, resetRule } = useProgramStore();
|
||||
const { refreshPoster } = useBangumiStore();
|
||||
@@ -70,17 +69,9 @@ const items = [
|
||||
const { isDark } = useDarkMode();
|
||||
const onSearchFocus = ref(false);
|
||||
|
||||
function addSearchResult(bangumi: BangumiRule) {
|
||||
showAddRSS.value = true;
|
||||
searchRule.value = bangumi;
|
||||
}
|
||||
|
||||
watch(showAddRSS, (val) => {
|
||||
if (!val) {
|
||||
searchRule.value = ruleTemplate;
|
||||
setTimeout(() => {
|
||||
onSearchFocus.value = false;
|
||||
}, 300);
|
||||
rssRule.value = ruleTemplate;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,7 +102,7 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="topbar-search">
|
||||
<ab-search-bar @add-bangumi="addSearchResult" />
|
||||
<ab-search-bar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,7 +118,7 @@ onUnmounted(() => {
|
||||
<ab-change-account v-model:show="showAccount"></ab-change-account>
|
||||
<ab-add-rss
|
||||
v-model:show="showAddRSS"
|
||||
v-model:rule="searchRule"
|
||||
v-model:rule="rssRule"
|
||||
></ab-add-rss>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import { ErrorPicture, Plus } from '@icon-park/vue-next';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
import { ErrorPicture } from '@icon-park/vue-next';
|
||||
import type { GroupedBangumi } from '@/store/search';
|
||||
|
||||
const props = defineProps<{
|
||||
bangumi: BangumiRule;
|
||||
group: GroupedBangumi;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', bangumi: BangumiRule): void;
|
||||
(e: 'select', group: GroupedBangumi): void;
|
||||
}>();
|
||||
|
||||
const posterSrc = computed(() => resolvePosterUrl(props.bangumi.poster_link));
|
||||
const posterSrc = computed(() => resolvePosterUrl(props.group.poster_link));
|
||||
|
||||
// Format season display
|
||||
const seasonDisplay = computed(() => {
|
||||
if (props.bangumi.season_raw) {
|
||||
return props.bangumi.season_raw;
|
||||
}
|
||||
return props.bangumi.season ? `S${props.bangumi.season}` : '';
|
||||
});
|
||||
// Count of variants
|
||||
const variantCount = computed(() => props.group.variants.length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -26,48 +21,29 @@ const seasonDisplay = computed(() => {
|
||||
class="search-card"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="`Add ${bangumi.official_title}`"
|
||||
@click="emit('select', bangumi)"
|
||||
@keydown.enter="emit('select', bangumi)"
|
||||
:aria-label="`View ${group.official_title}`"
|
||||
@click="emit('select', group)"
|
||||
@keydown.enter="emit('select', group)"
|
||||
>
|
||||
<!-- Poster -->
|
||||
<div class="card-poster">
|
||||
<template v-if="bangumi.poster_link">
|
||||
<img :src="posterSrc" :alt="bangumi.official_title" loading="lazy" />
|
||||
<template v-if="group.poster_link">
|
||||
<img :src="posterSrc" :alt="group.official_title" loading="lazy" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="card-placeholder">
|
||||
<ErrorPicture theme="outline" size="24" />
|
||||
<ErrorPicture theme="outline" size="32" />
|
||||
</div>
|
||||
</template>
|
||||
<!-- Variant count badge -->
|
||||
<div v-if="variantCount > 1" class="variant-badge">
|
||||
{{ variantCount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="card-info">
|
||||
<h3 class="card-title">{{ bangumi.official_title }}</h3>
|
||||
<p v-if="bangumi.title_raw" class="card-subtitle">{{ bangumi.title_raw }}</p>
|
||||
|
||||
<div class="card-tags">
|
||||
<span v-if="bangumi.group_name" class="tag tag-group">
|
||||
{{ bangumi.group_name }}
|
||||
</span>
|
||||
<span v-if="bangumi.dpi" class="tag tag-resolution">
|
||||
{{ bangumi.dpi }}
|
||||
</span>
|
||||
<span v-if="bangumi.subtitle" class="tag tag-subtitle">
|
||||
{{ bangumi.subtitle }}
|
||||
</span>
|
||||
<span v-if="seasonDisplay" class="tag tag-season">
|
||||
{{ seasonDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add button -->
|
||||
<div class="card-action">
|
||||
<div class="add-btn">
|
||||
<Plus theme="outline" size="18" />
|
||||
</div>
|
||||
<h3 class="card-title">{{ group.official_title }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -75,24 +51,27 @@ const seasonDisplay = computed(() => {
|
||||
<style lang="scss" scoped>
|
||||
.search-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
flex-direction: column;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
.add-btn {
|
||||
.select-btn {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-poster img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -106,10 +85,10 @@ const seasonDisplay = computed(() => {
|
||||
}
|
||||
|
||||
.card-poster {
|
||||
width: 60px;
|
||||
height: 84px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 5 / 7;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background: var(--color-surface-hover);
|
||||
|
||||
@@ -117,6 +96,7 @@ const seasonDisplay = computed(() => {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,92 +107,41 @@ const seasonDisplay = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.variant-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-full);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
transition: color var(--transition-normal);
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-full);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-group {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tag-resolution {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.tag-subtitle {
|
||||
background: rgba(249, 115, 22, 0.15);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.tag-season {
|
||||
background: var(--color-surface-hover);
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.card-action {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--color-surface-hover);
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { CheckOne, Close, Copy, Down, ErrorPicture, Right } from '@icon-park/vue
|
||||
import { NDynamicTags, NSpin, useMessage } from 'naive-ui';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const props = defineProps<{
|
||||
bangumi: BangumiRule;
|
||||
}>();
|
||||
@@ -14,6 +12,8 @@ const emit = defineEmits<{
|
||||
(e: 'cancel'): void;
|
||||
}>();
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// Local deep copy of bangumi for editing (prevents mutation of original prop)
|
||||
const localBangumi = ref<BangumiRule>(JSON.parse(JSON.stringify(props.bangumi)));
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import { Close, Down, Search } from '@icon-park/vue-next';
|
||||
import { NSpin } from 'naive-ui';
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
import AbSearchFilters from './ab-search-filters.vue';
|
||||
import AbSearchCard from './ab-search-card.vue';
|
||||
import AbSearchConfirm from './ab-search-confirm.vue';
|
||||
import type { BangumiRule } from '#/bangumi';
|
||||
import type { GroupedBangumi } from '@/store/search';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
@@ -19,10 +18,7 @@ const {
|
||||
provider,
|
||||
loading,
|
||||
inputValue,
|
||||
bangumiList,
|
||||
filteredResults,
|
||||
filters,
|
||||
filterOptions,
|
||||
groupedResults,
|
||||
showModal,
|
||||
selectedResult,
|
||||
} = storeToRefs(useSearchStore());
|
||||
@@ -31,8 +27,6 @@ const {
|
||||
getProviders,
|
||||
onSearch,
|
||||
clearSearch,
|
||||
toggleFilter,
|
||||
clearFilters,
|
||||
selectResult,
|
||||
clearSelectedResult,
|
||||
} = useSearchStore();
|
||||
@@ -42,6 +36,19 @@ const subscribing = ref(false);
|
||||
const showProvider = ref(false);
|
||||
const searchInputRef = ref<HTMLInputElement | null>(null);
|
||||
|
||||
// Tag filter state
|
||||
const activeFilters = ref<{
|
||||
group: string | null;
|
||||
resolution: string | null;
|
||||
subtitle: string | null;
|
||||
season: string | null;
|
||||
}>({
|
||||
group: null,
|
||||
resolution: null,
|
||||
subtitle: null,
|
||||
season: null,
|
||||
});
|
||||
|
||||
// Close on Escape
|
||||
onKeyStroke('Escape', () => {
|
||||
if (selectedResult.value) {
|
||||
@@ -59,12 +66,17 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// Clear filters when search changes
|
||||
watch(inputValue, () => {
|
||||
activeFilters.value = { group: null, resolution: null, subtitle: null, season: null };
|
||||
});
|
||||
|
||||
function onSelectProvider(site: string) {
|
||||
provider.value = site;
|
||||
showProvider.value = false;
|
||||
}
|
||||
|
||||
function handleCardClick(bangumi: BangumiRule) {
|
||||
function handleVariantSelect(bangumi: BangumiRule) {
|
||||
selectResult(bangumi);
|
||||
}
|
||||
|
||||
@@ -98,8 +110,72 @@ async function handleConfirm(bangumi: BangumiRule) {
|
||||
|
||||
function handleClose() {
|
||||
clearSearch();
|
||||
activeFilters.value = { group: null, resolution: null, subtitle: null, season: null };
|
||||
emit('close');
|
||||
}
|
||||
|
||||
// Get resolution display for variant
|
||||
function getResolution(bangumi: BangumiRule): string {
|
||||
return bangumi.dpi || '';
|
||||
}
|
||||
|
||||
// Get subtitle display for variant
|
||||
function getSubtitle(bangumi: BangumiRule): string {
|
||||
return bangumi.subtitle || '';
|
||||
}
|
||||
|
||||
// Get season display for variant
|
||||
function getSeason(bangumi: BangumiRule): string {
|
||||
if (bangumi.season_raw) return bangumi.season_raw;
|
||||
if (bangumi.season) return `S${bangumi.season}`;
|
||||
return '';
|
||||
}
|
||||
|
||||
// Resolve poster URL for template
|
||||
function getPosterUrl(link: string | null | undefined): string {
|
||||
return resolvePosterUrl(link);
|
||||
}
|
||||
|
||||
// Toggle filter
|
||||
function toggleFilter(type: 'group' | 'resolution' | 'subtitle' | 'season', value: string) {
|
||||
if (activeFilters.value[type] === value) {
|
||||
activeFilters.value[type] = null;
|
||||
} else {
|
||||
activeFilters.value[type] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if variant matches active filters
|
||||
function variantMatchesFilters(variant: BangumiRule): boolean {
|
||||
const { group, resolution, subtitle, season } = activeFilters.value;
|
||||
|
||||
if (group && variant.group_name !== group) return false;
|
||||
if (resolution && getResolution(variant) !== resolution) return false;
|
||||
if (subtitle && getSubtitle(variant) !== subtitle) return false;
|
||||
if (season && getSeason(variant) !== season) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get filtered variants for a group
|
||||
function getFilteredVariants(group: GroupedBangumi): BangumiRule[] {
|
||||
const hasActiveFilter = Object.values(activeFilters.value).some(v => v !== null);
|
||||
if (!hasActiveFilter) return group.variants;
|
||||
return group.variants.filter(variantMatchesFilters);
|
||||
}
|
||||
|
||||
// Check if tag is active
|
||||
function isTagActive(type: 'group' | 'resolution' | 'subtitle' | 'season', value: string): boolean {
|
||||
return activeFilters.value[type] === value;
|
||||
}
|
||||
|
||||
// Check if any filter is active
|
||||
const hasActiveFilters = computed(() => Object.values(activeFilters.value).some(v => v !== null));
|
||||
|
||||
// Clear all filters
|
||||
function clearFilters() {
|
||||
activeFilters.value = { group: null, resolution: null, subtitle: null, season: null };
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -167,43 +243,92 @@ function handleClose() {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- Filters -->
|
||||
<AbSearchFilters
|
||||
:filters="filters"
|
||||
:filter-options="filterOptions"
|
||||
:filtered-count="filteredResults.length"
|
||||
:total-count="bangumiList.length"
|
||||
@toggle-filter="toggleFilter"
|
||||
@clear-filters="clearFilters"
|
||||
/>
|
||||
<!-- Active Filters Bar -->
|
||||
<div v-if="hasActiveFilters" class="active-filters">
|
||||
<span class="filter-label">{{ $t('search.filter.active') }}:</span>
|
||||
<span v-if="activeFilters.group" class="filter-tag filter-tag-group" @click="toggleFilter('group', activeFilters.group)">
|
||||
{{ activeFilters.group }} ×
|
||||
</span>
|
||||
<span v-if="activeFilters.resolution" class="filter-tag filter-tag-res" @click="toggleFilter('resolution', activeFilters.resolution)">
|
||||
{{ activeFilters.resolution }} ×
|
||||
</span>
|
||||
<span v-if="activeFilters.subtitle" class="filter-tag filter-tag-sub" @click="toggleFilter('subtitle', activeFilters.subtitle)">
|
||||
{{ activeFilters.subtitle }} ×
|
||||
</span>
|
||||
<span v-if="activeFilters.season" class="filter-tag filter-tag-season" @click="toggleFilter('season', activeFilters.season)">
|
||||
{{ activeFilters.season }} ×
|
||||
</span>
|
||||
<button class="clear-filters-btn" @click="clearFilters">{{ $t('search.filter.clear') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Results Grid -->
|
||||
<!-- Results List -->
|
||||
<div class="results-container">
|
||||
<!-- Empty state -->
|
||||
<div v-if="!loading && filteredResults.length === 0 && inputValue" class="empty-state">
|
||||
<div v-if="!loading && groupedResults.length === 0 && inputValue" class="empty-state">
|
||||
<p>{{ $t('search.no_results') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Initial state -->
|
||||
<div v-else-if="!inputValue && filteredResults.length === 0" class="empty-state">
|
||||
<div v-else-if="!inputValue && groupedResults.length === 0" class="empty-state">
|
||||
<p>{{ $t('search.start_typing') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Results grid -->
|
||||
<transition-group
|
||||
v-else
|
||||
name="card"
|
||||
tag="div"
|
||||
class="results-grid"
|
||||
>
|
||||
<AbSearchCard
|
||||
v-for="(result, index) in filteredResults"
|
||||
:key="result.order"
|
||||
:bangumi="result.value"
|
||||
:style="{ '--stagger-index': index }"
|
||||
@select="handleCardClick"
|
||||
/>
|
||||
</transition-group>
|
||||
<!-- Bangumi list -->
|
||||
<div v-else class="bangumi-list">
|
||||
<template v-for="group in groupedResults" :key="group.key">
|
||||
<div
|
||||
v-if="getFilteredVariants(group).length > 0"
|
||||
class="bangumi-row"
|
||||
>
|
||||
<!-- Left: Poster -->
|
||||
<div class="bangumi-poster">
|
||||
<img
|
||||
v-if="group.poster_link"
|
||||
:src="getPosterUrl(group.poster_link)"
|
||||
:alt="group.official_title"
|
||||
/>
|
||||
<div v-else class="bangumi-poster-placeholder">
|
||||
<span class="placeholder-title">{{ group.official_title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Variant Grid -->
|
||||
<div class="bangumi-variants">
|
||||
<div
|
||||
v-for="variant in getFilteredVariants(group)"
|
||||
:key="variant.rss_link?.[0] || variant.title_raw"
|
||||
class="variant-chip"
|
||||
@click.stop="handleVariantSelect(variant)"
|
||||
>
|
||||
<span
|
||||
v-if="variant.group_name"
|
||||
class="chip-tag chip-tag-group"
|
||||
:class="{ active: isTagActive('group', variant.group_name) }"
|
||||
@click.stop="toggleFilter('group', variant.group_name)"
|
||||
>{{ variant.group_name }}</span>
|
||||
<span
|
||||
v-if="getResolution(variant)"
|
||||
class="chip-tag chip-tag-res"
|
||||
:class="{ active: isTagActive('resolution', getResolution(variant)) }"
|
||||
@click.stop="toggleFilter('resolution', getResolution(variant))"
|
||||
>{{ getResolution(variant) }}</span>
|
||||
<span
|
||||
v-if="getSubtitle(variant)"
|
||||
class="chip-tag chip-tag-sub"
|
||||
:class="{ active: isTagActive('subtitle', getSubtitle(variant)) }"
|
||||
@click.stop="toggleFilter('subtitle', getSubtitle(variant))"
|
||||
>{{ getSubtitle(variant) }}</span>
|
||||
<span
|
||||
v-if="getSeason(variant)"
|
||||
class="chip-tag chip-tag-season"
|
||||
:class="{ active: isTagActive('season', getSeason(variant)) }"
|
||||
@click.stop="toggleFilter('season', getSeason(variant))"
|
||||
>{{ getSeason(variant) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -414,24 +539,6 @@ function handleClose() {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.results-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@include forTablet {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@include forDesktop {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1200px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -460,22 +567,201 @@ function handleClose() {
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
}
|
||||
|
||||
// Card stagger animation
|
||||
.card-enter-active {
|
||||
transition: opacity var(--transition-slow), transform var(--transition-slow);
|
||||
transition-delay: calc(var(--stagger-index, 0) * 40ms);
|
||||
// Bangumi list
|
||||
.bangumi-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card-leave-active {
|
||||
transition: opacity 150ms ease-in;
|
||||
.bangumi-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.card-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
.bangumi-poster {
|
||||
width: 100px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@include forDesktop {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
aspect-ratio: 5 / 7;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.card-leave-to {
|
||||
opacity: 0;
|
||||
.bangumi-poster-placeholder {
|
||||
width: 100%;
|
||||
aspect-ratio: 5 / 7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.bangumi-variants {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.variant-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
|
||||
.chip-tag {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chip-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.chip-tag-group {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.chip-tag-res {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.chip-tag-sub {
|
||||
background: rgba(249, 115, 22, 0.15);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.chip-tag-season {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: rgb(139, 92, 246);
|
||||
}
|
||||
|
||||
// Active filters bar
|
||||
.active-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: var(--color-surface-hover);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.filter-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-tag-group {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-tag-res {
|
||||
background: var(--color-success);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-tag-sub {
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-tag-season {
|
||||
background: rgb(139, 92, 246);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.clear-filters-btn {
|
||||
margin-left: auto;
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
color: var(--color-text-muted);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
// Active tag highlight
|
||||
.chip-tag.active {
|
||||
box-shadow: 0 0 0 2px var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,10 +81,10 @@ const azureItems: SettingItem<ExperimentalOpenAI>[] = [
|
||||
</div>
|
||||
|
||||
<ab-setting
|
||||
v-model:data="openAI.enable"
|
||||
config-key="enable"
|
||||
:label="() => t('config.experimental_openai_set.enable')"
|
||||
type="switch"
|
||||
v-model:data="openAI.enable"
|
||||
/>
|
||||
|
||||
<transition name="slide-fade">
|
||||
|
||||
@@ -44,7 +44,7 @@ export const useMyI18n = createSharedComposable(() => {
|
||||
function returnUserLangText(texts: {
|
||||
[k in Languages]: string;
|
||||
}) {
|
||||
return texts[lang.value] ?? texts['en'];
|
||||
return texts[lang.value] ?? texts.en;
|
||||
}
|
||||
|
||||
function returnUserLangMsg(res: ApiSuccess) {
|
||||
|
||||
@@ -333,9 +333,11 @@
|
||||
},
|
||||
"common": {
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm"
|
||||
"confirm": "Confirm",
|
||||
"select": "Select"
|
||||
},
|
||||
"search": {
|
||||
"subscribe": "Subscribe",
|
||||
"no_results": "No results found. Try different keywords.",
|
||||
"start_typing": "Enter keywords to start searching",
|
||||
"filter": {
|
||||
@@ -344,7 +346,8 @@
|
||||
"subtitle_type": "Subtitle",
|
||||
"season": "Season",
|
||||
"clear": "Clear",
|
||||
"results": "results"
|
||||
"results": "results",
|
||||
"active": "Filtering"
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Add Subscription",
|
||||
@@ -352,8 +355,11 @@
|
||||
"group": "Subtitle Group",
|
||||
"resolution": "Resolution",
|
||||
"subtitle": "Subtitle Type",
|
||||
"season": "Season",
|
||||
"filter": "Filter Rules",
|
||||
"filter_hint": "Regex patterns to exclude unwanted torrents",
|
||||
"save_path": "Save Path",
|
||||
"save_path_placeholder": "Leave empty for default path",
|
||||
"advanced": "Advanced Settings",
|
||||
"subscribe": "Subscribe"
|
||||
}
|
||||
|
||||
@@ -333,9 +333,11 @@
|
||||
},
|
||||
"common": {
|
||||
"cancel": "取消",
|
||||
"confirm": "确认"
|
||||
"confirm": "确认",
|
||||
"select": "选择"
|
||||
},
|
||||
"search": {
|
||||
"subscribe": "订阅",
|
||||
"no_results": "未找到相关结果,试试其他关键词",
|
||||
"start_typing": "输入关键词开始搜索",
|
||||
"filter": {
|
||||
@@ -344,7 +346,8 @@
|
||||
"subtitle_type": "字幕语言",
|
||||
"season": "季度",
|
||||
"clear": "清除筛选",
|
||||
"results": "个结果"
|
||||
"results": "个结果",
|
||||
"active": "筛选中"
|
||||
},
|
||||
"confirm": {
|
||||
"title": "添加订阅",
|
||||
@@ -352,8 +355,11 @@
|
||||
"group": "字幕组",
|
||||
"resolution": "分辨率",
|
||||
"subtitle": "字幕类型",
|
||||
"season": "季度",
|
||||
"filter": "过滤规则",
|
||||
"filter_hint": "用于排除不需要的种子,支持正则表达式",
|
||||
"save_path": "保存路径",
|
||||
"save_path_placeholder": "留空使用默认路径",
|
||||
"advanced": "高级设置",
|
||||
"subscribe": "确认订阅"
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
|
||||
.archived-badge {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
bottom: 36px; // Above the card title area
|
||||
left: 4px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="tsx" setup>
|
||||
import { NDataTable, NTooltip, type DataTableColumns } from 'naive-ui';
|
||||
import { type DataTableColumns, NDataTable, NTooltip } from 'naive-ui';
|
||||
import type { RSS } from '#/rss';
|
||||
|
||||
definePage({
|
||||
|
||||
@@ -15,6 +15,15 @@ export interface FilterOptions {
|
||||
season: string[];
|
||||
}
|
||||
|
||||
// Grouped bangumi result
|
||||
export interface GroupedBangumi {
|
||||
key: string;
|
||||
official_title: string;
|
||||
poster_link: string;
|
||||
year?: string | null;
|
||||
variants: BangumiRule[];
|
||||
}
|
||||
|
||||
// Helper to parse metadata from title/bangumi
|
||||
function parseResolution(bangumi: BangumiRule): string {
|
||||
// Check dpi field first
|
||||
@@ -81,9 +90,10 @@ export const useSearchStore = defineStore('search', () => {
|
||||
// Modal state
|
||||
const showModal = ref(false);
|
||||
const selectedResult = ref<BangumiRule | null>(null);
|
||||
const selectedGroup = ref<GroupedBangumi | null>(null);
|
||||
|
||||
// Filter state
|
||||
const filters = ref<SearchFilters>({
|
||||
// Filter state for selected group variants
|
||||
const variantFilters = ref<SearchFilters>({
|
||||
group: [],
|
||||
resolution: [],
|
||||
subtitleType: [],
|
||||
@@ -97,14 +107,46 @@ export const useSearchStore = defineStore('search', () => {
|
||||
searchData.value.map((item, index) => ({ order: index, value: item }))
|
||||
);
|
||||
|
||||
// Extract filter options from results
|
||||
const filterOptions = computed<FilterOptions>(() => {
|
||||
// Group results by official_title
|
||||
const groupedResults = computed<GroupedBangumi[]>(() => {
|
||||
const map = new Map<string, BangumiRule[]>();
|
||||
|
||||
for (const item of searchData.value) {
|
||||
const key = item.official_title || item.title_raw || '';
|
||||
if (!map.has(key)) {
|
||||
map.set(key, []);
|
||||
}
|
||||
map.get(key)!.push(item);
|
||||
}
|
||||
|
||||
const groups: GroupedBangumi[] = [];
|
||||
for (const [key, variants] of map) {
|
||||
// Use the first variant's poster and info
|
||||
const first = variants[0];
|
||||
groups.push({
|
||||
key,
|
||||
official_title: first.official_title || first.title_raw || '',
|
||||
poster_link: first.poster_link || '',
|
||||
year: first.year,
|
||||
variants,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
});
|
||||
|
||||
// Extract filter options from selected group's variants
|
||||
const variantFilterOptions = computed<FilterOptions>(() => {
|
||||
if (!selectedGroup.value) {
|
||||
return { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
}
|
||||
|
||||
const groups = new Set<string>();
|
||||
const resolutions = new Set<string>();
|
||||
const subtitleTypes = new Set<string>();
|
||||
const seasons = new Set<string>();
|
||||
|
||||
for (const item of searchData.value) {
|
||||
for (const item of selectedGroup.value.variants) {
|
||||
if (item.group_name) groups.add(item.group_name);
|
||||
|
||||
const res = parseResolution(item);
|
||||
@@ -120,7 +162,6 @@ export const useSearchStore = defineStore('search', () => {
|
||||
return {
|
||||
group: Array.from(groups).sort(),
|
||||
resolution: Array.from(resolutions).sort((a, b) => {
|
||||
// Sort by resolution quality descending
|
||||
const order = ['4k', '2160p', '1080p', '720p', '480p'];
|
||||
return order.indexOf(a.toLowerCase()) - order.indexOf(b.toLowerCase());
|
||||
}),
|
||||
@@ -129,43 +170,43 @@ export const useSearchStore = defineStore('search', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Filtered results
|
||||
const filteredResults = computed<SearchResult[]>(() => {
|
||||
const hasFilters = Object.values(filters.value).some((arr) => arr.length > 0);
|
||||
// Filtered variants based on selected filters
|
||||
const filteredVariants = computed<BangumiRule[]>(() => {
|
||||
if (!selectedGroup.value) return [];
|
||||
|
||||
const hasFilters = Object.values(variantFilters.value).some((arr) => arr.length > 0);
|
||||
if (!hasFilters) {
|
||||
return bangumiList.value;
|
||||
return selectedGroup.value.variants;
|
||||
}
|
||||
|
||||
return bangumiList.value.filter((item) => {
|
||||
const bangumi = item.value;
|
||||
|
||||
return selectedGroup.value.variants.filter((bangumi) => {
|
||||
// Group filter
|
||||
if (filters.value.group.length > 0) {
|
||||
if (!bangumi.group_name || !filters.value.group.includes(bangumi.group_name)) {
|
||||
if (variantFilters.value.group.length > 0) {
|
||||
if (!bangumi.group_name || !variantFilters.value.group.includes(bangumi.group_name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolution filter
|
||||
if (filters.value.resolution.length > 0) {
|
||||
if (variantFilters.value.resolution.length > 0) {
|
||||
const res = parseResolution(bangumi);
|
||||
if (!res || !filters.value.resolution.includes(res)) {
|
||||
if (!res || !variantFilters.value.resolution.includes(res)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Subtitle type filter
|
||||
if (filters.value.subtitleType.length > 0) {
|
||||
if (variantFilters.value.subtitleType.length > 0) {
|
||||
const subType = parseSubtitleType(bangumi);
|
||||
if (!subType || !filters.value.subtitleType.includes(subType)) {
|
||||
if (!subType || !variantFilters.value.subtitleType.includes(subType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Season filter
|
||||
if (filters.value.season.length > 0) {
|
||||
if (variantFilters.value.season.length > 0) {
|
||||
const season = parseSeason(bangumi);
|
||||
if (!season || !filters.value.season.includes(season)) {
|
||||
if (!season || !variantFilters.value.season.includes(season)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -199,12 +240,13 @@ export const useSearchStore = defineStore('search', () => {
|
||||
function clearSearch() {
|
||||
keyword.value = '';
|
||||
searchData.value = [];
|
||||
filters.value = { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
selectedGroup.value = null;
|
||||
closeSearch();
|
||||
}
|
||||
|
||||
function toggleFilter(category: keyof SearchFilters, value: string) {
|
||||
const arr = filters.value[category];
|
||||
function toggleVariantFilter(category: keyof SearchFilters, value: string) {
|
||||
const arr = variantFilters.value[category];
|
||||
const index = arr.indexOf(value);
|
||||
if (index === -1) {
|
||||
arr.push(value);
|
||||
@@ -213,8 +255,18 @@ export const useSearchStore = defineStore('search', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filters.value = { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
function clearVariantFilters() {
|
||||
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
}
|
||||
|
||||
function selectGroup(group: GroupedBangumi) {
|
||||
selectedGroup.value = group;
|
||||
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
}
|
||||
|
||||
function clearSelectedGroup() {
|
||||
selectedGroup.value = null;
|
||||
variantFilters.value = { group: [], resolution: [], subtitleType: [], season: [] };
|
||||
}
|
||||
|
||||
function selectResult(bangumi: BangumiRule) {
|
||||
@@ -232,9 +284,11 @@ export const useSearchStore = defineStore('search', () => {
|
||||
provider,
|
||||
providers,
|
||||
bangumiList,
|
||||
filteredResults,
|
||||
filters,
|
||||
filterOptions,
|
||||
groupedResults,
|
||||
selectedGroup,
|
||||
variantFilters,
|
||||
variantFilterOptions,
|
||||
filteredVariants,
|
||||
showModal,
|
||||
selectedResult,
|
||||
|
||||
@@ -246,8 +300,10 @@ export const useSearchStore = defineStore('search', () => {
|
||||
openModal,
|
||||
closeModal,
|
||||
toggleModal,
|
||||
toggleFilter,
|
||||
clearFilters,
|
||||
toggleVariantFilter,
|
||||
clearVariantFilters,
|
||||
selectGroup,
|
||||
clearSelectedGroup,
|
||||
selectResult,
|
||||
clearSelectedResult,
|
||||
};
|
||||
|
||||
4
webui/types/dts/components.d.ts
vendored
4
webui/types/dts/components.d.ts
vendored
@@ -31,6 +31,10 @@ declare module '@vue/runtime-core' {
|
||||
AbRule: typeof import('./../../src/components/ab-rule.vue')['default']
|
||||
AbSearch: typeof import('./../../src/components/basic/ab-search.vue')['default']
|
||||
AbSearchBar: typeof import('./../../src/components/ab-search-bar.vue')['default']
|
||||
AbSearchCard: typeof import('./../../src/components/search/ab-search-card.vue')['default']
|
||||
AbSearchConfirm: typeof import('./../../src/components/search/ab-search-confirm.vue')['default']
|
||||
AbSearchFilters: typeof import('./../../src/components/search/ab-search-filters.vue')['default']
|
||||
AbSearchModal: typeof import('./../../src/components/search/ab-search-modal.vue')['default']
|
||||
AbSelect: typeof import('./../../src/components/basic/ab-select.vue')['default']
|
||||
AbSetting: typeof import('./../../src/components/ab-setting.vue')['default']
|
||||
AbSidebar: typeof import('./../../src/components/layout/ab-sidebar.vue')['default']
|
||||
|
||||
@@ -15,7 +15,7 @@ export const rssTemplate: RSS = {
|
||||
name: '',
|
||||
url: '',
|
||||
aggregate: false,
|
||||
parser: '',
|
||||
parser: 'tmdb',
|
||||
enabled: false,
|
||||
connection_status: null,
|
||||
last_checked_at: null,
|
||||
|
||||
Reference in New Issue
Block a user