Compare commits
4 Commits
947ea31fe0
...
ad9fc9aa7a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad9fc9aa7a | ||
|
|
13c03caeae | ||
|
|
6593ca4d8b | ||
|
|
ea7b2e8ec2 |
48
src/lib/utils/app-config.ts
Normal file
48
src/lib/utils/app-config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export type ProxyKind = 'Http' | 'Socks5';
|
||||
|
||||
export type ProxyConfig = {
|
||||
enabled: boolean;
|
||||
kind: ProxyKind;
|
||||
host: string | null;
|
||||
port: number | null;
|
||||
username: string | null;
|
||||
password: string | null;
|
||||
};
|
||||
|
||||
export type AppConfig = {
|
||||
proxy?: Partial<ProxyConfig> | null;
|
||||
} & Record<string, unknown>;
|
||||
|
||||
export async function loadAppConfig() {
|
||||
return invoke<AppConfig>('load_app_config');
|
||||
}
|
||||
|
||||
export async function saveAppConfig(config: AppConfig) {
|
||||
await invoke('save_app_config', { config });
|
||||
}
|
||||
|
||||
export function getProxyConfig(config: AppConfig): ProxyConfig {
|
||||
const proxy = config.proxy ?? {};
|
||||
|
||||
return {
|
||||
enabled: proxy.enabled === true,
|
||||
kind: proxy.kind === 'Socks5' ? 'Socks5' : 'Http',
|
||||
host: typeof proxy.host === 'string' ? proxy.host : null,
|
||||
port: typeof proxy.port === 'number' && Number.isInteger(proxy.port) ? proxy.port : null,
|
||||
username: typeof proxy.username === 'string' ? proxy.username : null,
|
||||
password: typeof proxy.password === 'string' ? proxy.password : null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateProxyConfig(updater: (current: ProxyConfig) => ProxyConfig) {
|
||||
const currentConfig = await loadAppConfig();
|
||||
const nextProxyConfig = updater(getProxyConfig(currentConfig));
|
||||
await saveAppConfig({
|
||||
...currentConfig,
|
||||
proxy: nextProxyConfig,
|
||||
});
|
||||
|
||||
return nextProxyConfig;
|
||||
}
|
||||
25
src/routes/settings/+layout.svelte
Normal file
25
src/routes/settings/+layout.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { currentActivate } from '$lib/stores/navigate';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { children, data }: { data: { activePath: string }; children: Snippet } = $props();
|
||||
afterNavigate(() => {
|
||||
currentActivate.set('settings');
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="px-4 py-4 size-full flex flex-row items-stretch gap-0 overflow-hidden">
|
||||
<nav class="max-w-[12em] grow flex flex-col items-stretch gap-2">
|
||||
<a
|
||||
href="/settings/proxy"
|
||||
class={[
|
||||
'inline-block px-4 py-2 rounded-md hover:bg-cyan-700',
|
||||
data.activePath === '/settings/proxy' && 'bg-cyan-900',
|
||||
]}>Proxy</a>
|
||||
</nav>
|
||||
<div class="divider divider-horizontal mx-2"></div>
|
||||
<div class="grow">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</main>
|
||||
6
src/routes/settings/+layout.ts
Normal file
6
src/routes/settings/+layout.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
const path = url.pathname;
|
||||
return { activePath: path };
|
||||
}) satisfies LayoutLoad;
|
||||
@@ -1,8 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { currentActivate } from '$lib/stores/navigate';
|
||||
|
||||
afterNavigate(() => {
|
||||
currentActivate.set('settings');
|
||||
});
|
||||
</script>
|
||||
6
src/routes/settings/+page.ts
Normal file
6
src/routes/settings/+page.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
throw redirect(307, "/settings/proxy");
|
||||
};
|
||||
19
src/routes/settings/proxy/+page.svelte
Normal file
19
src/routes/settings/proxy/+page.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import EnableProxy from './EnableProxy.svelte';
|
||||
import ProtocolSelect from './ProtocolSelect.svelte';
|
||||
import ProxyHost from './ProxyHost.svelte';
|
||||
import ProxyPassword from './ProxyPassword.svelte';
|
||||
import ProxyPort from './ProxyPort.svelte';
|
||||
import ProxyUsername from './ProxyUsername.svelte';
|
||||
|
||||
let enabledProxy = $state<boolean>(false);
|
||||
</script>
|
||||
|
||||
<section class="px-2 py-4 flex flex-col gap-2">
|
||||
<ProtocolSelect />
|
||||
<ProxyHost />
|
||||
<ProxyPort />
|
||||
<ProxyUsername />
|
||||
<ProxyPassword />
|
||||
<EnableProxy />
|
||||
</section>
|
||||
51
src/routes/settings/proxy/EnableProxy.svelte
Normal file
51
src/routes/settings/proxy/EnableProxy.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { getProxyConfig, loadAppConfig, updateProxyConfig } from '$lib/utils/app-config';
|
||||
import { createSaveFeedbackController, type SaveFeedbackState } from '$lib/utils/form-save';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let enabledProxy = $state<boolean>(false);
|
||||
let saveFeedback = $state<SaveFeedbackState>('idle');
|
||||
|
||||
const feedback = createSaveFeedbackController((state) => {
|
||||
saveFeedback = state;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig();
|
||||
enabledProxy = getProxyConfig(config).enabled;
|
||||
} catch (error) {
|
||||
console.error('Failed to load proxy enabled status:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
feedback.dispose();
|
||||
});
|
||||
|
||||
async function saveEnabledProxy() {
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
enabled: enabledProxy,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to save proxy enabled status:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row gap-3 h-(--size) py-2">
|
||||
<div class="min-w-[10em]"> </div>
|
||||
<label
|
||||
class={[
|
||||
'label transition-colors duration-300 ease-out',
|
||||
saveFeedback === 'updated' && 'text-green-600',
|
||||
saveFeedback === 'not-updated' && 'text-red-600',
|
||||
]}>
|
||||
<input type="checkbox" bind:checked={enabledProxy} onchange={saveEnabledProxy} class="toggle" />
|
||||
Enable Proxy
|
||||
</label>
|
||||
</div>
|
||||
77
src/routes/settings/proxy/ProtocolSelect.svelte
Normal file
77
src/routes/settings/proxy/ProtocolSelect.svelte
Normal file
@@ -0,0 +1,77 @@
|
||||
<script lang="ts">
|
||||
import type { Option } from '$lib/types/base';
|
||||
import {
|
||||
getProxyConfig,
|
||||
loadAppConfig,
|
||||
updateProxyConfig,
|
||||
type ProxyKind,
|
||||
} from '$lib/utils/app-config';
|
||||
import { createSaveFeedbackController, type SaveFeedbackState } from '$lib/utils/form-save';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let selectedProtocol = $state<string>('Http');
|
||||
let saveFeedback = $state<SaveFeedbackState>('idle');
|
||||
|
||||
const feedback = createSaveFeedbackController((state) => {
|
||||
saveFeedback = state;
|
||||
});
|
||||
|
||||
const protocolChoicesPromise = invoke<Option<string>[]>('list_proxy_kind_options');
|
||||
|
||||
function isProxyKind(value: string): value is ProxyKind {
|
||||
return value === 'Http' || value === 'Socks5';
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig();
|
||||
selectedProtocol = getProxyConfig(config).kind;
|
||||
} catch (error) {
|
||||
console.error('Failed to load proxy protocol:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
feedback.dispose();
|
||||
});
|
||||
|
||||
async function saveProtocol() {
|
||||
if (!isProxyKind(selectedProtocol)) {
|
||||
feedback.markNotUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
const nextProtocol: ProxyKind = selectedProtocol;
|
||||
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
kind: nextProtocol,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to save proxy protocol:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={[
|
||||
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
|
||||
saveFeedback === 'updated' && 'border-green-500',
|
||||
saveFeedback === 'not-updated' && 'border-red-500',
|
||||
]}>
|
||||
<span class="label min-w-[10em]">Protocol</span>
|
||||
<select
|
||||
bind:value={selectedProtocol}
|
||||
onchange={saveProtocol}
|
||||
class="min-w-[20em] focus:outline-none">
|
||||
{#await protocolChoicesPromise then ProtocolChoices}
|
||||
{#each ProtocolChoices as protocol}
|
||||
<option value={protocol.value}>{protocol.label}</option>
|
||||
{/each}
|
||||
{/await}
|
||||
</select>
|
||||
</label>
|
||||
67
src/routes/settings/proxy/ProxyHost.svelte
Normal file
67
src/routes/settings/proxy/ProxyHost.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { getProxyConfig, loadAppConfig, updateProxyConfig } from '$lib/utils/app-config';
|
||||
import {
|
||||
createDebouncedTrigger,
|
||||
createSaveFeedbackController,
|
||||
type SaveFeedbackState,
|
||||
} from '$lib/utils/form-save';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let proxyHost = $state<string>('');
|
||||
let saveFeedback = $state<SaveFeedbackState>('idle');
|
||||
|
||||
const feedback = createSaveFeedbackController((state) => {
|
||||
saveFeedback = state;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig();
|
||||
proxyHost = getProxyConfig(config).host ?? '';
|
||||
} catch (error) {
|
||||
console.error('Failed to load proxy host:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
saveLater.cancel();
|
||||
feedback.dispose();
|
||||
});
|
||||
|
||||
async function saveProxyHost(value: string) {
|
||||
const nextHost = value.trim();
|
||||
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
host: nextHost || null,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to save proxy host:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
const saveLater = createDebouncedTrigger(() => {
|
||||
void saveProxyHost(proxyHost);
|
||||
}, 800);
|
||||
|
||||
function saveHostDelayed() {
|
||||
saveLater.trigger();
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={[
|
||||
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
|
||||
saveFeedback === 'updated' && 'border-green-500',
|
||||
saveFeedback === 'not-updated' && 'border-red-500',
|
||||
]}>
|
||||
<span class="label min-w-[10em]">Host</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={proxyHost}
|
||||
oninput={saveHostDelayed}
|
||||
class="min-w-[20em] focus:outline-none" />
|
||||
</label>
|
||||
67
src/routes/settings/proxy/ProxyPassword.svelte
Normal file
67
src/routes/settings/proxy/ProxyPassword.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { getProxyConfig, loadAppConfig, updateProxyConfig } from '$lib/utils/app-config';
|
||||
import {
|
||||
createDebouncedTrigger,
|
||||
createSaveFeedbackController,
|
||||
type SaveFeedbackState,
|
||||
} from '$lib/utils/form-save';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let proxyPassword = $state<string>('');
|
||||
let saveFeedback = $state<SaveFeedbackState>('idle');
|
||||
|
||||
const feedback = createSaveFeedbackController((state) => {
|
||||
saveFeedback = state;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig();
|
||||
proxyPassword = getProxyConfig(config).password ?? '';
|
||||
} catch (error) {
|
||||
console.error('Failed to load proxy password:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
saveLater.cancel();
|
||||
feedback.dispose();
|
||||
});
|
||||
|
||||
async function saveProxyPassword(value: string) {
|
||||
const nextPassword = value.trim();
|
||||
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
password: nextPassword || null,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to save proxy password:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
const saveLater = createDebouncedTrigger(() => {
|
||||
void saveProxyPassword(proxyPassword);
|
||||
}, 800);
|
||||
|
||||
function savePasswordDelayed() {
|
||||
saveLater.trigger();
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={[
|
||||
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
|
||||
saveFeedback === 'updated' && 'border-green-500',
|
||||
saveFeedback === 'not-updated' && 'border-red-500',
|
||||
]}>
|
||||
<span class="label min-w-[10em]">Password</span>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={proxyPassword}
|
||||
oninput={savePasswordDelayed}
|
||||
class="min-w-[20em] focus:outline-none" />
|
||||
</label>
|
||||
85
src/routes/settings/proxy/ProxyPort.svelte
Normal file
85
src/routes/settings/proxy/ProxyPort.svelte
Normal file
@@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { getProxyConfig, loadAppConfig, updateProxyConfig } from '$lib/utils/app-config';
|
||||
import {
|
||||
createDebouncedTrigger,
|
||||
createSaveFeedbackController,
|
||||
type SaveFeedbackState,
|
||||
} from '$lib/utils/form-save';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let proxyPort = $state<number | null>(null);
|
||||
let saveFeedback = $state<SaveFeedbackState>('idle');
|
||||
|
||||
const feedback = createSaveFeedbackController((state) => {
|
||||
saveFeedback = state;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig();
|
||||
const port = getProxyConfig(config).port;
|
||||
proxyPort = port ?? null;
|
||||
} catch (error) {
|
||||
console.error('Failed to load proxy port:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
saveLater.cancel();
|
||||
feedback.dispose();
|
||||
});
|
||||
|
||||
async function saveProxyPort(value: number | null) {
|
||||
if (value === null || Number.isNaN(value)) {
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
port: null,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to clear proxy port:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
||||
feedback.markNotUpdated();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
port: value,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to save proxy port:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
const saveLater = createDebouncedTrigger(() => {
|
||||
void saveProxyPort(proxyPort);
|
||||
}, 800);
|
||||
|
||||
function savePortDelayed() {
|
||||
saveLater.trigger();
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={[
|
||||
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
|
||||
saveFeedback === 'updated' && 'border-green-500',
|
||||
saveFeedback === 'not-updated' && 'border-red-500',
|
||||
]}>
|
||||
<span class="label min-w-[10em]">Port</span>
|
||||
<input
|
||||
type="number"
|
||||
bind:value={proxyPort}
|
||||
oninput={savePortDelayed}
|
||||
class="min-w-[10em] focus:outline-none" />
|
||||
</label>
|
||||
67
src/routes/settings/proxy/ProxyUsername.svelte
Normal file
67
src/routes/settings/proxy/ProxyUsername.svelte
Normal file
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { getProxyConfig, loadAppConfig, updateProxyConfig } from '$lib/utils/app-config';
|
||||
import {
|
||||
createDebouncedTrigger,
|
||||
createSaveFeedbackController,
|
||||
type SaveFeedbackState,
|
||||
} from '$lib/utils/form-save';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let proxyUsername = $state<string>('');
|
||||
let saveFeedback = $state<SaveFeedbackState>('idle');
|
||||
|
||||
const feedback = createSaveFeedbackController((state) => {
|
||||
saveFeedback = state;
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const config = await loadAppConfig();
|
||||
proxyUsername = getProxyConfig(config).username ?? '';
|
||||
} catch (error) {
|
||||
console.error('Failed to load proxy username:', error);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
saveLater.cancel();
|
||||
feedback.dispose();
|
||||
});
|
||||
|
||||
async function saveProxyUsername(value: string) {
|
||||
const nextUsername = value.trim();
|
||||
|
||||
try {
|
||||
await updateProxyConfig((current) => ({
|
||||
...current,
|
||||
username: nextUsername || null,
|
||||
}));
|
||||
feedback.markUpdated();
|
||||
} catch (error) {
|
||||
console.error('Failed to save proxy username:', error);
|
||||
feedback.markNotUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
const saveLater = createDebouncedTrigger(() => {
|
||||
void saveProxyUsername(proxyUsername);
|
||||
}, 800);
|
||||
|
||||
function saveUsernameDelayed() {
|
||||
saveLater.trigger();
|
||||
}
|
||||
</script>
|
||||
|
||||
<label
|
||||
class={[
|
||||
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
|
||||
saveFeedback === 'updated' && 'border-green-500',
|
||||
saveFeedback === 'not-updated' && 'border-red-500',
|
||||
]}>
|
||||
<span class="label min-w-[10em]">Username</span>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={proxyUsername}
|
||||
oninput={saveUsernameDelayed}
|
||||
class="min-w-[20em] focus:outline-none" />
|
||||
</label>
|
||||
Reference in New Issue
Block a user