添加收藏和下载队列

This commit is contained in:
柚子
2025-02-06 16:43:09 +08:00
parent b2f458f3b8
commit 56fa6a8a2d
29 changed files with 1284 additions and 19 deletions

View File

@@ -1,17 +1,49 @@
import { Component } from 'solid-js';
import { Component, createSignal } from 'solid-js';
import { useParams } from '@solidjs/router';
import { useAppDetailStore } from './store';
import './AppDetail.css';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import ScreenshotCarousel from '@/components/ScreenshotCarousel';
import { generateShareLinks, copy } from '@/lib/share';
import { showToast } from "@/components/ui/toast"
import { showToast } from "@/components/ui/toast";
import { useCollectionStore } from '@/features/collection/store';
import { useDownloadsStore } from '@/features/downloads/store';
import { Progress } from '@/components/ui/progress';
import { X } from 'lucide-solid';
const AppDetail: Component = () => {
const params = useParams();
const { app, loading } = useAppDetailStore(params.category, params.pkgname);
const { collections, addCollection, addAppToCollections, removeAppFromCollection, isAppInCollection } = useCollectionStore();
const { downloads, addDownload, cancelDownload } = useDownloadsStore();
const [newCollectionName, setNewCollectionName] = createSignal('');
const [newCollectionDesc, setNewCollectionDesc] = createSignal('');
const [showNewCollectionDialog, setShowNewCollectionDialog] = createSignal(false);
const [showCollectionDialog, setShowCollectionDialog] = createSignal(false);
const [selectedCollections, setSelectedCollections] = createSignal<string[]>([]);
const handleCreateCollection = () => {
if (!newCollectionName()) {
showToast({ description: '请输入收藏单名称', variant: "warning" });
return;
}
const newCollection = addCollection({
name: newCollectionName(),
description: newCollectionDesc(),
});
// 直接将当前应用添加到新创建的收藏单中
addAppToCollections([newCollection.id], params.category, params.pkgname);
setNewCollectionName('');
setNewCollectionDesc('');
setShowNewCollectionDialog(false);
showToast({ description: '收藏单创建成功', variant: "success" });
};
return (
<div class="w-full h-full">
@@ -26,9 +58,10 @@ const AppDetail: Component = () => {
<div class="py-2" />
<Skeleton height={32} width={200} />
</div>
<div class="flex flex-col gap-2">
<Skeleton height={40} width={96} radius={6} />
<Skeleton height={40} width={96} radius={6} />
<div class="flex flex-col gap-2 w-[120px]">
<Skeleton height={40} width={120} radius={6} />
<Skeleton height={40} width={120} radius={6} />
<Skeleton height={40} width={120} radius={6} />
</div>
</div>
<div class="mt-2">
@@ -76,11 +109,96 @@ const AppDetail: Component = () => {
<div class="py-2" />
<h2 class="app-name text-2xl font-bold pt-2">{app()?.Name}</h2>
</div>
<div class="flex flex-col gap-2">
<Button size="lg"></Button>
<div class="flex flex-col gap-2 w-[120px]">
{downloads().some(task => task.category === params.category && task.pkgname === params.pkgname) ? (
<div class="flex flex-col gap-2 w-full pb-2">
{downloads().map(download => {
if (download.category === params.category && download.pkgname === params.pkgname) {
return (
<div class="flex items-center gap-2 group relative">
<Progress value={download.progress} class="flex-1" />
<div class="flex items-center gap-2 group-hover:opacity-0 transition-opacity">
<span class="text-sm text-muted-foreground">{download.progress}%</span>
{download.speed && (
<span class="text-sm text-muted-foreground">{download.speed}</span>
)}
</div>
<button
class="absolute right-1 bg-red-500 rounded-full w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center text-white hover:bg-red-600"
onClick={() => {
cancelDownload(params.category, params.pkgname);
}}
>
<X />
</button>
</div>
);
}
return null;
})}
</div>
) : (
<Button
size="lg"
class='w-full'
onClick={() => {
const currentApp = app();
if (!currentApp) {
showToast({ description: '获取应用信息失败', variant: "error" });
return;
}
if (!currentApp.Filename) {
showToast({ description: '获取应用下载信息失败', variant: "error" });
return;
}
addDownload(params.category, params.pkgname, currentApp.Filename, currentApp.Name || '');
showToast({ description: '已添加到下载队列', variant: "success" });
}}
>
</Button>
)}
<Dialog open={showCollectionDialog()} onOpenChange={setShowCollectionDialog}>
<DialogTrigger>
<Button variant="outline" size="lg" class='w-full'></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div class="flex flex-col gap-4">
{collections().map(collection => (
<div class="flex items-center space-x-2">
<Checkbox
id={`collection-${collection.id}`}
checked={isAppInCollection(collection.id, params.category, params.pkgname)}
onChange={(checked) => {
if (checked) {
addAppToCollections([collection.id], params.category, params.pkgname);
} else {
removeAppFromCollection(collection.id, params.category, params.pkgname);
}
}}
/>
<Label for={`collection-${collection.id}-input`}>{collection.name}</Label>
</div>
))}
<Button variant="outline" onClick={() => setShowNewCollectionDialog(true)}></Button>
<Button onClick={() => {
const selected = selectedCollections();
addAppToCollections(selected, params.category, params.pkgname);
showToast({ description: '添加到收藏单成功', variant: "success" });
setSelectedCollections([]);
setShowCollectionDialog(false);
}}></Button>
</div>
</DialogContent>
</Dialog>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="outline" size="lg"></Button>
<Button variant="outline" size="lg" class='w-full'></Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel></DropdownMenuLabel>
@@ -145,6 +263,32 @@ const AppDetail: Component = () => {
</div>
</div>
)}
<Dialog open={showNewCollectionDialog()} onOpenChange={setShowNewCollectionDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<Label></Label>
<Input
value={newCollectionName()}
onInput={(e) => setNewCollectionName(e.currentTarget.value)}
placeholder="请输入收藏单名称"
/>
</div>
<div class="flex flex-col gap-2">
<Label></Label>
<Input
value={newCollectionDesc()}
onInput={(e) => setNewCollectionDesc(e.currentTarget.value)}
placeholder="请输入收藏单描述(选填)"
/>
</div>
<Button onClick={handleCreateCollection}></Button>
</div>
</DialogContent>
</Dialog>
</div>
);
};

View File

@@ -0,0 +1,51 @@
import { Component, createEffect, createSignal } from 'solid-js';
import { useParams } from '@solidjs/router';
import { useCollectionStore } from './store';
import AppList from '@/components/AppList';
import { AppItem } from '@/types/app';
import { fetchCategoryApps } from '@/features/categories/store';
const CollectionDetail: Component = () => {
const params = useParams();
const { collections } = useCollectionStore();
const [apps, setApps] = createSignal<AppItem[]>([]);
const [loading, setLoading] = createSignal(true);
createEffect(async () => {
if (params.id) {
setLoading(true);
const collection = collections().find(c => c.id === params.id);
if (collection) {
try {
const appPromises = collection.apps.map(async (app) => {
const categoryApps = await fetchCategoryApps(app.Category);
return categoryApps.find(categoryApp => categoryApp.pkgname === app.Pkgname);
});
const appResults = await Promise.all(appPromises);
setApps(appResults.filter((app): app is AppItem => app !== undefined));
} catch (error) {
console.error('Failed to load collection apps:', error);
} finally {
setLoading(false);
}
}
}
});
return (
<div class="p-6 w-full h-full">
<h1 class="text-2xl font-bold">
{collections()?.find(c => c.id === params.id)?.name}
</h1>
<p class="text-gray-500 mb-6">
{collections()?.find(c => c.id === params.id)?.description}
</p>
<AppList
apps={apps()}
loading={loading()}
/>
</div>
);
};
export default CollectionDetail;

View File

@@ -0,0 +1,37 @@
import { Component } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { useCollectionStore } from './store';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
const Collections: Component = () => {
const navigate = useNavigate();
const { collections } = useCollectionStore();
return (
<div class="p-6 w-full h-full">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{collections().map((collection) => (
<Card
class="cursor-pointer hover:bg-accent/50 transition-colors"
onClick={() => navigate(`/collectionDetail/${collection.id}`)}
>
<CardHeader class='p-4'>
<CardTitle>{collection.name}</CardTitle>
<CardDescription>{collection.description}</CardDescription>
</CardHeader>
<CardContent class='px-4'>
<p class="text-sm text-muted-foreground">
{collection.apps.length}
</p>
<p class="text-sm text-muted-foreground">
{new Date(collection.updatedAt).toLocaleDateString()}
</p>
</CardContent>
</Card>
))}
</div>
</div>
);
};
export default Collections;

View File

@@ -0,0 +1,95 @@
import { createSignal, createEffect } from 'solid-js';
import { Collection, CreateCollectionParams } from '@/types/collection';
import { saveTextFile, readTextFile } from '@/lib/api/file';
const COLLECTIONS_FILE = 'collections.json';
const [collections, setCollections] = createSignal<Collection[]>([]);
const [initialized, setInitialized] = createSignal(false);
async function initCollections() {
try {
const data = await readTextFile(COLLECTIONS_FILE);
const parsedData = JSON.parse(data);
if (Array.isArray(parsedData)) {
setCollections(parsedData);
}
} catch (error) {
console.error('Failed to load collections:', error);
// 如果文件不存在,使用空数组,但不会覆盖已有文件
} finally {
setInitialized(true);
}
}
createEffect(() => {
// 只有在初始化完成后才开始自动保存
if (initialized()) {
const currentCollections = collections();
saveTextFile(COLLECTIONS_FILE, JSON.stringify(currentCollections))
.catch(error => console.error('Failed to save collections:', error));
}
});
export const useCollectionStore = () => {
const addCollection = (params: CreateCollectionParams): Collection => {
const newCollection: Collection = {
id: Date.now().toString(),
name: params.name,
description: params.description,
apps: [],
createdAt: Date.now(),
updatedAt: Date.now(),
};
setCollections([...collections(), newCollection]);
return newCollection;
};
const addAppToCollections = (collectionIds: string[], Category: string, Pkgname: string) => {
setCollections(prev => prev.map(collection => {
if (collectionIds.includes(collection.id)) {
const appExists = collection.apps.some(app =>
app.Category === Category && app.Pkgname === Pkgname
);
if (!appExists) {
return {
...collection,
apps: [...collection.apps, { Category, Pkgname }],
updatedAt: Date.now(),
};
}
}
return collection;
}));
};
const removeAppFromCollection = (collectionId: string, Category: string, Pkgname: string) => {
setCollections(prev => prev.map(collection => {
if (collection.id === collectionId) {
return {
...collection,
apps: collection.apps.filter(app =>
!(app.Category === Category && app.Pkgname === Pkgname)
),
updatedAt: Date.now(),
};
}
return collection;
}));
};
const isAppInCollection = (collectionId: string, Category: string, Pkgname: string): boolean => {
const collection = collections().find(c => c.id === collectionId);
return collection?.apps.some(app => app.Category === Category && app.Pkgname === Pkgname) || false;
};
return {
collections,
addCollection,
addAppToCollections,
removeAppFromCollection,
isAppInCollection,
};
};
// 初始化收藏单数据
initCollections();

View File

@@ -0,0 +1,35 @@
import { Component, For } from 'solid-js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import DownloadCard from '../../components/DownloadCard';
import { useDownloadsStore } from './store';
const Downloads: Component = () => {
const { activeDownloads, completedDownloads } = useDownloadsStore();
return (
<div class="p-6 w-full h-full">
<Tabs defaultValue="active" class="w-full">
<TabsList>
<TabsTrigger value="active"> ({activeDownloads().length})</TabsTrigger>
<TabsTrigger value="completed"> ({completedDownloads().length})</TabsTrigger>
</TabsList>
<TabsContent value="active" class="mt-6">
<div class="flex flex-col gap-4">
<For each={activeDownloads()}>
{(download) => <DownloadCard download={download} />}
</For>
</div>
</TabsContent>
<TabsContent value="completed" class="mt-6">
<div class="flex flex-col gap-4">
<For each={completedDownloads()}>
{(download) => <DownloadCard download={download} />}
</For>
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default Downloads;

View File

@@ -0,0 +1,75 @@
import { createSignal, createEffect } from 'solid-js';
import { DownloadTask } from '@/types/download';
import { getDownloads, addDownload as addDownloadApi, pauseDownload as pauseDownloadApi, resumeDownload as resumeDownloadApi, cancelDownload as cancelDownloadApi } from '@/lib/api/download';
const [downloads, setDownloads] = createSignal<DownloadTask[]>([]);
// 初始化时获取下载列表
createEffect(async () => {
try {
const downloadList = await getDownloads();
setDownloads(downloadList);
} catch (error) {
console.error('获取下载列表失败:', error);
}
});
export const useDownloadsStore = () => {
const activeDownloads = () => downloads().filter(item =>
['downloading', 'paused', 'queued'].includes(item.status)
);
const completedDownloads = () => downloads().filter(item =>
item.status === 'completed'
);
const addDownload = async (category: string, pkgname: string, filename: string, name: string) => {
try {
await addDownloadApi(category, pkgname, filename, name);
const updatedList = await getDownloads();
setDownloads(updatedList);
} catch (error) {
console.error('添加下载任务失败:', error);
}
};
const pauseDownload = async (category: string, pkgname: string) => {
try {
await pauseDownloadApi(category, pkgname);
const updatedList = await getDownloads();
setDownloads(updatedList);
} catch (error) {
console.error('暂停下载任务失败:', error);
}
};
const resumeDownload = async (category: string, pkgname: string) => {
try {
await resumeDownloadApi(category, pkgname);
const updatedList = await getDownloads();
setDownloads(updatedList);
} catch (error) {
console.error('继续下载任务失败:', error);
}
};
const cancelDownload = async (category: string, pkgname: string) => {
try {
await cancelDownloadApi(category, pkgname);
const updatedList = await getDownloads();
setDownloads(updatedList);
} catch (error) {
console.error('取消下载任务失败:', error);
}
};
return {
downloads,
activeDownloads,
completedDownloads,
addDownload,
pauseDownload,
resumeDownload,
cancelDownload
};
};