SnowLuma v2.4.2 source snapshot
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,267 @@
|
||||
import { useEffect, useId, useState } from 'react';
|
||||
import { normalizeUpdateSourceConfig } from '@snowluma/common/update-source';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { DropdownSelect } from '@/components/ui/dropdown-select';
|
||||
import { SkeletonSwap } from '@/components/interior/skeleton-swap';
|
||||
import { useActionFeedback } from '@/contexts/ActionFeedbackContext';
|
||||
import { useApi } from '@/lib/api';
|
||||
import type { UpdateSourceConfig, UpdateSourceProbe } from '@/types';
|
||||
|
||||
export function UpdateSources() {
|
||||
const api = useApi();
|
||||
const { runAction } = useActionFeedback();
|
||||
const id = useId();
|
||||
const [config, setConfig] = useState<UpdateSourceConfig | null>(null);
|
||||
const [mirrors, setMirrors] = useState('');
|
||||
const [results, setResults] = useState<UpdateSourceProbe[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
const [reload, setReload] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let disposed = false;
|
||||
setReady(false);
|
||||
void api.update
|
||||
.sources()
|
||||
.then(({ config: next }) => {
|
||||
if (!disposed) {
|
||||
setConfig(next);
|
||||
setMirrors(next.mirrors.join('\n'));
|
||||
setError(null);
|
||||
}
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!disposed) {
|
||||
setError(caught instanceof Error ? caught.message : String(caught));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!disposed) {
|
||||
setReady(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposed = true;
|
||||
};
|
||||
}, [api, reload]);
|
||||
|
||||
const act = async (testing: boolean) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await runAction(
|
||||
{
|
||||
title: testing ? '正在检测更新来源' : '正在保存更新来源',
|
||||
successTitle: testing ? '来源检测完成' : '更新来源已保存',
|
||||
successDetail: testing
|
||||
? '检测从 SnowLuma 所在服务器发起'
|
||||
: '下次检查和下载时生效',
|
||||
errorTitle: testing ? '来源检测失败' : '保存失败',
|
||||
},
|
||||
async () => {
|
||||
const next = normalizeUpdateSourceConfig({
|
||||
...config,
|
||||
mirrors: mirrors
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean),
|
||||
});
|
||||
|
||||
if (testing) {
|
||||
setResults((await api.update.testSources(next)).results);
|
||||
} else {
|
||||
const saved = await api.update.saveSources(next);
|
||||
|
||||
if (!saved.saved || !saved.applied) {
|
||||
throw new Error(saved.errors.join(';') || saved.message);
|
||||
}
|
||||
|
||||
setConfig(saved.config);
|
||||
setMirrors(saved.config.mirrors.join('\n'));
|
||||
setResults([]);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (caught) {
|
||||
setError(caught instanceof Error ? caught.message : String(caught));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 px-5 py-4">
|
||||
<p className="text-sm font-medium">更新来源</p>
|
||||
{error && (
|
||||
<p role="alert" className="break-words text-xs text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<SkeletonSwap ready={ready} reserve={100} lines={3} label="更新来源">
|
||||
{config ? (
|
||||
<fieldset
|
||||
disabled={busy}
|
||||
className="flex min-w-0 flex-col gap-3 disabled:opacity-60"
|
||||
>
|
||||
<DropdownSelect
|
||||
value={config.mode}
|
||||
ariaLabel="下载来源模式"
|
||||
options={[
|
||||
{ value: 'auto', label: '自动切换' },
|
||||
{ value: 'direct', label: 'GitHub 直连' },
|
||||
{ value: 'mirror', label: '指定镜像' },
|
||||
]}
|
||||
onChange={(mode) => {
|
||||
setConfig({ ...config, mode });
|
||||
setResults([]);
|
||||
}}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
自动模式先尝试
|
||||
GitHub,失败后按列表顺序切换。镜像用于文件下载,版本查询也会尝试 GitHub
|
||||
发布页面。
|
||||
</p>
|
||||
{config.mode === 'mirror' && (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor={`${id}-mirror`} className="text-xs">
|
||||
指定镜像地址
|
||||
</label>
|
||||
<Input
|
||||
id={`${id}-mirror`}
|
||||
value={config.mirror}
|
||||
placeholder="https://ghfast.top"
|
||||
onChange={(event) => {
|
||||
setConfig({ ...config, mirror: event.target.value });
|
||||
setResults([]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<details>
|
||||
<summary className="cursor-pointer text-xs">
|
||||
编辑候选镜像(每行一个地址)
|
||||
</summary>
|
||||
<Textarea
|
||||
aria-label="候选镜像地址"
|
||||
className="mt-2 min-h-32 font-mono text-xs"
|
||||
value={mirrors}
|
||||
onChange={(event) => {
|
||||
setMirrors(event.target.value);
|
||||
setResults([]);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
地址后会追加完整 GitHub 链接;支持带路径的代理前缀。
|
||||
</p>
|
||||
</details>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
void act(true);
|
||||
}}
|
||||
>
|
||||
检测可用性
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
void act(false);
|
||||
}}
|
||||
>
|
||||
保存来源
|
||||
</Button>
|
||||
</div>
|
||||
{results.length > 0 && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
按可用性和响应延迟排序;检测结果不代表下载速度。
|
||||
</p>
|
||||
<ul className="flex max-h-64 flex-col gap-2 overflow-auto">
|
||||
{results.map((result) => (
|
||||
<li
|
||||
key={result.mirror}
|
||||
className="flex items-center gap-2 text-xs"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="break-all">
|
||||
{result.mirror || 'GitHub 直连'}
|
||||
</p>
|
||||
<p
|
||||
className={
|
||||
result.available
|
||||
? 'text-success'
|
||||
: 'text-destructive'
|
||||
}
|
||||
>
|
||||
{result.available
|
||||
? `可用 · ${result.latencyMs} ms`
|
||||
: result.error || '不可用'}
|
||||
</p>
|
||||
</div>
|
||||
{result.available && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setConfig({
|
||||
...config,
|
||||
mode: result.mirror
|
||||
? 'mirror'
|
||||
: 'direct',
|
||||
mirror: result.mirror,
|
||||
});
|
||||
}}
|
||||
>
|
||||
选用
|
||||
</Button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
setMirrors(
|
||||
results
|
||||
.filter(
|
||||
(result) => result.available && result.mirror,
|
||||
)
|
||||
.map((result) => result.mirror)
|
||||
.join('\n'),
|
||||
);
|
||||
setConfig({ ...config, mode: 'auto' });
|
||||
setResults([]);
|
||||
}}
|
||||
>
|
||||
仅保留可用镜像
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
选用或筛选后,点击「保存来源」生效。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</fieldset>
|
||||
) : (
|
||||
ready && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => setReload((value) => value + 1)}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</SkeletonSwap>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user