Files
sync/src/routes/home/+page.svelte
Sticks 3ffb8cf528
Some checks failed
FanslySync Build & Test / FanslySync Test Runner (push) Failing after 23m7s
fix: fix updater not working correctly
2025-04-30 10:51:47 -04:00

591 lines
19 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { fade, fly, slide } from 'svelte/transition';
import { info, error } from '@tauri-apps/plugin-log';
import { ask, message } from '@tauri-apps/plugin-dialog';
import { check, Update } from '@tauri-apps/plugin-updater';
import { getVersion, getTauriVersion } from '@tauri-apps/api/app';
import { invoke } from '@tauri-apps/api/core';
import { sendNotification } from '@tauri-apps/plugin-notification';
import { isEnabled, enable } from '@tauri-apps/plugin-autostart';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { platform } from '@tauri-apps/plugin-os';
import { toast } from 'svelte-french-toast';
import { awaiter } from '$lib/utils';
import type { Config, SyncData } from '$lib/types';
// --- App State ---
let loadingSync = true;
let upToDate: boolean | null = null;
let updateData: Update | null = null;
let versionData = { appVersion: 'Loading...', tauriVersion: 'Loading...' };
// --- Sync State ---
let syncState = {
show: false,
syncing: false,
success: false,
error: false,
url: '',
message: ''
};
let syncProgress = {
currentStep: '',
percentage: 0,
current_count: 0,
total_count: 0,
complete: false
};
let syncStartTime = 0;
// --- Config & AutoSync ---
let config: Config | null = null;
let syncInterval: ReturnType<typeof setInterval> | null = null;
let isAutoSyncConfigModalOpen = false;
let canSave = false;
let autoSyncConfig = { interval: 0, syncToken: '', didRunInitialValidation: false };
let autoSyncConfigState = { validatingToken: false, tokenValid: false };
onMount(async () => {
info('[FanslySync::page_init] Initializing...');
const [cfg, cfgErr] = await awaiter(invoke('get_config') as Promise<Config>);
if (cfgErr || !cfg) return await handleInitError(cfgErr);
config = cfg;
autoSyncConfig.interval = cfg.sync_interval;
autoSyncConfig.syncToken = cfg.sync_token;
const status = await check();
upToDate = status === null;
updateData = status;
versionData.appVersion = await getVersion();
versionData.tauriVersion = await getTauriVersion();
scheduleAutoSync();
loadingSync = false;
info('[FanslySync::page_init] Initialization complete.');
});
async function handleInitError(err: unknown) {
error(`[Init Error] ${err}`);
await message(`Failed to load configuration. Error: ${err}`, { kind: 'error' });
await writeText(String(err));
invoke('quit', { code: 1 });
}
function scheduleAutoSync() {
syncInterval && clearInterval(syncInterval);
syncInterval = setInterval(runAutoSync, (config?.sync_interval ?? 1) * 3600000);
}
async function runAutoSync() {
if (!config?.auto_sync_enabled) return;
info('[AutoSync] Starting...');
const nextTime = new Date(Date.now() + config.sync_interval * 3600000).toLocaleTimeString();
const data = await syncNow(true);
if (!data) return;
const [_, uploadErr] = await awaiter(
invoke('fansly_upload_auto_sync_data', { token: config.sync_token, data })
);
if (uploadErr) {
error('[AutoSync] Upload failed');
sendNotification({
title: 'Auto Sync Failed',
body: `We failed to upload your data. ${uploadErr}. We will try again in ${config.sync_interval} hours.`
});
} else {
sendNotification({
title: 'Auto Sync Successful',
body: `Your data has been uploaded successfully. Next sync will be at ${nextTime}.`
});
}
}
async function syncNow(auto = false): Promise<SyncData | null> {
syncState = { show: true, syncing: true, success: false, error: false, url: '', message: '' };
syncStartTime = Date.now();
const thread = setInterval(updateProgress, 500);
const [data, err] = await awaiter(invoke('fansly_sync', { auto }) as Promise<SyncData>);
clearInterval(thread);
if (err || !data) {
syncState = { ...syncState, syncing: false, error: true, message: String(err) };
sendNotification({ title: 'Sync Failed', body: String(err) });
return null;
}
syncState.url = data.sync_data_url;
config!.last_sync = Date.now();
config!.last_sync_data = data;
await saveConfig();
syncState = { ...syncState, syncing: false, success: true };
!auto && sendNotification({ title: 'Sync Successful', body: 'Check app for details.' });
auto && setTimeout(() => (syncState.show = false), 5000);
return auto ? data : null;
}
async function updateProgress() {
const [p] = await awaiter<{
current_step: string;
percentage_done: number;
current_count: number;
total_count: number;
complete: boolean;
}>(invoke('fansly_get_sync_status'));
if (p) {
syncProgress.currentStep = p.current_step;
syncProgress.percentage = p.percentage_done;
syncProgress.current_count = p.current_count;
syncProgress.total_count = p.total_count;
}
}
async function saveConfig() {
const [_, err] = await awaiter(invoke('save_config', { config }));
err && error('[SaveConfig] ' + err);
}
async function doUpdate() {
if (updateData === null) {
return message(`Up to date! v${versionData.appVersion}`, { kind: 'info' });
}
const confirm = await ask(
`A new version (v${updateData?.version}) is available. Do you want to update?`,
{
title: 'Update Available',
kind: 'warning'
}
);
if (confirm) {
await updateData.downloadAndInstall();
invoke('quit', { code: 0 });
}
}
async function enableAutoSync() {
if (!(await isEnabled())) {
const confirm = await ask(
"We've detected that auto-start is disabled. It's required for auto-sync to work. Do you want to enable it?",
{
title: 'Auto Sync Setup',
kind: 'info'
}
);
if (!confirm) return;
await toast.promise(enable(), {
loading: 'Enabling...',
success: 'Enabled!',
error: 'Failed'
});
}
if (!config?.sync_token)
return message('Set a sync token first.', {
kind: 'error',
title: 'Auto Sync Setup | Error'
});
config!.auto_sync_enabled = !config.auto_sync_enabled;
await saveConfig();
config.auto_sync_enabled ? scheduleAutoSync() : syncInterval && clearInterval(syncInterval);
toast.success(`${config.auto_sync_enabled ? 'Enabled' : 'Disabled'} Auto Sync`);
}
async function onSyncTokenEntered() {
autoSyncConfigState.validatingToken = true;
const [_, err] = await awaiter(
invoke('fansly_check_sync_token', { token: autoSyncConfig.syncToken })
);
autoSyncConfigState.validatingToken = false;
autoSyncConfigState.tokenValid = !err;
canSave = autoSyncConfigState.tokenValid;
}
async function onAutoSyncSave() {
isAutoSyncConfigModalOpen = false;
autoSyncConfig.didRunInitialValidation = false;
config!.sync_interval = autoSyncConfig.interval;
config!.sync_token = autoSyncConfig.syncToken;
await saveConfig();
toast.success('Settings saved');
}
onDestroy(() => syncInterval && clearInterval(syncInterval));
$: if (isAutoSyncConfigModalOpen && !autoSyncConfig.didRunInitialValidation) {
onSyncTokenEntered();
autoSyncConfig.didRunInitialValidation = true;
}
function useDebounce(fn: Function, delay: number) {
let timeout: ReturnType<typeof setTimeout>;
return (...args: any[]) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
}
</script>
<div class="min-h-screen bg-zinc-900 text-zinc-200 flex flex-col">
<!-- Header -->
<header class="flex items-center justify-between px-6 py-4 bg-zinc-800 shadow">
<div class="flex items-center space-x-3">
<img src="/fanslySync.png" alt="FanslySync" class="w-8 h-8" />
<div>
<h1 class="text-2xl font-semibold">FanslySync</h1>
<span class="text-sm text-zinc-400">
v{versionData.appVersion} · Tauri {versionData.tauriVersion}
</span>
</div>
</div>
<button
on:click={doUpdate}
class="text-sm font-medium px-3 py-1 rounded hover:bg-zinc-700 transition"
class:text-green-400={upToDate === true}
class:text-red-400={upToDate === false}
>
{#if upToDate === true}Up to date!{:else if upToDate === false}Update available!{/if}
</button>
</header>
<!-- Main Content -->
<main class="flex-1 overflow-auto p-6 space-y-6">
<!-- Loading / Error State -->
{#if loadingSync}
<div class="flex items-center justify-center p-10 text-zinc-400 space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-8 animate-spin"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
<span>Preparing, this shouldn't take long.</span>
</div>
{:else if config === null}
<div class="flex flex-col items-center justify-center p-10 text-zinc-400 space-x-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-8 text-yellow-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
/>
</svg>
<span> We couldn't load your config or your configuration doesn't exist.</span>
<p class="text-sm text-zinc-400">
Please restart the app or use the button below to reconfigure.
</p>
<button
class="mt-4 px-4 py-2 bg-blue-600 rounded hover:bg-blue-500 transition text-white"
on:click={() => (window.location.href = '/setup')}
>
Reconfigure
</button>
</div>
{:else if config !== null && !loadingSync}
<!-- Sync Controls -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Auto Sync Card -->
<div class="bg-zinc-800 p-6 rounded-lg shadow">
<div class="flex justify-between items-center mb-2">
<h2 class="text-xl font-semibold">Automatic Sync</h2>
<span
class="px-2 py-1 text-xs font-medium rounded"
class:bg-green-600={config?.auto_sync_enabled}
class:bg-red-600={!config?.auto_sync_enabled}
>
{config?.auto_sync_enabled ? 'Enabled' : 'Disabled'}
</span>
</div>
<p class="text-zinc-400 mb-4">
Your data will be automatically synced to your configured Discord server every {config?.sync_interval}
{config?.sync_interval === 1 ? 'hour' : 'hours'}
</p>
<div class="flex space-x-4">
<button
class="flex-1 py-2 rounded font-medium transition hover:opacity-90"
class:bg-green-600={!config?.auto_sync_enabled}
class:bg-red-600={config?.auto_sync_enabled}
on:click={enableAutoSync}
disabled={syncState.syncing}
>
{syncState.syncing
? 'Syncing…'
: config?.auto_sync_enabled
? 'Disable Auto Sync'
: 'Enable Auto Sync'}
</button>
<button
class="flex-1 py-2 bg-zinc-700 rounded font-medium transition hover:opacity-90"
on:click={() => (isAutoSyncConfigModalOpen = true)}
disabled={config?.auto_sync_enabled || syncState.syncing}
>
Edit Settings
</button>
</div>
</div>
<!-- Manual Sync Card -->
<div class="bg-zinc-800 p-6 rounded-lg shadow">
<h2 class="text-xl font-semibold mb-2">Manual Sync</h2>
<p class="text-zinc-400 mb-4">Trigger a sync immediately.</p>
<button
class="w-full py-2 bg-blue-600 rounded font-medium transition hover:bg-blue-500 disabled:opacity-50"
on:click={() => syncNow(false)}
disabled={syncState.syncing}
>
{syncState.syncing ? 'Syncing…' : 'Sync Now'}
</button>
</div>
</section>
{/if}
</main>
<!-- Progress Bar & Status -->
{#if syncState.show}
<div class="fixed bottom-0 left-0 right-0">
<div
class="fixed bottom-0 left-0 right-0 h-16 overflow-hidden"
transition:slide={{ duration: 500 }}
>
<!-- Background track -->
<div class="absolute inset-0 bg-zinc-800"></div>
<!-- Fill using message box background -->
<div
class={`absolute inset-y-0 left-0 transition-all duration-500 ease-in-out
${syncState.syncing ? 'bg-blue-500' : 'bg-zinc-800'}`}
style="width: {syncState.syncing
? `${syncProgress.percentage === 0 ? 2 : syncProgress.percentage}%`
: '100%'};"
></div>
<!-- Message Box Content -->
<div class="relative z-10 flex items-center h-full px-4 text-white">
{#if syncState.syncing}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6 animate-spin mr-3"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
<div>
<p class="font-medium">Syncing… {syncProgress.percentage}%</p>
<p class="text-sm">
{syncProgress.current_count} / {syncProgress.total_count}{syncProgress.currentStep}
</p>
</div>
{:else if syncState.success}
<div class="flex flex-col">
<p class="font-medium">Sync Successful!</p>
<p class="text-sm">Use Copy to grab URL.</p>
</div>
<div class="ml-auto flex space-x-2">
<button
class="px-3 py-1 bg-zinc-800 rounded"
on:click={() => writeText(syncState.url)}>Copy</button
>
<button
class="px-3 py-1 bg-zinc-800 rounded"
on:click={() => (syncState.show = false)}>Close</button
>
</div>
{:else}
<div class="flex flex-col">
<p class="font-medium">Sync Failed!</p>
<p class="text-sm">{syncState.message}</p>
</div>
<button
class="ml-auto px-3 py-1 bg-zinc-800 rounded"
on:click={() => (syncState.show = false)}>Close</button
>
{/if}
</div>
</div>
</div>
{/if}
<!-- Auto Sync Modal -->
{#if isAutoSyncConfigModalOpen}
<div
class="fixed inset-0 bg-black bg-opacity-60 flex items-center justify-center"
transition:fade
>
<div
class="bg-zinc-800 p-6 rounded-lg w-full max-w-md"
in:fly={{ y: 20 }}
out:fly={{ y: 20 }}
>
<h3 class="text-xl font-semibold mb-4 flex items-center space-x-2">
<!-- Cog Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0
1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257
1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296
2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723
7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26
1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47
6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213
1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52
6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125
1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932
6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125
1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072
1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
<span>Auto Sync Settings</span>
</h3>
<div class="space-y-4">
<!-- Interval Field -->
<div>
<label for="interval" class="block text-sm mb-1">Interval (hours)</label>
<input
id="interval"
type="number"
bind:value={autoSyncConfig.interval}
min="1"
class="w-full p-2 bg-zinc-700 rounded"
/>
<p class="text-xs text-zinc-400 mt-1">
How often (in hours) the app will automatically sync.
</p>
</div>
<!-- Token Field -->
<div>
<label for="syncToken" class="block text-sm mb-1">Sync Token</label>
<div class="relative">
<input
id="syncToken"
type="text"
bind:value={autoSyncConfig.syncToken}
on:input={useDebounce(onSyncTokenEntered, 500)}
class="w-full p-2 bg-zinc-700 rounded pr-10"
/>
{#if autoSyncConfigState.validatingToken}
<!-- Spinner Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="absolute right-2 bottom-2 w-6 h-6 animate-spin text-zinc-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0
0h4.992m-4.993 0 3.181 3.183a8.25 8.25
0 0 0 13.803-3.7M4.031 9.865a8.25 8.25
0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
{:else if autoSyncConfigState.tokenValid}
<!-- Check Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="absolute right-2 bottom-2 w-6 h-6 text-green-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0
1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
{:else}
<!-- X Icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="absolute right-2 bottom-2 w-6 h-6 text-red-400"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
{/if}
</div>
<p class="text-xs text-zinc-400 mt-1">
Enter your Fansly sync-token. Well validate it before saving.
</p>
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button
class="px-4 py-2 bg-zinc-700 rounded hover:bg-zinc-600"
on:click={() => (isAutoSyncConfigModalOpen = false)}
>
Cancel
</button>
<button
class="px-4 py-2 bg-blue-600 rounded hover:bg-blue-500 disabled:opacity-50"
on:click={onAutoSyncSave}
disabled={!canSave}
>
Save
</button>
</div>
</div>
</div>
{/if}
</div>