fix: 正确处理“我追的合集 / 收藏夹”中的收藏夹条目,以及一些样式、文本调整 (#553)

This commit is contained in:
ᴀᴍᴛᴏᴀᴇʀ
2025-12-05 16:38:10 +08:00
committed by GitHub
parent f37d9af678
commit b5ef76b0ed
14 changed files with 277 additions and 242 deletions

12
Cargo.lock generated
View File

@@ -367,6 +367,7 @@ dependencies = [
"git2",
"handlebars",
"hex",
"itertools 0.14.0",
"leaky-bucket",
"md5",
"memchr",
@@ -1785,6 +1786,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
@@ -2468,7 +2478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools",
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.96",

View File

@@ -36,6 +36,7 @@ futures = "0.3.31"
git2 = { version = "0.20.2", features = [], default-features = false }
handlebars = "6.3.2"
hex = "0.4.3"
itertools = "0.14.0"
leaky-bucket = "1.1.2"
md5 = "0.8.0"
memchr = "2.7.6"

View File

@@ -29,6 +29,7 @@ float-ord = { workspace = true }
futures = { workspace = true }
handlebars = { workspace = true }
hex = { workspace = true }
itertools = { workspace = true }
leaky-bucket = { workspace = true }
md5 = { workspace = true }
memchr = { workspace = true }

View File

@@ -92,47 +92,48 @@ where
}
#[derive(Serialize)]
pub struct FavoriteWithSubscriptionStatus {
pub title: String,
pub media_count: i64,
pub fid: i64,
pub mid: i64,
pub subscribed: bool,
}
#[derive(Serialize)]
pub struct CollectionWithSubscriptionStatus {
pub title: String,
pub sid: i64,
pub mid: i64,
pub invalid: bool,
pub subscribed: bool,
}
#[derive(Serialize)]
pub struct UpperWithSubscriptionStatus {
pub mid: i64,
pub uname: String,
pub face: String,
pub sign: String,
pub invalid: bool,
pub subscribed: bool,
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Followed {
Favorite {
title: String,
media_count: i64,
fid: i64,
mid: i64,
invalid: bool,
subscribed: bool,
},
Collection {
title: String,
sid: i64,
mid: i64,
media_count: i64,
invalid: bool,
subscribed: bool,
},
Upper {
mid: i64,
uname: String,
face: String,
sign: String,
invalid: bool,
subscribed: bool,
},
}
#[derive(Serialize)]
pub struct FavoritesResponse {
pub favorites: Vec<FavoriteWithSubscriptionStatus>,
pub favorites: Vec<Followed>,
}
#[derive(Serialize)]
pub struct CollectionsResponse {
pub collections: Vec<CollectionWithSubscriptionStatus>,
pub collections: Vec<Followed>,
pub total: i64,
}
#[derive(Serialize)]
pub struct UppersResponse {
pub uppers: Vec<UpperWithSubscriptionStatus>,
pub uppers: Vec<Followed>,
pub total: i64,
}

View File

@@ -6,13 +6,11 @@ use axum::Router;
use axum::extract::{Extension, Query};
use axum::routing::get;
use bili_sync_entity::*;
use itertools::{Either, Itertools};
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect};
use crate::api::request::{FollowedCollectionsRequest, FollowedUppersRequest};
use crate::api::response::{
CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse,
UpperWithSubscriptionStatus, UppersResponse,
};
use crate::api::response::{CollectionsResponse, FavoritesResponse, Followed, UppersResponse};
use crate::api::wrapper::{ApiError, ApiResponse};
use crate::bilibili::{BiliClient, Me};
use crate::config::VersionedConfig;
@@ -36,25 +34,26 @@ pub async fn get_created_favorites(
let favorites = if let Some(bili_favorites) = bili_favorites {
// b 站收藏夹相关接口使用的所谓“fid”其实是该处的 id即 fid + mid 后两位
let bili_fids: Vec<_> = bili_favorites.iter().map(|fav| fav.id).collect();
let subscribed_fids: Vec<i64> = favorite::Entity::find()
let subscribed_fids: HashSet<i64> = favorite::Entity::find()
.select_only()
.column(favorite::Column::FId)
.filter(favorite::Column::FId.is_in(bili_fids))
.into_tuple()
.all(&db)
.await?;
let subscribed_set: HashSet<i64> = subscribed_fids.into_iter().collect();
.await?
.into_iter()
.collect();
bili_favorites
.into_iter()
.map(|fav| FavoriteWithSubscriptionStatus {
.map(|fav| Followed::Favorite {
title: fav.title,
media_count: fav.media_count,
// api 返回的 id 才是真实的 fid
fid: fav.id,
mid: fav.mid,
subscribed: subscribed_set.contains(&fav.id),
invalid: false,
subscribed: subscribed_fids.contains(&fav.id),
})
.collect()
} else {
@@ -64,7 +63,7 @@ pub async fn get_created_favorites(
Ok(ApiResponse::ok(FavoritesResponse { favorites }))
}
/// 获取当前用户收藏的合集
/// 获取当前用户收藏的合集/收藏夹
pub async fn get_followed_collections(
Extension(db): Extension<DatabaseConnection>,
Extension(bili_client): Extension<Arc<BiliClient>>,
@@ -76,25 +75,63 @@ pub async fn get_followed_collections(
let bili_collections = me.get_followed_collections(page_num, page_size).await?;
let collections = if let Some(collection_list) = bili_collections.list {
let bili_sids: Vec<_> = collection_list.iter().map(|col| col.id).collect();
let subscribed_ids: Vec<i64> = collection::Entity::find()
.select_only()
.column(collection::Column::SId)
.filter(collection::Column::SId.is_in(bili_sids))
.into_tuple()
.all(&db)
.await?;
let subscribed_set: HashSet<i64> = subscribed_ids.into_iter().collect();
// collection_list 中的条目可能是合集或者收藏夹,需要分类处理
// 目前看下来,最显著的区别是合集的 fid 是 0
let (bili_fids, bili_sids): (Vec<_>, Vec<_>) = collection_list.iter().partition_map(|col| {
if col.fid != 0 {
Either::Left(col.id)
} else {
Either::Right(col.id)
}
});
let (subscribed_fids, subscribed_sids): (HashSet<i64>, HashSet<i64>) = tokio::try_join!(
async {
Result::<_, anyhow::Error>::Ok(
favorite::Entity::find()
.select_only()
.column(favorite::Column::FId)
.filter(favorite::Column::FId.is_in(bili_fids))
.into_tuple()
.all(&db)
.await?
.into_iter()
.collect(),
)
},
async {
Ok(collection::Entity::find()
.select_only()
.column(collection::Column::SId)
.filter(collection::Column::SId.is_in(bili_sids))
.into_tuple()
.all(&db)
.await?
.into_iter()
.collect())
}
)?;
collection_list
.into_iter()
.map(|col| CollectionWithSubscriptionStatus {
title: col.title,
sid: col.id,
mid: col.mid,
invalid: col.state == 1,
subscribed: subscribed_set.contains(&col.id),
.map(|col| {
if col.fid != 0 {
Followed::Favorite {
title: col.title,
media_count: col.media_count,
fid: col.id,
mid: col.mid,
invalid: col.state == 1,
subscribed: subscribed_fids.contains(&col.id),
}
} else {
Followed::Collection {
title: col.title,
sid: col.id,
mid: col.mid,
media_count: col.media_count,
invalid: col.state == 1,
subscribed: subscribed_sids.contains(&col.id),
}
}
})
.collect()
} else {
@@ -132,7 +169,7 @@ pub async fn get_followed_uppers(
let uppers = bili_uppers
.list
.into_iter()
.map(|upper| UpperWithSubscriptionStatus {
.map(|upper| Followed::Upper {
mid: upper.mid,
// 官方没有提供字段,但是可以使用这种方式简单判断下
invalid: upper.uname == "账号已注销" && upper.face == "https://i0.hdslb.com/bfs/face/member/noface.jpg",

View File

@@ -100,9 +100,11 @@ pub struct FavoriteItem {
#[derive(Debug, serde::Deserialize)]
pub struct CollectionItem {
pub id: i64,
pub fid: i64,
pub mid: i64,
pub state: i32,
pub title: String,
pub media_count: i64,
}
#[derive(Debug, serde::Deserialize)]

View File

@@ -4,7 +4,7 @@
import BotIcon from '@lucide/svelte/icons/bot';
import ChartPieIcon from '@lucide/svelte/icons/chart-pie';
import HeartIcon from '@lucide/svelte/icons/heart';
import FolderIcon from '@lucide/svelte/icons/folder';
import FoldersIcon from '@lucide/svelte/icons/folders';
import UserIcon from '@lucide/svelte/icons/user';
import Settings2Icon from '@lucide/svelte/icons/settings-2';
import SquareTerminalIcon from '@lucide/svelte/icons/square-terminal';
@@ -62,12 +62,12 @@
href: '/me/favorites'
},
{
title: '我关注的合集',
icon: FolderIcon,
title: '我的合集 / 收藏夹',
icon: FoldersIcon,
href: '/me/collections'
},
{
title: '我关注的 up 主',
title: '我关注的 UP 主',
icon: UserIcon,
href: '/me/uppers'
}

View File

@@ -10,28 +10,20 @@
import CheckIcon from '@lucide/svelte/icons/check';
import PlusIcon from '@lucide/svelte/icons/plus';
import XIcon from '@lucide/svelte/icons/x';
import type {
FavoriteWithSubscriptionStatus,
CollectionWithSubscriptionStatus,
UpperWithSubscriptionStatus
} from '$lib/types';
import type { Followed } from '$lib/types';
export let item:
| FavoriteWithSubscriptionStatus
| CollectionWithSubscriptionStatus
| UpperWithSubscriptionStatus;
export let type: 'favorites' | 'collections' | 'submissions' = 'favorites';
export let item: Followed;
export let onSubscriptionSuccess: (() => void) | null = null;
let dialogOpen = false;
function getIcon() {
switch (type) {
case 'favorites':
switch (item.type) {
case 'favorite':
return HeartIcon;
case 'collections':
case 'collection':
return FolderIcon;
case 'submissions':
case 'upper':
return UserIcon;
default:
return VideoIcon;
@@ -39,12 +31,12 @@
}
function getTypeLabel() {
switch (type) {
case 'favorites':
switch (item.type) {
case 'favorite':
return '收藏夹';
case 'collections':
case 'collection':
return '合集';
case 'submissions':
case 'upper':
return 'UP 主';
default:
return '';
@@ -52,55 +44,52 @@
}
function getTitle(): string {
switch (type) {
case 'favorites':
return (item as FavoriteWithSubscriptionStatus).title;
case 'collections':
return (item as CollectionWithSubscriptionStatus).title;
case 'submissions':
return (item as UpperWithSubscriptionStatus).uname;
switch (item.type) {
case 'favorite':
case 'collection':
return item.title;
case 'upper':
return item.uname;
default:
return '';
}
}
function getSubtitle(): string {
switch (type) {
case 'favorites':
return `uid: ${(item as FavoriteWithSubscriptionStatus).mid}`;
case 'collections':
return `uid: ${(item as CollectionWithSubscriptionStatus).mid}`;
switch (item.type) {
case 'favorite':
case 'collection':
return `UID${item.mid}`;
default:
return '';
}
}
function getDescription(): string {
switch (type) {
case 'submissions':
return (item as UpperWithSubscriptionStatus).sign || '';
switch (item.type) {
case 'upper':
return item.sign || '';
default:
return '';
}
}
function isDisabled(): boolean {
switch (type) {
case 'collections':
return (item as CollectionWithSubscriptionStatus).invalid;
case 'submissions': {
return (item as UpperWithSubscriptionStatus).invalid;
}
switch (item.type) {
case 'collection':
case 'upper':
case 'favorite':
return item.invalid;
default:
return false;
}
}
function getDisabledReason(): string {
switch (type) {
case 'collections':
switch (item.type) {
case 'collection':
return '已失效';
case 'submissions':
case 'upper':
return '账号已注销';
default:
return '';
@@ -108,22 +97,19 @@
}
function getCount(): number | null {
switch (type) {
case 'favorites':
return (item as FavoriteWithSubscriptionStatus).media_count;
switch (item.type) {
case 'favorite':
case 'collection':
return item.media_count;
default:
return null;
}
}
function getCountLabel(): string {
return '个视频';
}
function getAvatarUrl(): string {
switch (type) {
case 'submissions':
return (item as UpperWithSubscriptionStatus).face;
switch (item.type) {
case 'upper':
return item.face;
default:
return '';
}
@@ -149,7 +135,6 @@
const subtitle = getSubtitle();
const description = getDescription();
const count = getCount();
const countLabel = getCountLabel();
const avatarUrl = getAvatarUrl();
const subscribed = item.subscribed;
const disabled = isDisabled();
@@ -161,7 +146,7 @@
? 'opacity-60'
: ''}"
>
<CardHeader class="flex-shrink-0 pb-4">
<CardHeader class="flex-shrink-0">
<div class="flex items-start gap-3">
<!-- 头像或图标 - 简化设计 -->
<div
@@ -169,7 +154,7 @@
? 'opacity-50'
: ''}"
>
{#if avatarUrl && type === 'submissions'}
{#if avatarUrl && item.type === 'upper'}
<img
src={avatarUrl}
alt={title}
@@ -197,7 +182,7 @@
{#if disabled}
<Badge variant="destructive" class="shrink-0 text-xs">不可用</Badge>
{:else}
<Badge variant={subscribed ? 'outline' : 'secondary'} class="shrink-0 text-xs">
<Badge variant="secondary" class="shrink-0 text-xs">
{subscribed ? '已订阅' : typeLabel}
</Badge>
{/if}
@@ -211,25 +196,26 @@
</div>
{/if}
<!-- 计数信息 -->
{#if count !== null && !disabled}
<div class="text-muted-foreground flex items-center gap-1 text-sm">
<VideoIcon class="h-3 w-3 shrink-0" />
<span class="truncate">视频数:{count}</span>
</div>
{/if}
<!-- 描述信息 -->
{#if description && !disabled}
<p class="text-muted-foreground line-clamp-1 text-sm" title={description}>
{description}
</p>
{/if}
<!-- 计数信息 -->
{#if count !== null && !disabled}
<div class="text-muted-foreground text-sm">
{count}
{countLabel}
</div>
{/if}
</div>
</div>
</CardHeader>
<!-- 底部按钮区域 -->
<CardContent class="flex min-w-0 flex-1 flex-col justify-end pt-0 pb-4">
<CardContent class="flex min-w-0 flex-1 flex-col justify-end">
<div class="flex justify-end">
{#if disabled}
<Button
@@ -262,4 +248,4 @@
</Card>
<!-- 订阅对话框 -->
<SubscriptionDialog bind:open={dialogOpen} {item} {type} onSuccess={handleSubscriptionSuccess} />
<SubscriptionDialog bind:open={dialogOpen} {item} onSuccess={handleSubscriptionSuccess} />

View File

@@ -13,9 +13,7 @@
} from '$lib/components/ui/sheet/index.js';
import api from '$lib/api';
import type {
FavoriteWithSubscriptionStatus,
CollectionWithSubscriptionStatus,
UpperWithSubscriptionStatus,
Followed,
InsertFavoriteRequest,
InsertCollectionRequest,
InsertSubmissionRequest,
@@ -24,38 +22,37 @@
interface Props {
open: boolean;
item:
| FavoriteWithSubscriptionStatus
| CollectionWithSubscriptionStatus
| UpperWithSubscriptionStatus
| null;
type: 'favorites' | 'collections' | 'submissions';
item: Followed | null;
onSuccess: (() => void) | null;
}
let {
open = $bindable(false),
item = null,
type = 'favorites',
onSuccess = null
}: Props = $props();
let { open = $bindable(false), item = null, onSuccess = null }: Props = $props();
let customPath = $state('');
let loading = $state(false);
// 根据类型和 item 生成默认路径
async function generateDefaultPath(): Promise<string> {
if (!itemTitle) return '';
return (await api.getDefaultPath(type, itemTitle)).data;
if (!item || !itemTitle) return '';
// 根据 item.type 映射到对应的 API 类型
const apiType =
item.type === 'favorite'
? 'favorites'
: item.type === 'collection'
? 'collections'
: 'submissions';
return (await api.getDefaultPath(apiType, itemTitle)).data;
}
function getTypeLabel(): string {
switch (type) {
case 'favorites':
if (!item) return '';
switch (item.type) {
case 'favorite':
return '收藏夹';
case 'collections':
case 'collection':
return '合集';
case 'submissions':
case 'upper':
return 'UP 主';
default:
return '';
@@ -65,13 +62,12 @@
function getItemTitle(): string {
if (!item) return '';
switch (type) {
case 'favorites':
return (item as FavoriteWithSubscriptionStatus).title;
case 'collections':
return (item as CollectionWithSubscriptionStatus).title;
case 'submissions':
return (item as UpperWithSubscriptionStatus).uname;
switch (item.type) {
case 'favorite':
case 'collection':
return item.title;
case 'upper':
return item.uname;
default:
return '';
}
@@ -84,30 +80,27 @@
try {
let response;
switch (type) {
case 'favorites': {
const favorite = item as FavoriteWithSubscriptionStatus;
switch (item.type) {
case 'favorite': {
const request: InsertFavoriteRequest = {
fid: favorite.fid,
fid: item.fid,
path: customPath.trim()
};
response = await api.insertFavorite(request);
break;
}
case 'collections': {
const collection = item as CollectionWithSubscriptionStatus;
case 'collection': {
const request: InsertCollectionRequest = {
sid: collection.sid,
mid: collection.mid,
sid: item.sid,
mid: item.mid,
path: customPath.trim()
};
response = await api.insertCollection(request);
break;
}
case 'submissions': {
const upper = item as UpperWithSubscriptionStatus;
case 'upper': {
const request: InsertSubmissionRequest = {
upper_id: upper.mid,
upper_id: item.mid,
path: customPath.trim()
};
response = await api.insertSubmission(request);
@@ -176,21 +169,16 @@
<span class="text-muted-foreground text-sm font-medium">{typeLabel}名称:</span>
<span class="text-sm">{itemTitle}</span>
</div>
{#if type === 'favorites'}
{@const favorite = item as FavoriteWithSubscriptionStatus}
{#if item!.type !== 'upper'}
<div class="flex items-center gap-2">
<span class="text-muted-foreground text-sm font-medium">视频数量:</span>
<span class="text-sm">{favorite.media_count} </span>
<span class="text-sm">{item!.media_count} </span>
</div>
{:else if item!.sign}
<div class="flex items-start gap-2">
<span class="text-muted-foreground text-sm font-medium">个人简介:</span>
<span class="text-muted-foreground text-sm">{item!.sign}</span>
</div>
{/if}
{#if type === 'submissions'}
{@const upper = item as UpperWithSubscriptionStatus}
{#if upper.sign}
<div class="flex items-start gap-2">
<span class="text-muted-foreground text-sm font-medium">个人简介:</span>
<span class="text-muted-foreground text-sm">{upper.sign}</span>
</div>
{/if}
{/if}
</div>
</div>

View File

@@ -110,45 +110,46 @@ export interface ResetRequest {
force: boolean;
}
// 收藏夹相关类型
export interface FavoriteWithSubscriptionStatus {
title: string;
media_count: number;
fid: number;
mid: number;
subscribed: boolean;
}
export type Followed =
| {
type: 'favorite';
title: string;
media_count: number;
fid: number;
mid: number;
invalid: boolean;
subscribed: boolean;
}
| {
type: 'collection';
title: string;
sid: number;
mid: number;
media_count: number;
invalid: boolean;
subscribed: boolean;
}
| {
type: 'upper';
mid: number;
uname: string;
face: string;
sign: string;
invalid: boolean;
subscribed: boolean;
};
export interface FavoritesResponse {
favorites: FavoriteWithSubscriptionStatus[];
}
// 合集相关类型
export interface CollectionWithSubscriptionStatus {
title: string;
sid: number;
mid: number;
invalid: boolean;
subscribed: boolean;
favorites: Followed[];
}
export interface CollectionsResponse {
collections: CollectionWithSubscriptionStatus[];
collections: Followed[];
total: number;
}
// UP 主相关类型
export interface UpperWithSubscriptionStatus {
mid: number;
uname: string;
face: string;
sign: string;
invalid: boolean;
subscribed: boolean;
}
export interface UppersResponse {
uppers: UpperWithSubscriptionStatus[];
uppers: Followed[];
total: number;
}

View File

@@ -1,5 +1,16 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import type { Followed } from './types';
export function getFollowedKey(followed: Followed): number {
if (followed.type == 'favorite') {
return followed.fid;
} else if (followed.type == 'collection') {
return followed.sid;
} else {
return followed.mid;
}
}
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));

View File

@@ -5,9 +5,10 @@
import Pagination from '$lib/components/pagination.svelte';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import api from '$lib/api';
import type { CollectionWithSubscriptionStatus, ApiError } from '$lib/types';
import type { Followed, ApiError } from '$lib/types';
import { getFollowedKey } from '$lib/utils';
let collections: CollectionWithSubscriptionStatus[] = [];
let collections: Followed[] = [];
let totalCount = 0;
let currentPage = 0;
let loading = false;
@@ -21,8 +22,8 @@
collections = response.data.collections;
totalCount = response.data.total;
} catch (error) {
console.error('加载合集失败:', error);
toast.error('加载合集失败', {
console.error('加载合集 / 收藏夹失败:', error);
toast.error('加载合集 / 收藏夹失败', {
description: (error as ApiError).message
});
} finally {
@@ -43,7 +44,7 @@
onMount(async () => {
setBreadcrumb([
{
label: '我关注的合集'
label: '我的合集 / 收藏夹'
}
]);
await loadCollections();
@@ -53,14 +54,19 @@
</script>
<svelte:head>
<title>关注的合集 - Bili Sync</title>
<title>我追的合集 / 收藏夹 - Bili Sync</title>
</svelte:head>
<div>
<div class="mb-6 flex items-center justify-between">
<div class="text-sm font-medium">
<div class="flex items-center gap-6">
{#if !loading}
{totalCount} 个合集
<div class=" text-sm font-medium">
{totalCount} 个合集 / 收藏夹
</div>
<div class=" text-sm font-medium">
当前第 {currentPage + 1} / {totalPages}
</div>
{/if}
</div>
</div>
@@ -73,13 +79,9 @@
<div
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
>
{#each collections as collection (collection.sid)}
{#each collections as collection (getFollowedKey(collection))}
<div style="max-width: 450px; width: 100%;">
<SubscriptionCard
item={collection}
type="collections"
onSubscriptionSuccess={handleSubscriptionSuccess}
/>
<SubscriptionCard item={collection} onSubscriptionSuccess={handleSubscriptionSuccess} />
</div>
{/each}
</div>
@@ -91,8 +93,10 @@
{:else}
<div class="flex items-center justify-center py-12">
<div class="space-y-2 text-center">
<p class="text-muted-foreground">暂无合集数据</p>
<p class="text-muted-foreground text-sm">请先在 B 站关注一些合集,或检查账号配置</p>
<p class="text-muted-foreground">暂无合集 / 收藏夹数据</p>
<p class="text-muted-foreground text-sm">
请先在 B 站关注一些合集 / 收藏夹,或检查账号配置
</p>
</div>
</div>
{/if}

View File

@@ -6,9 +6,10 @@
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import api from '$lib/api';
import type { FavoriteWithSubscriptionStatus, ApiError } from '$lib/types';
import type { Followed, ApiError } from '$lib/types';
import { getFollowedKey } from '$lib/utils';
let favorites: FavoriteWithSubscriptionStatus[] = [];
let favorites: Followed[] = [];
let loading = false;
async function loadFavorites() {
@@ -39,7 +40,7 @@
</script>
<svelte:head>
<title>我的收藏夹 - Bili Sync</title>
<title>创建的收藏夹 - Bili Sync</title>
</svelte:head>
<div>
@@ -59,13 +60,9 @@
<div
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
>
{#each favorites as favorite (favorite.fid)}
{#each favorites as favorite (getFollowedKey(favorite))}
<div style="max-width: 450px; width: 100%;">
<SubscriptionCard
item={favorite}
type="favorites"
onSubscriptionSuccess={handleSubscriptionSuccess}
/>
<SubscriptionCard item={favorite} onSubscriptionSuccess={handleSubscriptionSuccess} />
</div>
{/each}
</div>

View File

@@ -5,9 +5,9 @@
import Pagination from '$lib/components/pagination.svelte';
import { setBreadcrumb } from '$lib/stores/breadcrumb';
import api from '$lib/api';
import type { UpperWithSubscriptionStatus, ApiError } from '$lib/types';
import type { Followed, ApiError } from '$lib/types';
let uppers: UpperWithSubscriptionStatus[] = [];
let uppers: Followed[] = [];
let totalCount = 0;
let currentPage = 0;
let loading = false;
@@ -49,7 +49,7 @@
</script>
<svelte:head>
<title>关注的UP主 - Bili Sync</title>
<title>关注的 UP 主 - Bili Sync</title>
</svelte:head>
<div>
@@ -76,11 +76,7 @@
>
{#each uppers as upper (upper.mid)}
<div style="max-width: 450px; width: 100%;">
<SubscriptionCard
item={upper}
type="submissions"
onSubscriptionSuccess={handleSubscriptionSuccess}
/>
<SubscriptionCard item={upper} onSubscriptionSuccess={handleSubscriptionSuccess} />
</div>
{/each}
</div>