From 4d4073a7eb237e3ec6cd978ae46854f1b53ee859 Mon Sep 17 00:00:00 2001 From: Gaubee Date: Tue, 3 Feb 2026 15:56:24 +0800 Subject: [PATCH] feat(sources): improve source management with edit mode and auto-fetch name - Add edit mode for existing sources (click to edit) - Auto-fetch source name from URL when adding/editing - Show fetched name as placeholder in name field - Trigger refresh immediately after add/save - Add updateSourceName action to ecosystem store - Add nameWithDefault translation key for all locales Co-Authored-By: Claude Opus 4.5 --- src/i18n/locales/ar/common.json | 1 + src/i18n/locales/en/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + .../activities/SettingsSourcesActivity.tsx | 169 ++++++++++++++---- src/stores/ecosystem.ts | 8 + 6 files changed, 148 insertions(+), 33 deletions(-) diff --git a/src/i18n/locales/ar/common.json b/src/i18n/locales/ar/common.json index f9a240246..0471e715b 100644 --- a/src/i18n/locales/ar/common.json +++ b/src/i18n/locales/ar/common.json @@ -489,6 +489,7 @@ "noSources": "لا توجد مصادر", "urlPlaceholder": "عنوان URL للاشتراك (https://...)", "namePlaceholder": "الاسم (اختياري)", + "nameWithDefault": "الاسم ({{name}})", "addSource": "إضافة مصدر", "official": "رسمي", "updatedAt": "تم التحديث {{date}}", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 24ba710e9..a714b8f79 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -489,6 +489,7 @@ "noSources": "No sources", "urlPlaceholder": "Subscription URL (https://...)", "namePlaceholder": "Name (optional)", + "nameWithDefault": "Name ({{name}})", "addSource": "Add source", "official": "Official", "updatedAt": "Updated {{date}}", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 7b3fe96b0..6a533ecfd 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -489,6 +489,7 @@ "noSources": "暂无订阅源", "urlPlaceholder": "订阅 URL (https://...)", "namePlaceholder": "名称 (可选)", + "nameWithDefault": "名称 ({{name}})", "addSource": "添加订阅源", "official": "官方", "updatedAt": "更新于 {{date}}", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 493469974..41243eacc 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -489,6 +489,7 @@ "noSources": "暫無訂閱源", "urlPlaceholder": "訂閱 URL (https://...)", "namePlaceholder": "名稱 (可選)", + "nameWithDefault": "名稱 ({{name}})", "addSource": "添加訂閱源", "official": "官方", "updatedAt": "更新於 {{date}}", diff --git a/src/stackflow/activities/SettingsSourcesActivity.tsx b/src/stackflow/activities/SettingsSourcesActivity.tsx index 7d7bdf584..1d5a2264b 100644 --- a/src/stackflow/activities/SettingsSourcesActivity.tsx +++ b/src/stackflow/activities/SettingsSourcesActivity.tsx @@ -3,7 +3,7 @@ * 管理小程序订阅源 */ -import { useState } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import type { ActivityComponentType } from '@stackflow/react'; import { AppScreen } from '@stackflow/plugin-basic-ui'; import { useTranslation } from 'react-i18next'; @@ -19,6 +19,7 @@ import { IconArrowLeft, IconLoader2, IconAlertCircle, + IconEdit, } from '@tabler/icons-react'; import { ecosystemStore, ecosystemActions, type SourceRecord } from '@/stores/ecosystem'; import { refreshSources, refreshSource } from '@/services/ecosystem/registry'; @@ -29,13 +30,83 @@ export const SettingsSourcesActivity: ActivityComponentType = () => { const { pop } = useFlow(); const state = useStore(ecosystemStore); - const [isAdding, setIsAdding] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editingUrl, setEditingUrl] = useState(null); // null = adding new, string = editing existing const [newUrl, setNewUrl] = useState(''); const [newName, setNewName] = useState(''); + const [fetchedName, setFetchedName] = useState(null); + const [isFetchingName, setIsFetchingName] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); - const handleAdd = async () => { + // 当 URL 变化时,尝试获取源的名称 + const fetchSourceName = useCallback(async (url: string) => { + setIsFetchingName(true); + setFetchedName(null); + try { + const normalizedUrl = new URL(url).toString(); + const response = await fetch(normalizedUrl); + if (response.ok) { + const data = await response.json(); + if (data.name && typeof data.name === 'string') { + setFetchedName(data.name); + } + } + } catch { + // 忽略错误,用户可以手动输入名称 + } finally { + setIsFetchingName(false); + } + }, []); + + // URL 输入防抖获取名称 + useEffect(() => { + if (!newUrl.trim()) { + setFetchedName(null); + return; + } + + try { + new URL(newUrl); + } catch { + return; // URL 无效,不获取 + } + + const timer = setTimeout(() => { + fetchSourceName(newUrl); + }, 500); + + return () => clearTimeout(timer); + }, [newUrl, fetchSourceName]); + + const handleStartAdd = () => { + setIsEditing(true); + setEditingUrl(null); + setNewUrl(''); + setNewName(''); + setFetchedName(null); + setError(null); + }; + + const handleStartEdit = (source: SourceRecord) => { + setIsEditing(true); + setEditingUrl(source.url); + setNewUrl(source.url); + setNewName(source.name); + setFetchedName(null); + setError(null); + }; + + const handleCancel = () => { + setIsEditing(false); + setEditingUrl(null); + setNewUrl(''); + setNewName(''); + setFetchedName(null); + setError(null); + }; + + const handleSave = async () => { if (!newUrl.trim()) { setError(t('sources.enterUrl')); return; @@ -50,17 +121,34 @@ export const SettingsSourcesActivity: ActivityComponentType = () => { return; } - // 检查是否已存在 - if (state.sources.some((s) => s.url === normalizedUrl)) { - setError(t('sources.alreadyExists')); - return; + const finalName = newName.trim() || fetchedName || t('sources.customSource'); + + if (editingUrl) { + // 编辑模式 + if (editingUrl !== normalizedUrl) { + // URL 变了,检查是否已存在 + if (state.sources.some((s) => s.url === normalizedUrl)) { + setError(t('sources.alreadyExists')); + return; + } + // 删除旧的,添加新的 + ecosystemActions.removeSource(editingUrl); + ecosystemActions.addSource(normalizedUrl, finalName); + } else { + // URL 没变,只更新名称 + ecosystemActions.updateSourceName(editingUrl, finalName); + } + } else { + // 添加模式 + if (state.sources.some((s) => s.url === normalizedUrl)) { + setError(t('sources.alreadyExists')); + return; + } + ecosystemActions.addSource(normalizedUrl, finalName); } - ecosystemActions.addSource(normalizedUrl, newName || t('sources.customSource')); - setNewUrl(''); - setNewName(''); - setIsAdding(false); - setError(null); + handleCancel(); + // 立即触发刷新 void refreshSource(normalizedUrl); }; @@ -80,6 +168,11 @@ export const SettingsSourcesActivity: ActivityComponentType = () => { setIsRefreshing(false); }; + const isAddMode = editingUrl === null; + const namePlaceholder = fetchedName + ? t('sources.nameWithDefault', { name: fetchedName }) + : t('sources.namePlaceholder'); + return (
@@ -113,6 +206,8 @@ export const SettingsSourcesActivity: ActivityComponentType = () => { source={source} onToggle={() => handleToggle(source.url)} onRemove={() => handleRemove(source.url)} + onEdit={() => handleStartEdit(source)} + isSelected={editingUrl === source.url} /> ))} @@ -121,8 +216,8 @@ export const SettingsSourcesActivity: ActivityComponentType = () => { )}
- {/* Add Source */} - {isAdding ? ( + {/* Add/Edit Source */} + {isEditing ? (
{ }} placeholder={t('sources.urlPlaceholder')} className="bg-background focus:ring-primary w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none" + disabled={editingUrl !== null && editingUrl === newUrl} /> - setNewName(e.target.value)} - placeholder={t('sources.namePlaceholder')} - className="bg-background focus:ring-primary w-full rounded-lg border px-3 py-2 focus:ring-2 focus:outline-none" - /> +
+ setNewName(e.target.value)} + placeholder={namePlaceholder} + className="bg-background focus:ring-primary w-full rounded-lg border px-3 py-2 pr-8 focus:ring-2 focus:outline-none" + /> + {isFetchingName && ( + + )} +
{error &&

{error}

}
) : (