feat: profile start pages

This commit is contained in:
zhom committed 2026-10-12 00:52:24 +04:00
1 parent 3f07a41fac
commit 4044c845c7
25 files changed
+755 -54

No files matched your search

+2
View File
@@ -1155,6 +1155,7 @@ export default function Home() {
ephemeral?: boolean;
dnsBlocklist?: string;
dnsServer?: string;
startUrls?: string[];
launchHook?: string;
password?: string;
}) => {
@@ -1178,6 +1179,7 @@ export default function Home() {
dnsBlocklist: profileData.dnsBlocklist,
dnsServer: profileData.dnsServer,
launchHook: profileData.launchHook,
startUrls: profileData.startUrls,
},
);
+28
View File
@@ -61,6 +61,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
@@ -74,6 +75,7 @@ import { useVpnEvents } from "@/hooks/use-vpn-events";
import { getOSDisplayName, getOSIcon } from "@/lib/browser-utils";
import { DNS_BLOCKLIST_LEVELS } from "@/lib/dns-blocklist-levels";
import { MOTION_EASE_OUT } from "@/lib/motion";
import { startPagesFromText } from "@/lib/start-pages";
import { cn } from "@/lib/utils";
import type { BrowserReleaseTypes, WayfernConfig, WayfernOS } from "@/types";
@@ -113,6 +115,7 @@ interface CreateProfileDialogProps {
ephemeral?: boolean;
dnsBlocklist?: string;
dnsServer?: string;
startUrls?: string[];
launchHook?: string;
password?: string;
}) => Promise<void>;
@@ -146,6 +149,7 @@ export function CreateProfileDialog({
const [dnsBlocklist, setDnsBlocklist] = useState("");
const [dnsServer, setDnsServer] = useState("");
const [dnsServerValid, setDnsServerValid] = useState(true);
const [startPages, setStartPages] = useState("");
const [launchHook, setLaunchHook] = useState("");
const [wayfernConfig, setWayfernConfig] =
useState<WayfernConfig>(defaultWayfernConfig);
@@ -306,6 +310,7 @@ export function CreateProfileDialog({
setShowProxyForm(false);
setDnsBlocklist("");
setDnsServer("");
setStartPages("");
setLaunchHook("");
setSelectedExtensionGroupId(undefined);
setReleaseTypes({});
@@ -351,6 +356,7 @@ export function CreateProfileDialog({
ephemeral,
dnsBlocklist: dnsBlocklist || undefined,
dnsServer: dnsServer.trim() || undefined,
startUrls: startPagesFromText(startPages),
launchHook: launchHook.trim() || undefined,
password: enablePassword && !ephemeral ? password : undefined,
});
@@ -1006,6 +1012,28 @@ export function CreateProfileDialog({
disabled={isCreating}
/>
</div>
<div className="space-y-1.5">
<Label
htmlFor="profile-start-pages"
className={FIELD_LABEL_CLASS}
>
{t("profileInfo.sections.startPages")}
</Label>
<Textarea
id="profile-start-pages"
variant="soft"
value={startPages}
onChange={(event) => setStartPages(event.target.value)}
placeholder={t("startPages.placeholder")}
rows={3}
spellCheck={false}
disabled={isCreating}
className="font-mono text-xs"
/>
<p className="text-xs text-muted-foreground">
{t("startPages.description")}
</p>
</div>
<div className="space-y-4 pt-1">
{creatableVersion && (
<p className="text-xs text-muted-foreground">
+99
View File
@@ -23,6 +23,7 @@ import {
LuFingerprint,
LuGlobe,
LuGroup,
LuHouse,
LuKey,
LuLink,
LuLock,
@@ -84,6 +85,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { LaunchArgumentsEditor } from "@/components/wayfern-config-form";
import { WindowDragArea } from "@/components/window-drag-area";
import { useInputModality } from "@/hooks/use-input-modality";
@@ -94,6 +96,7 @@ import { formatRelativeTime } from "@/lib/flag-utils";
import { MOTION_EASE_OUT } from "@/lib/motion";
import { rotateProxyIps } from "@/lib/proxy-rotation";
import type { RemoteHandoffState } from "@/lib/remote-sessions";
import { startPagesFromText } from "@/lib/start-pages";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import { cn } from "@/lib/utils";
import type {
@@ -961,6 +964,7 @@ type ProfileSection =
| "overview"
| "fingerprint"
| "network"
| "startPages"
| "cookies"
| "extensions"
| "sync"
@@ -1081,6 +1085,14 @@ function ProfileInfoLayout({
label: t("profileInfo.sections.network"),
badge: profile.proxy_id || profile.vpn_id ? networkLabel : undefined,
},
{
id: "startPages",
icon: <LuHouse className="size-3.5" />,
label: t("profileInfo.sections.startPages"),
badge: profile.start_urls?.length
? String(profile.start_urls.length)
: undefined,
},
{
id: "cookies",
icon: <LuCookie className="size-3.5" />,
@@ -1393,6 +1405,10 @@ function ProfileInfoLayout({
/>
)}
{section === "startPages" && (
<StartPagesEditor profile={profile} isDisabled={isDisabled} t={t} />
)}
{section === "cookies" &&
(isEphemeral ? (
<EphemeralSectionNotice
@@ -1647,6 +1663,89 @@ function LaunchHookEditor({
);
}
function StartPagesEditor({
profile,
isDisabled,
t,
}: {
profile: BrowserProfile;
isDisabled: boolean;
t: (key: string, options?: Record<string, unknown>) => string;
}) {
const { t: tFn } = useTranslation();
const initial = (profile.start_urls ?? []).join("\n");
const [value, setValue] = React.useState(initial);
const [isSaving, setIsSaving] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const dirty = value.trim() !== initial;
React.useEffect(() => {
setValue(initial);
}, [initial]);
const onSave = async () => {
setIsSaving(true);
setError(null);
try {
const saved = await invoke<BrowserProfile>("update_profile_start_urls", {
profileId: profile.id,
startUrls: startPagesFromText(value),
});
setValue((saved.start_urls ?? []).join("\n"));
} catch (e) {
setError(translateBackendError(tFn, e));
} finally {
setIsSaving(false);
}
};
return (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2 text-sm font-semibold">
<LuHouse className="size-4" />
{t("profileInfo.sections.startPages")}
</div>
<p className="text-xs text-muted-foreground">
{t("startPages.description")}
</p>
<Textarea
data-slot="profile-start-pages-input"
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={t("startPages.placeholder")}
rows={5}
spellCheck={false}
disabled={isDisabled || isSaving}
className="font-mono text-xs"
/>
{error && <p className="text-xs text-destructive-text">{error}</p>}
<div className="flex items-center gap-2">
<Button
size="sm"
className="h-7 text-xs"
disabled={!dirty || isSaving || isDisabled}
onClick={() => void onSave()}
>
{isSaving ? t("common.buttons.saving") : t("common.buttons.save")}
</Button>
{dirty && (
<Button
size="sm"
variant="ghost"
className="h-7 text-xs"
onClick={() => {
setValue(initial);
setError(null);
}}
>
{t("common.buttons.cancel")}
</Button>
)}
</div>
</div>
);
}
function SyncSectionInline({
profile,
syncMode,
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "Profil löschen",
"activity": "Aktivität",
"launchHook": "Start-Hook",
"transfer": "Übertragung"
"transfer": "Übertragung",
"startPages": "Startseiten"
},
"sectionDesc": {
"fingerprint": "Legen Sie fest, wie dieses Profil für Fingerprinting-Skripte erscheint.",
@@ -1754,6 +1755,10 @@
"deleteTitle": "„{{name}}“ löschen?",
"deleteDescription": "Profile mit diesem Status haben danach keinen Status. Dies lässt sich nicht rückgängig machen."
},
"startPages": {
"description": "Seiten, die jeder Start aus Donut öffnet, eine pro Zeile, statt der letzten Sitzung. Ein Lauf mit eigener URL öffnet nur diese URL.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "VPN bearbeiten",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "Dieser Proxy hat keinen Link zum IP-Wechsel. Fügen Sie einen beim Bearbeiten des Proxys hinzu.",
"proxyRotationFailed": "Der Anbieter hat den IP-Wechsel abgelehnt (HTTP {{status}}).",
"proxyRotationTimeout": "Der Anbieter hat innerhalb von 30 Sekunden nicht auf den IP-Wechsel geantwortet.",
"proxyRotationUnreachable": "Donut konnte den Link zum IP-Wechsel des Anbieters nicht erreichen."
"proxyRotationUnreachable": "Donut konnte den Link zum IP-Wechsel des Anbieters nicht erreichen.",
"startUrlInvalid": "„{{url}}“ ist keine Webseite. Startseiten müssen http://- oder https://-Adressen sein.",
"startUrlsTooMany": "Ein Profil kann höchstens 20 Startseiten haben."
},
"rail": {
"profiles": "Profile",
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "Delete profile",
"activity": "Activity",
"launchHook": "Launch hook",
"transfer": "Transfer"
"transfer": "Transfer",
"startPages": "Start pages"
},
"sectionDesc": {
"fingerprint": "Configure how this profile appears to fingerprinting scripts.",
@@ -1754,6 +1755,10 @@
"deleteTitle": "Delete \"{{name}}\"?",
"deleteDescription": "Profiles with this status will have no status. You cannot undo this."
},
"startPages": {
"description": "Pages that every launch from Donut opens, one per line, instead of the last session. A run with its own URL opens only that URL.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "Edit VPN",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "This proxy has no change IP link. Add one when you edit the proxy.",
"proxyRotationFailed": "The provider refused the IP change (HTTP {{status}}).",
"proxyRotationTimeout": "The provider did not answer the IP change within 30 seconds.",
"proxyRotationUnreachable": "Donut could not reach the provider's change IP link."
"proxyRotationUnreachable": "Donut could not reach the provider's change IP link.",
"startUrlInvalid": "\"{{url}}\" is not a web page. Start pages must be http:// or https:// addresses.",
"startUrlsTooMany": "A profile can have at most 20 start pages."
},
"rail": {
"profiles": "Profiles",
+9 -2
View File
@@ -1518,7 +1518,8 @@
"delete": "Eliminar perfil",
"activity": "Actividad",
"launchHook": "Hook de inicio",
"transfer": "Transferir"
"transfer": "Transferir",
"startPages": "Páginas de inicio"
},
"sectionDesc": {
"fingerprint": "Configura cómo aparece este perfil para los scripts de fingerprinting.",
@@ -1761,6 +1762,10 @@
"deleteTitle": "¿Eliminar «{{name}}»?",
"deleteDescription": "Los perfiles con este estado quedarán sin estado. No se puede deshacer."
},
"startPages": {
"description": "Páginas que abre cada inicio desde Donut, una por línea, en lugar de la última sesión. Una ejecución con su propia URL abre solo esa URL.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "Editar VPN",
@@ -2498,7 +2503,9 @@
"proxyRotationUrlMissing": "Este proxy no tiene enlace para cambiar IP. Añade uno al editar el proxy.",
"proxyRotationFailed": "El proveedor rechazó el cambio de IP (HTTP {{status}}).",
"proxyRotationTimeout": "El proveedor no respondió al cambio de IP en 30 segundos.",
"proxyRotationUnreachable": "Donut no pudo acceder al enlace para cambiar IP del proveedor."
"proxyRotationUnreachable": "Donut no pudo acceder al enlace para cambiar IP del proveedor.",
"startUrlInvalid": "«{{url}}» no es una página web. Las páginas de inicio deben ser direcciones http:// o https://.",
"startUrlsTooMany": "Un perfil puede tener como máximo 20 páginas de inicio."
},
"rail": {
"profiles": "Perfiles",
+9 -2
View File
@@ -1518,7 +1518,8 @@
"delete": "Supprimer le profil",
"activity": "Activité",
"launchHook": "Hook de lancement",
"transfer": "Transfert"
"transfer": "Transfert",
"startPages": "Pages de démarrage"
},
"sectionDesc": {
"fingerprint": "Configurez l’apparence de ce profil pour les scripts de fingerprinting.",
@@ -1761,6 +1762,10 @@
"deleteTitle": "Supprimer « {{name}} » ?",
"deleteDescription": "Les profils ayant ce statut n'auront plus de statut. Cette action est irréversible."
},
"startPages": {
"description": "Pages que chaque lancement depuis Donut ouvre, une par ligne, à la place de la dernière session. Une exécution avec sa propre URL n'ouvre que cette URL.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "Modifier le VPN",
@@ -2498,7 +2503,9 @@
"proxyRotationUrlMissing": "Ce proxy n'a pas de lien de changement d'IP. Ajoutez-en un en modifiant le proxy.",
"proxyRotationFailed": "Le fournisseur a refusé le changement d'IP (HTTP {{status}}).",
"proxyRotationTimeout": "Le fournisseur n'a pas répondu au changement d'IP en 30 secondes.",
"proxyRotationUnreachable": "Donut n'a pas pu joindre le lien de changement d'IP du fournisseur."
"proxyRotationUnreachable": "Donut n'a pas pu joindre le lien de changement d'IP du fournisseur.",
"startUrlInvalid": "« {{url}} » n'est pas une page web. Les pages de démarrage doivent être des adresses http:// ou https://.",
"startUrlsTooMany": "Un profil peut avoir au plus 20 pages de démarrage."
},
"rail": {
"profiles": "Profils",
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "プロファイルを削除",
"activity": "アクティビティ",
"launchHook": "起動フック",
"transfer": "転送"
"transfer": "転送",
"startPages": "スタートページ"
},
"sectionDesc": {
"fingerprint": "フィンガープリント対策スクリプトに対するこのプロファイルの表示を設定します。",
@@ -1754,6 +1755,10 @@
"deleteTitle": "「{{name}}」を削除しますか?",
"deleteDescription": "このステータスのプロファイルはステータスなしになります。元に戻すことはできません。"
},
"startPages": {
"description": "Donut から起動するたびに前回のセッションの代わりに開くページです。1 行に 1 つ入力します。独自の URL を指定した実行では、その URL だけが開きます。",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "VPN を編集",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "このプロキシには IP 変更リンクがありません。プロキシの編集時に追加してください。",
"proxyRotationFailed": "プロバイダーが IP 変更を拒否しました (HTTP {{status}})。",
"proxyRotationTimeout": "プロバイダーが 30 秒以内に IP 変更に応答しませんでした。",
"proxyRotationUnreachable": "プロバイダーの IP 変更リンクに接続できませんでした。"
"proxyRotationUnreachable": "プロバイダーの IP 変更リンクに接続できませんでした。",
"startUrlInvalid": "「{{url}}」はウェブページではありません。スタートページは http:// または https:// のアドレスにしてください。",
"startUrlsTooMany": "1 つのプロファイルに設定できるスタートページは 20 件までです。"
},
"rail": {
"profiles": "プロファイル",
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "프로필 삭제",
"activity": "활동",
"launchHook": "실행 후크",
"transfer": "전송"
"transfer": "전송",
"startPages": "시작 페이지"
},
"sectionDesc": {
"fingerprint": "이 프로필이 핑거프린팅 스크립트에 어떻게 표시되는지 구성합니다.",
@@ -1754,6 +1755,10 @@
"deleteTitle": "\"{{name}}\"을(를) 삭제할까요?",
"deleteDescription": "이 상태의 프로필은 상태가 없어집니다. 되돌릴 수 없습니다."
},
"startPages": {
"description": "Donut에서 실행할 때마다 마지막 세션 대신 여는 페이지입니다. 한 줄에 하나씩 입력하세요. 자체 URL이 있는 실행은 그 URL만 엽니다.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "VPN 편집",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "이 프록시에는 IP 변경 링크가 없습니다. 프록시를 편집할 때 추가하세요.",
"proxyRotationFailed": "공급자가 IP 변경을 거부했습니다 (HTTP {{status}}).",
"proxyRotationTimeout": "공급자가 30초 안에 IP 변경에 응답하지 않았습니다.",
"proxyRotationUnreachable": "공급자의 IP 변경 링크에 연결할 수 없습니다."
"proxyRotationUnreachable": "공급자의 IP 변경 링크에 연결할 수 없습니다.",
"startUrlInvalid": "\"{{url}}\"은(는) 웹 페이지가 아닙니다. 시작 페이지는 http:// 또는 https:// 주소여야 합니다.",
"startUrlsTooMany": "프로필 하나에 시작 페이지는 최대 20개까지 가능합니다."
},
"rail": {
"profiles": "프로필",
+9 -2
View File
@@ -1518,7 +1518,8 @@
"delete": "Excluir perfil",
"activity": "Atividade",
"launchHook": "Hook de inicialização",
"transfer": "Transferência"
"transfer": "Transferência",
"startPages": "Páginas iniciais"
},
"sectionDesc": {
"fingerprint": "Configure como este perfil aparece para scripts de fingerprinting.",
@@ -1761,6 +1762,10 @@
"deleteTitle": "Excluir \"{{name}}\"?",
"deleteDescription": "Os perfis com este status ficarão sem status. Não é possível desfazer."
},
"startPages": {
"description": "Páginas que cada inicialização pelo Donut abre, uma por linha, em vez da última sessão. Uma execução com URL própria abre só essa URL.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "Editar VPN",
@@ -2498,7 +2503,9 @@
"proxyRotationUrlMissing": "Este proxy não tem link de troca de IP. Adicione um ao editar o proxy.",
"proxyRotationFailed": "O provedor recusou a troca de IP (HTTP {{status}}).",
"proxyRotationTimeout": "O provedor não respondeu à troca de IP em 30 segundos.",
"proxyRotationUnreachable": "O Donut não conseguiu acessar o link de troca de IP do provedor."
"proxyRotationUnreachable": "O Donut não conseguiu acessar o link de troca de IP do provedor.",
"startUrlInvalid": "\"{{url}}\" não é uma página da web. As páginas iniciais precisam ser endereços http:// ou https://.",
"startUrlsTooMany": "Um perfil pode ter no máximo 20 páginas iniciais."
},
"rail": {
"profiles": "Perfis",
+9 -2
View File
@@ -1525,7 +1525,8 @@
"delete": "Удалить профиль",
"activity": "Активность",
"launchHook": "Хук запуска",
"transfer": "Перенос"
"transfer": "Перенос",
"startPages": "Стартовые страницы"
},
"sectionDesc": {
"fingerprint": "Настройте, как этот профиль выглядит для скриптов отпечатков.",
@@ -1768,6 +1769,10 @@
"deleteTitle": "Удалить «{{name}}»?",
"deleteDescription": "Профили с этим статусом останутся без статуса. Это действие нельзя отменить."
},
"startPages": {
"description": "Страницы, которые открывает каждый запуск из Donut вместо прошлой сессии, по одной на строку. Запуск со своим URL открывает только этот URL.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "Изменить VPN",
@@ -2509,7 +2514,9 @@
"proxyRotationUrlMissing": "У этого прокси нет ссылки для смены IP. Добавьте её при редактировании прокси.",
"proxyRotationFailed": "Провайдер отклонил смену IP (HTTP {{status}}).",
"proxyRotationTimeout": "Провайдер не ответил на смену IP за 30 секунд.",
"proxyRotationUnreachable": "Donut не удалось открыть ссылку провайдера для смены IP."
"proxyRotationUnreachable": "Donut не удалось открыть ссылку провайдера для смены IP.",
"startUrlInvalid": "«{{url}}» — не веб-страница. Стартовые страницы должны быть адресами http:// или https://.",
"startUrlsTooMany": "У профиля может быть не более 20 стартовых страниц."
},
"rail": {
"profiles": "Профили",
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "Profili sil",
"activity": "Etkinlik",
"launchHook": "Başlatma kancası",
"transfer": "Aktarım"
"transfer": "Aktarım",
"startPages": "Başlangıç sayfaları"
},
"sectionDesc": {
"fingerprint": "Bu profilin parmak izi tarama betiklerine nasıl görüneceğini yapılandırın.",
@@ -1754,6 +1755,10 @@
"deleteTitle": "\"{{name}}\" silinsin mi?",
"deleteDescription": "Bu durumdaki profillerin durumu kalmaz. Bu işlem geri alınamaz."
},
"startPages": {
"description": "Donut'tan her başlatmada son oturum yerine açılan sayfalar, satır başına bir tane. Kendi URL'si olan bir çalıştırma yalnızca o URL'yi açar.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "VPN'i Düzenle",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "Bu proxy'nin IP değiştirme bağlantısı yok. Proxy'yi düzenlerken ekleyin.",
"proxyRotationFailed": "Sağlayıcı IP değişikliğini reddetti (HTTP {{status}}).",
"proxyRotationTimeout": "Sağlayıcı IP değişikliğine 30 saniye içinde yanıt vermedi.",
"proxyRotationUnreachable": "Donut, sağlayıcının IP değiştirme bağlantısına ulaşamadı."
"proxyRotationUnreachable": "Donut, sağlayıcının IP değiştirme bağlantısına ulaşamadı.",
"startUrlInvalid": "\"{{url}}\" bir web sayfası değil. Başlangıç sayfaları http:// veya https:// adresleri olmalıdır.",
"startUrlsTooMany": "Bir profilde en fazla 20 başlangıç sayfası olabilir."
},
"rail": {
"profiles": "Profiller",
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "Xóa profile",
"activity": "Hoạt động",
"launchHook": "Hook khởi chạy",
"transfer": "Chuyển"
"transfer": "Chuyển",
"startPages": "Trang khởi động"
},
"sectionDesc": {
"fingerprint": "Cấu hình cách profile này hiển thị với các script vân tay.",
@@ -1754,6 +1755,10 @@
"deleteTitle": "Xóa \"{{name}}\"?",
"deleteDescription": "Các profile có trạng thái này sẽ không còn trạng thái. Không thể hoàn tác."
},
"startPages": {
"description": "Các trang mà mỗi lần khởi chạy từ Donut sẽ mở thay cho phiên trước, mỗi dòng một trang. Lần chạy có URL riêng chỉ mở URL đó.",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "Chỉnh sửa VPN",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "Proxy này chưa có liên kết đổi IP. Hãy thêm khi chỉnh sửa proxy.",
"proxyRotationFailed": "Nhà cung cấp từ chối đổi IP (HTTP {{status}}).",
"proxyRotationTimeout": "Nhà cung cấp không phản hồi yêu cầu đổi IP trong 30 giây.",
"proxyRotationUnreachable": "Donut không thể truy cập liên kết đổi IP của nhà cung cấp."
"proxyRotationUnreachable": "Donut không thể truy cập liên kết đổi IP của nhà cung cấp.",
"startUrlInvalid": "\"{{url}}\" không phải là trang web. Trang khởi động phải là địa chỉ http:// hoặc https://.",
"startUrlsTooMany": "Mỗi profile có tối đa 20 trang khởi động."
},
"rail": {
"profiles": "Profile",
+9 -2
View File
@@ -1511,7 +1511,8 @@
"delete": "删除配置文件",
"activity": "活动",
"launchHook": "启动钩子",
"transfer": "迁移"
"transfer": "迁移",
"startPages": "启动页"
},
"sectionDesc": {
"fingerprint": "配置此配置文件如何对指纹脚本显示。",
@@ -1754,6 +1755,10 @@
"deleteTitle": "删除“{{name}}”?",
"deleteDescription": "具有此状态的配置文件将不再有状态。此操作无法撤销。"
},
"startPages": {
"description": "每次从 Donut 启动时打开的页面,每行一个,代替上次会话。带有自身 URL 的运行只打开该 URL。",
"placeholder": "https://mail.example.com\nhttps://shop.example.com"
},
"vpns": {
"form": {
"titleEdit": "编辑 VPN",
@@ -2487,7 +2492,9 @@
"proxyRotationUrlMissing": "此代理没有更换 IP 链接。请在编辑代理时添加。",
"proxyRotationFailed": "服务商拒绝了更换 IP(HTTP {{status}})。",
"proxyRotationTimeout": "服务商在 30 秒内未响应更换 IP。",
"proxyRotationUnreachable": "Donut 无法访问服务商的更换 IP 链接。"
"proxyRotationUnreachable": "Donut 无法访问服务商的更换 IP 链接。",
"startUrlInvalid": "“{{url}}”不是网页。启动页必须是 http:// 或 https:// 地址。",
"startUrlsTooMany": "一个配置文件最多可以有 20 个启动页。"
},
"rail": {
"profiles": "配置文件",
+8
View File
@@ -35,6 +35,8 @@ export type BackendErrorCode =
| "PROXY_ROTATION_TIMEOUT"
| "PROXY_ROTATION_UNREACHABLE"
| "PROXY_ROTATION_FAILED"
| "START_URL_INVALID"
| "START_URLS_TOO_MANY"
| "COOKIE_DB_LOCKED"
| "COOKIE_DB_UNAVAILABLE"
| "COOKIE_IMPORT_BROWSER_RUNNING"
@@ -393,6 +395,12 @@ export function translateBackendError(t: TFunction, err: unknown): string {
return t("backendErrors.proxyRotationFailed", {
status: parsed.params?.status ?? "",
});
case "START_URL_INVALID":
return t("backendErrors.startUrlInvalid", {
url: parsed.params?.url ?? "",
});
case "START_URLS_TOO_MANY":
return t("backendErrors.startUrlsTooMany");
case "COOKIE_DB_LOCKED":
return t("backendErrors.cookieDbLocked");
case "COOKIE_DB_UNAVAILABLE":
+7
View File
@@ -0,0 +1,7 @@
/** One page per line, as the start-pages fields show and take them. */
export function startPagesFromText(text: string): string[] {
return text
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
}