✨ 添加收藏和下载队列
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
51
src/features/collection/CollectionDetail.tsx
Normal file
51
src/features/collection/CollectionDetail.tsx
Normal 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;
|
||||
37
src/features/collection/Collections.tsx
Normal file
37
src/features/collection/Collections.tsx
Normal 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;
|
||||
95
src/features/collection/store.ts
Normal file
95
src/features/collection/store.ts
Normal 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();
|
||||
35
src/features/downloads/Downloads.tsx
Normal file
35
src/features/downloads/Downloads.tsx
Normal 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;
|
||||
75
src/features/downloads/store.ts
Normal file
75
src/features/downloads/store.ts
Normal 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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user