mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-09 09:27:13 +08:00
fix: 正确处理“我追的合集 / 收藏夹”中的收藏夹条目,以及一些样式、文本调整 (#553)
This commit is contained in:
Generated
+11
-1
@@ -367,6 +367,7 @@ dependencies = [
|
|||||||
"git2",
|
"git2",
|
||||||
"handlebars",
|
"handlebars",
|
||||||
"hex",
|
"hex",
|
||||||
|
"itertools 0.14.0",
|
||||||
"leaky-bucket",
|
"leaky-bucket",
|
||||||
"md5",
|
"md5",
|
||||||
"memchr",
|
"memchr",
|
||||||
@@ -1785,6 +1786,15 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.11"
|
version = "1.0.11"
|
||||||
@@ -2468,7 +2478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
|
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools",
|
"itertools 0.12.1",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.96",
|
"syn 2.0.96",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ futures = "0.3.31"
|
|||||||
git2 = { version = "0.20.2", features = [], default-features = false }
|
git2 = { version = "0.20.2", features = [], default-features = false }
|
||||||
handlebars = "6.3.2"
|
handlebars = "6.3.2"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
|
itertools = "0.14.0"
|
||||||
leaky-bucket = "1.1.2"
|
leaky-bucket = "1.1.2"
|
||||||
md5 = "0.8.0"
|
md5 = "0.8.0"
|
||||||
memchr = "2.7.6"
|
memchr = "2.7.6"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ float-ord = { workspace = true }
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
handlebars = { workspace = true }
|
handlebars = { workspace = true }
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
leaky-bucket = { workspace = true }
|
leaky-bucket = { workspace = true }
|
||||||
md5 = { workspace = true }
|
md5 = { workspace = true }
|
||||||
memchr = { workspace = true }
|
memchr = { workspace = true }
|
||||||
|
|||||||
@@ -92,47 +92,48 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct FavoriteWithSubscriptionStatus {
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub title: String,
|
pub enum Followed {
|
||||||
pub media_count: i64,
|
Favorite {
|
||||||
pub fid: i64,
|
title: String,
|
||||||
pub mid: i64,
|
media_count: i64,
|
||||||
pub subscribed: bool,
|
fid: i64,
|
||||||
}
|
mid: i64,
|
||||||
|
invalid: bool,
|
||||||
#[derive(Serialize)]
|
subscribed: bool,
|
||||||
pub struct CollectionWithSubscriptionStatus {
|
},
|
||||||
pub title: String,
|
Collection {
|
||||||
pub sid: i64,
|
title: String,
|
||||||
pub mid: i64,
|
sid: i64,
|
||||||
pub invalid: bool,
|
mid: i64,
|
||||||
pub subscribed: bool,
|
media_count: i64,
|
||||||
}
|
invalid: bool,
|
||||||
|
subscribed: bool,
|
||||||
#[derive(Serialize)]
|
},
|
||||||
pub struct UpperWithSubscriptionStatus {
|
Upper {
|
||||||
pub mid: i64,
|
mid: i64,
|
||||||
pub uname: String,
|
uname: String,
|
||||||
pub face: String,
|
face: String,
|
||||||
pub sign: String,
|
sign: String,
|
||||||
pub invalid: bool,
|
invalid: bool,
|
||||||
pub subscribed: bool,
|
subscribed: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct FavoritesResponse {
|
pub struct FavoritesResponse {
|
||||||
pub favorites: Vec<FavoriteWithSubscriptionStatus>,
|
pub favorites: Vec<Followed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct CollectionsResponse {
|
pub struct CollectionsResponse {
|
||||||
pub collections: Vec<CollectionWithSubscriptionStatus>,
|
pub collections: Vec<Followed>,
|
||||||
pub total: i64,
|
pub total: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct UppersResponse {
|
pub struct UppersResponse {
|
||||||
pub uppers: Vec<UpperWithSubscriptionStatus>,
|
pub uppers: Vec<Followed>,
|
||||||
pub total: i64,
|
pub total: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ use axum::Router;
|
|||||||
use axum::extract::{Extension, Query};
|
use axum::extract::{Extension, Query};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use bili_sync_entity::*;
|
use bili_sync_entity::*;
|
||||||
|
use itertools::{Either, Itertools};
|
||||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect};
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect};
|
||||||
|
|
||||||
use crate::api::request::{FollowedCollectionsRequest, FollowedUppersRequest};
|
use crate::api::request::{FollowedCollectionsRequest, FollowedUppersRequest};
|
||||||
use crate::api::response::{
|
use crate::api::response::{CollectionsResponse, FavoritesResponse, Followed, UppersResponse};
|
||||||
CollectionWithSubscriptionStatus, CollectionsResponse, FavoriteWithSubscriptionStatus, FavoritesResponse,
|
|
||||||
UpperWithSubscriptionStatus, UppersResponse,
|
|
||||||
};
|
|
||||||
use crate::api::wrapper::{ApiError, ApiResponse};
|
use crate::api::wrapper::{ApiError, ApiResponse};
|
||||||
use crate::bilibili::{BiliClient, Me};
|
use crate::bilibili::{BiliClient, Me};
|
||||||
use crate::config::VersionedConfig;
|
use crate::config::VersionedConfig;
|
||||||
@@ -36,25 +34,26 @@ pub async fn get_created_favorites(
|
|||||||
let favorites = if let Some(bili_favorites) = bili_favorites {
|
let favorites = if let Some(bili_favorites) = bili_favorites {
|
||||||
// b 站收藏夹相关接口使用的所谓“fid”其实是该处的 id,即 fid + mid 后两位
|
// b 站收藏夹相关接口使用的所谓“fid”其实是该处的 id,即 fid + mid 后两位
|
||||||
let bili_fids: Vec<_> = bili_favorites.iter().map(|fav| fav.id).collect();
|
let bili_fids: Vec<_> = bili_favorites.iter().map(|fav| fav.id).collect();
|
||||||
|
let subscribed_fids: HashSet<i64> = favorite::Entity::find()
|
||||||
let subscribed_fids: Vec<i64> = favorite::Entity::find()
|
|
||||||
.select_only()
|
.select_only()
|
||||||
.column(favorite::Column::FId)
|
.column(favorite::Column::FId)
|
||||||
.filter(favorite::Column::FId.is_in(bili_fids))
|
.filter(favorite::Column::FId.is_in(bili_fids))
|
||||||
.into_tuple()
|
.into_tuple()
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?;
|
.await?
|
||||||
let subscribed_set: HashSet<i64> = subscribed_fids.into_iter().collect();
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
|
||||||
bili_favorites
|
bili_favorites
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|fav| FavoriteWithSubscriptionStatus {
|
.map(|fav| Followed::Favorite {
|
||||||
title: fav.title,
|
title: fav.title,
|
||||||
media_count: fav.media_count,
|
media_count: fav.media_count,
|
||||||
// api 返回的 id 才是真实的 fid
|
// api 返回的 id 才是真实的 fid
|
||||||
fid: fav.id,
|
fid: fav.id,
|
||||||
mid: fav.mid,
|
mid: fav.mid,
|
||||||
subscribed: subscribed_set.contains(&fav.id),
|
invalid: false,
|
||||||
|
subscribed: subscribed_fids.contains(&fav.id),
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -64,7 +63,7 @@ pub async fn get_created_favorites(
|
|||||||
Ok(ApiResponse::ok(FavoritesResponse { favorites }))
|
Ok(ApiResponse::ok(FavoritesResponse { favorites }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前用户收藏的合集
|
/// 获取当前用户收藏的合集/收藏夹
|
||||||
pub async fn get_followed_collections(
|
pub async fn get_followed_collections(
|
||||||
Extension(db): Extension<DatabaseConnection>,
|
Extension(db): Extension<DatabaseConnection>,
|
||||||
Extension(bili_client): Extension<Arc<BiliClient>>,
|
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 bili_collections = me.get_followed_collections(page_num, page_size).await?;
|
||||||
|
|
||||||
let collections = if let Some(collection_list) = bili_collections.list {
|
let collections = if let Some(collection_list) = bili_collections.list {
|
||||||
let bili_sids: Vec<_> = collection_list.iter().map(|col| col.id).collect();
|
// collection_list 中的条目可能是合集或者收藏夹,需要分类处理
|
||||||
|
// 目前看下来,最显著的区别是合集的 fid 是 0
|
||||||
let subscribed_ids: Vec<i64> = collection::Entity::find()
|
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()
|
.select_only()
|
||||||
.column(collection::Column::SId)
|
.column(collection::Column::SId)
|
||||||
.filter(collection::Column::SId.is_in(bili_sids))
|
.filter(collection::Column::SId.is_in(bili_sids))
|
||||||
.into_tuple()
|
.into_tuple()
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?;
|
.await?
|
||||||
let subscribed_set: HashSet<i64> = subscribed_ids.into_iter().collect();
|
.into_iter()
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
)?;
|
||||||
collection_list
|
collection_list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|col| CollectionWithSubscriptionStatus {
|
.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,
|
title: col.title,
|
||||||
sid: col.id,
|
sid: col.id,
|
||||||
mid: col.mid,
|
mid: col.mid,
|
||||||
|
media_count: col.media_count,
|
||||||
invalid: col.state == 1,
|
invalid: col.state == 1,
|
||||||
subscribed: subscribed_set.contains(&col.id),
|
subscribed: subscribed_sids.contains(&col.id),
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
@@ -132,7 +169,7 @@ pub async fn get_followed_uppers(
|
|||||||
let uppers = bili_uppers
|
let uppers = bili_uppers
|
||||||
.list
|
.list
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|upper| UpperWithSubscriptionStatus {
|
.map(|upper| Followed::Upper {
|
||||||
mid: upper.mid,
|
mid: upper.mid,
|
||||||
// 官方没有提供字段,但是可以使用这种方式简单判断下
|
// 官方没有提供字段,但是可以使用这种方式简单判断下
|
||||||
invalid: upper.uname == "账号已注销" && upper.face == "https://i0.hdslb.com/bfs/face/member/noface.jpg",
|
invalid: upper.uname == "账号已注销" && upper.face == "https://i0.hdslb.com/bfs/face/member/noface.jpg",
|
||||||
|
|||||||
@@ -100,9 +100,11 @@ pub struct FavoriteItem {
|
|||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
pub struct CollectionItem {
|
pub struct CollectionItem {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
|
pub fid: i64,
|
||||||
pub mid: i64,
|
pub mid: i64,
|
||||||
pub state: i32,
|
pub state: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub media_count: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import BotIcon from '@lucide/svelte/icons/bot';
|
import BotIcon from '@lucide/svelte/icons/bot';
|
||||||
import ChartPieIcon from '@lucide/svelte/icons/chart-pie';
|
import ChartPieIcon from '@lucide/svelte/icons/chart-pie';
|
||||||
import HeartIcon from '@lucide/svelte/icons/heart';
|
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 UserIcon from '@lucide/svelte/icons/user';
|
||||||
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
import Settings2Icon from '@lucide/svelte/icons/settings-2';
|
||||||
import SquareTerminalIcon from '@lucide/svelte/icons/square-terminal';
|
import SquareTerminalIcon from '@lucide/svelte/icons/square-terminal';
|
||||||
@@ -62,12 +62,12 @@
|
|||||||
href: '/me/favorites'
|
href: '/me/favorites'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '我关注的合集',
|
title: '我追的合集 / 收藏夹',
|
||||||
icon: FolderIcon,
|
icon: FoldersIcon,
|
||||||
href: '/me/collections'
|
href: '/me/collections'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '我关注的 up 主',
|
title: '我关注的 UP 主',
|
||||||
icon: UserIcon,
|
icon: UserIcon,
|
||||||
href: '/me/uppers'
|
href: '/me/uppers'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,28 +10,20 @@
|
|||||||
import CheckIcon from '@lucide/svelte/icons/check';
|
import CheckIcon from '@lucide/svelte/icons/check';
|
||||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||||
import XIcon from '@lucide/svelte/icons/x';
|
import XIcon from '@lucide/svelte/icons/x';
|
||||||
import type {
|
import type { Followed } from '$lib/types';
|
||||||
FavoriteWithSubscriptionStatus,
|
|
||||||
CollectionWithSubscriptionStatus,
|
|
||||||
UpperWithSubscriptionStatus
|
|
||||||
} from '$lib/types';
|
|
||||||
|
|
||||||
export let item:
|
export let item: Followed;
|
||||||
| FavoriteWithSubscriptionStatus
|
|
||||||
| CollectionWithSubscriptionStatus
|
|
||||||
| UpperWithSubscriptionStatus;
|
|
||||||
export let type: 'favorites' | 'collections' | 'submissions' = 'favorites';
|
|
||||||
export let onSubscriptionSuccess: (() => void) | null = null;
|
export let onSubscriptionSuccess: (() => void) | null = null;
|
||||||
|
|
||||||
let dialogOpen = false;
|
let dialogOpen = false;
|
||||||
|
|
||||||
function getIcon() {
|
function getIcon() {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites':
|
case 'favorite':
|
||||||
return HeartIcon;
|
return HeartIcon;
|
||||||
case 'collections':
|
case 'collection':
|
||||||
return FolderIcon;
|
return FolderIcon;
|
||||||
case 'submissions':
|
case 'upper':
|
||||||
return UserIcon;
|
return UserIcon;
|
||||||
default:
|
default:
|
||||||
return VideoIcon;
|
return VideoIcon;
|
||||||
@@ -39,12 +31,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTypeLabel() {
|
function getTypeLabel() {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites':
|
case 'favorite':
|
||||||
return '收藏夹';
|
return '收藏夹';
|
||||||
case 'collections':
|
case 'collection':
|
||||||
return '合集';
|
return '合集';
|
||||||
case 'submissions':
|
case 'upper':
|
||||||
return 'UP 主';
|
return 'UP 主';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
@@ -52,55 +44,52 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTitle(): string {
|
function getTitle(): string {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites':
|
case 'favorite':
|
||||||
return (item as FavoriteWithSubscriptionStatus).title;
|
case 'collection':
|
||||||
case 'collections':
|
return item.title;
|
||||||
return (item as CollectionWithSubscriptionStatus).title;
|
case 'upper':
|
||||||
case 'submissions':
|
return item.uname;
|
||||||
return (item as UpperWithSubscriptionStatus).uname;
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubtitle(): string {
|
function getSubtitle(): string {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites':
|
case 'favorite':
|
||||||
return `uid: ${(item as FavoriteWithSubscriptionStatus).mid}`;
|
case 'collection':
|
||||||
case 'collections':
|
return `UID:${item.mid}`;
|
||||||
return `uid: ${(item as CollectionWithSubscriptionStatus).mid}`;
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDescription(): string {
|
function getDescription(): string {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'submissions':
|
case 'upper':
|
||||||
return (item as UpperWithSubscriptionStatus).sign || '';
|
return item.sign || '';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDisabled(): boolean {
|
function isDisabled(): boolean {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'collections':
|
case 'collection':
|
||||||
return (item as CollectionWithSubscriptionStatus).invalid;
|
case 'upper':
|
||||||
case 'submissions': {
|
case 'favorite':
|
||||||
return (item as UpperWithSubscriptionStatus).invalid;
|
return item.invalid;
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisabledReason(): string {
|
function getDisabledReason(): string {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'collections':
|
case 'collection':
|
||||||
return '已失效';
|
return '已失效';
|
||||||
case 'submissions':
|
case 'upper':
|
||||||
return '账号已注销';
|
return '账号已注销';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
@@ -108,22 +97,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCount(): number | null {
|
function getCount(): number | null {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites':
|
case 'favorite':
|
||||||
return (item as FavoriteWithSubscriptionStatus).media_count;
|
case 'collection':
|
||||||
|
return item.media_count;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCountLabel(): string {
|
|
||||||
return '个视频';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAvatarUrl(): string {
|
function getAvatarUrl(): string {
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'submissions':
|
case 'upper':
|
||||||
return (item as UpperWithSubscriptionStatus).face;
|
return item.face;
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -149,7 +135,6 @@
|
|||||||
const subtitle = getSubtitle();
|
const subtitle = getSubtitle();
|
||||||
const description = getDescription();
|
const description = getDescription();
|
||||||
const count = getCount();
|
const count = getCount();
|
||||||
const countLabel = getCountLabel();
|
|
||||||
const avatarUrl = getAvatarUrl();
|
const avatarUrl = getAvatarUrl();
|
||||||
const subscribed = item.subscribed;
|
const subscribed = item.subscribed;
|
||||||
const disabled = isDisabled();
|
const disabled = isDisabled();
|
||||||
@@ -161,7 +146,7 @@
|
|||||||
? 'opacity-60'
|
? 'opacity-60'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<CardHeader class="flex-shrink-0 pb-4">
|
<CardHeader class="flex-shrink-0">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<!-- 头像或图标 - 简化设计 -->
|
<!-- 头像或图标 - 简化设计 -->
|
||||||
<div
|
<div
|
||||||
@@ -169,7 +154,7 @@
|
|||||||
? 'opacity-50'
|
? 'opacity-50'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
{#if avatarUrl && type === 'submissions'}
|
{#if avatarUrl && item.type === 'upper'}
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={title}
|
alt={title}
|
||||||
@@ -197,7 +182,7 @@
|
|||||||
{#if disabled}
|
{#if disabled}
|
||||||
<Badge variant="destructive" class="shrink-0 text-xs">不可用</Badge>
|
<Badge variant="destructive" class="shrink-0 text-xs">不可用</Badge>
|
||||||
{:else}
|
{:else}
|
||||||
<Badge variant={subscribed ? 'outline' : 'secondary'} class="shrink-0 text-xs">
|
<Badge variant="secondary" class="shrink-0 text-xs">
|
||||||
{subscribed ? '已订阅' : typeLabel}
|
{subscribed ? '已订阅' : typeLabel}
|
||||||
</Badge>
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -211,25 +196,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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}
|
{#if description && !disabled}
|
||||||
<p class="text-muted-foreground line-clamp-1 text-sm" title={description}>
|
<p class="text-muted-foreground line-clamp-1 text-sm" title={description}>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- 计数信息 -->
|
|
||||||
{#if count !== null && !disabled}
|
|
||||||
<div class="text-muted-foreground text-sm">
|
|
||||||
{count}
|
|
||||||
{countLabel}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</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">
|
<div class="flex justify-end">
|
||||||
{#if disabled}
|
{#if disabled}
|
||||||
<Button
|
<Button
|
||||||
@@ -262,4 +248,4 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- 订阅对话框 -->
|
<!-- 订阅对话框 -->
|
||||||
<SubscriptionDialog bind:open={dialogOpen} {item} {type} onSuccess={handleSubscriptionSuccess} />
|
<SubscriptionDialog bind:open={dialogOpen} {item} onSuccess={handleSubscriptionSuccess} />
|
||||||
|
|||||||
@@ -13,9 +13,7 @@
|
|||||||
} from '$lib/components/ui/sheet/index.js';
|
} from '$lib/components/ui/sheet/index.js';
|
||||||
import api from '$lib/api';
|
import api from '$lib/api';
|
||||||
import type {
|
import type {
|
||||||
FavoriteWithSubscriptionStatus,
|
Followed,
|
||||||
CollectionWithSubscriptionStatus,
|
|
||||||
UpperWithSubscriptionStatus,
|
|
||||||
InsertFavoriteRequest,
|
InsertFavoriteRequest,
|
||||||
InsertCollectionRequest,
|
InsertCollectionRequest,
|
||||||
InsertSubmissionRequest,
|
InsertSubmissionRequest,
|
||||||
@@ -24,38 +22,37 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
item:
|
item: Followed | null;
|
||||||
| FavoriteWithSubscriptionStatus
|
|
||||||
| CollectionWithSubscriptionStatus
|
|
||||||
| UpperWithSubscriptionStatus
|
|
||||||
| null;
|
|
||||||
type: 'favorites' | 'collections' | 'submissions';
|
|
||||||
onSuccess: (() => void) | null;
|
onSuccess: (() => void) | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { open = $bindable(false), item = null, onSuccess = null }: Props = $props();
|
||||||
open = $bindable(false),
|
|
||||||
item = null,
|
|
||||||
type = 'favorites',
|
|
||||||
onSuccess = null
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let customPath = $state('');
|
let customPath = $state('');
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
// 根据类型和 item 生成默认路径
|
// 根据类型和 item 生成默认路径
|
||||||
async function generateDefaultPath(): Promise<string> {
|
async function generateDefaultPath(): Promise<string> {
|
||||||
if (!itemTitle) return '';
|
if (!item || !itemTitle) return '';
|
||||||
return (await api.getDefaultPath(type, itemTitle)).data;
|
// 根据 item.type 映射到对应的 API 类型
|
||||||
|
const apiType =
|
||||||
|
item.type === 'favorite'
|
||||||
|
? 'favorites'
|
||||||
|
: item.type === 'collection'
|
||||||
|
? 'collections'
|
||||||
|
: 'submissions';
|
||||||
|
return (await api.getDefaultPath(apiType, itemTitle)).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeLabel(): string {
|
function getTypeLabel(): string {
|
||||||
switch (type) {
|
if (!item) return '';
|
||||||
case 'favorites':
|
|
||||||
|
switch (item.type) {
|
||||||
|
case 'favorite':
|
||||||
return '收藏夹';
|
return '收藏夹';
|
||||||
case 'collections':
|
case 'collection':
|
||||||
return '合集';
|
return '合集';
|
||||||
case 'submissions':
|
case 'upper':
|
||||||
return 'UP 主';
|
return 'UP 主';
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
@@ -65,13 +62,12 @@
|
|||||||
function getItemTitle(): string {
|
function getItemTitle(): string {
|
||||||
if (!item) return '';
|
if (!item) return '';
|
||||||
|
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites':
|
case 'favorite':
|
||||||
return (item as FavoriteWithSubscriptionStatus).title;
|
case 'collection':
|
||||||
case 'collections':
|
return item.title;
|
||||||
return (item as CollectionWithSubscriptionStatus).title;
|
case 'upper':
|
||||||
case 'submissions':
|
return item.uname;
|
||||||
return (item as UpperWithSubscriptionStatus).uname;
|
|
||||||
default:
|
default:
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -84,30 +80,27 @@
|
|||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
switch (type) {
|
switch (item.type) {
|
||||||
case 'favorites': {
|
case 'favorite': {
|
||||||
const favorite = item as FavoriteWithSubscriptionStatus;
|
|
||||||
const request: InsertFavoriteRequest = {
|
const request: InsertFavoriteRequest = {
|
||||||
fid: favorite.fid,
|
fid: item.fid,
|
||||||
path: customPath.trim()
|
path: customPath.trim()
|
||||||
};
|
};
|
||||||
response = await api.insertFavorite(request);
|
response = await api.insertFavorite(request);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'collections': {
|
case 'collection': {
|
||||||
const collection = item as CollectionWithSubscriptionStatus;
|
|
||||||
const request: InsertCollectionRequest = {
|
const request: InsertCollectionRequest = {
|
||||||
sid: collection.sid,
|
sid: item.sid,
|
||||||
mid: collection.mid,
|
mid: item.mid,
|
||||||
path: customPath.trim()
|
path: customPath.trim()
|
||||||
};
|
};
|
||||||
response = await api.insertCollection(request);
|
response = await api.insertCollection(request);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'submissions': {
|
case 'upper': {
|
||||||
const upper = item as UpperWithSubscriptionStatus;
|
|
||||||
const request: InsertSubmissionRequest = {
|
const request: InsertSubmissionRequest = {
|
||||||
upper_id: upper.mid,
|
upper_id: item.mid,
|
||||||
path: customPath.trim()
|
path: customPath.trim()
|
||||||
};
|
};
|
||||||
response = await api.insertSubmission(request);
|
response = await api.insertSubmission(request);
|
||||||
@@ -176,22 +169,17 @@
|
|||||||
<span class="text-muted-foreground text-sm font-medium">{typeLabel}名称:</span>
|
<span class="text-muted-foreground text-sm font-medium">{typeLabel}名称:</span>
|
||||||
<span class="text-sm">{itemTitle}</span>
|
<span class="text-sm">{itemTitle}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if type === 'favorites'}
|
{#if item!.type !== 'upper'}
|
||||||
{@const favorite = item as FavoriteWithSubscriptionStatus}
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-muted-foreground text-sm font-medium">视频数量:</span>
|
<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>
|
</div>
|
||||||
{/if}
|
{:else if item!.sign}
|
||||||
{#if type === 'submissions'}
|
|
||||||
{@const upper = item as UpperWithSubscriptionStatus}
|
|
||||||
{#if upper.sign}
|
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-muted-foreground text-sm font-medium">个人简介:</span>
|
<span class="text-muted-foreground text-sm font-medium">个人简介:</span>
|
||||||
<span class="text-muted-foreground text-sm">{upper.sign}</span>
|
<span class="text-muted-foreground text-sm">{item!.sign}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+21
-20
@@ -110,45 +110,46 @@ export interface ResetRequest {
|
|||||||
force: boolean;
|
force: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收藏夹相关类型
|
export type Followed =
|
||||||
export interface FavoriteWithSubscriptionStatus {
|
| {
|
||||||
|
type: 'favorite';
|
||||||
title: string;
|
title: string;
|
||||||
media_count: number;
|
media_count: number;
|
||||||
fid: number;
|
fid: number;
|
||||||
mid: number;
|
mid: number;
|
||||||
|
invalid: boolean;
|
||||||
subscribed: boolean;
|
subscribed: boolean;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
export interface FavoritesResponse {
|
type: 'collection';
|
||||||
favorites: FavoriteWithSubscriptionStatus[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合集相关类型
|
|
||||||
export interface CollectionWithSubscriptionStatus {
|
|
||||||
title: string;
|
title: string;
|
||||||
sid: number;
|
sid: number;
|
||||||
mid: number;
|
mid: number;
|
||||||
|
media_count: number;
|
||||||
invalid: boolean;
|
invalid: boolean;
|
||||||
subscribed: boolean;
|
subscribed: boolean;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
export interface CollectionsResponse {
|
type: 'upper';
|
||||||
collections: CollectionWithSubscriptionStatus[];
|
|
||||||
total: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UP 主相关类型
|
|
||||||
export interface UpperWithSubscriptionStatus {
|
|
||||||
mid: number;
|
mid: number;
|
||||||
uname: string;
|
uname: string;
|
||||||
face: string;
|
face: string;
|
||||||
sign: string;
|
sign: string;
|
||||||
invalid: boolean;
|
invalid: boolean;
|
||||||
subscribed: boolean;
|
subscribed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FavoritesResponse {
|
||||||
|
favorites: Followed[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectionsResponse {
|
||||||
|
collections: Followed[];
|
||||||
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UppersResponse {
|
export interface UppersResponse {
|
||||||
uppers: UpperWithSubscriptionStatus[];
|
uppers: Followed[];
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx';
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
import { twMerge } from 'tailwind-merge';
|
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[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
import Pagination from '$lib/components/pagination.svelte';
|
import Pagination from '$lib/components/pagination.svelte';
|
||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||||
import api from '$lib/api';
|
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 totalCount = 0;
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@@ -21,8 +22,8 @@
|
|||||||
collections = response.data.collections;
|
collections = response.data.collections;
|
||||||
totalCount = response.data.total;
|
totalCount = response.data.total;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载合集失败:', error);
|
console.error('加载合集 / 收藏夹失败:', error);
|
||||||
toast.error('加载合集失败', {
|
toast.error('加载合集 / 收藏夹失败', {
|
||||||
description: (error as ApiError).message
|
description: (error as ApiError).message
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
setBreadcrumb([
|
setBreadcrumb([
|
||||||
{
|
{
|
||||||
label: '我关注的合集'
|
label: '我追的合集 / 收藏夹'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
await loadCollections();
|
await loadCollections();
|
||||||
@@ -53,14 +54,19 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>关注的合集 - Bili Sync</title>
|
<title>我追的合集 / 收藏夹 - Bili Sync</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<div class="text-sm font-medium">
|
<div class="flex items-center gap-6">
|
||||||
{#if !loading}
|
{#if !loading}
|
||||||
共 {totalCount} 个合集
|
<div class=" text-sm font-medium">
|
||||||
|
共 {totalCount} 个合集 / 收藏夹
|
||||||
|
</div>
|
||||||
|
<div class=" text-sm font-medium">
|
||||||
|
当前第 {currentPage + 1} / {totalPages} 页
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,13 +79,9 @@
|
|||||||
<div
|
<div
|
||||||
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
|
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%;">
|
<div style="max-width: 450px; width: 100%;">
|
||||||
<SubscriptionCard
|
<SubscriptionCard item={collection} onSubscriptionSuccess={handleSubscriptionSuccess} />
|
||||||
item={collection}
|
|
||||||
type="collections"
|
|
||||||
onSubscriptionSuccess={handleSubscriptionSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -91,8 +93,10 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="flex items-center justify-center py-12">
|
<div class="flex items-center justify-center py-12">
|
||||||
<div class="space-y-2 text-center">
|
<div class="space-y-2 text-center">
|
||||||
<p class="text-muted-foreground">暂无合集数据</p>
|
<p class="text-muted-foreground">暂无合集 / 收藏夹数据</p>
|
||||||
<p class="text-muted-foreground text-sm">请先在 B 站关注一些合集,或检查账号配置</p>
|
<p class="text-muted-foreground text-sm">
|
||||||
|
请先在 B 站关注一些合集 / 收藏夹,或检查账号配置
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||||
|
|
||||||
import api from '$lib/api';
|
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;
|
let loading = false;
|
||||||
|
|
||||||
async function loadFavorites() {
|
async function loadFavorites() {
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>我的收藏夹 - Bili Sync</title>
|
<title>我创建的收藏夹 - Bili Sync</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -59,13 +60,9 @@
|
|||||||
<div
|
<div
|
||||||
style="display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; width: 100%; max-width: none; justify-items: start;"
|
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%;">
|
<div style="max-width: 450px; width: 100%;">
|
||||||
<SubscriptionCard
|
<SubscriptionCard item={favorite} onSubscriptionSuccess={handleSubscriptionSuccess} />
|
||||||
item={favorite}
|
|
||||||
type="favorites"
|
|
||||||
onSubscriptionSuccess={handleSubscriptionSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
import Pagination from '$lib/components/pagination.svelte';
|
import Pagination from '$lib/components/pagination.svelte';
|
||||||
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
import { setBreadcrumb } from '$lib/stores/breadcrumb';
|
||||||
import api from '$lib/api';
|
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 totalCount = 0;
|
||||||
let currentPage = 0;
|
let currentPage = 0;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>关注的UP主 - Bili Sync</title>
|
<title>我关注的 UP 主 - Bili Sync</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -76,11 +76,7 @@
|
|||||||
>
|
>
|
||||||
{#each uppers as upper (upper.mid)}
|
{#each uppers as upper (upper.mid)}
|
||||||
<div style="max-width: 450px; width: 100%;">
|
<div style="max-width: 450px; width: 100%;">
|
||||||
<SubscriptionCard
|
<SubscriptionCard item={upper} onSubscriptionSuccess={handleSubscriptionSuccess} />
|
||||||
item={upper}
|
|
||||||
type="submissions"
|
|
||||||
onSubscriptionSuccess={handleSubscriptionSuccess}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user