mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-10 09:45:44 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 086567897a |
+36
-1
@@ -19860,7 +19860,42 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![begin_dock_badge_session, is_log_paused])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_engine_status, get_aria2_engine_status, get_ytdlp_engine_status, get_ffmpeg_engine_status,
|
||||
get_deno_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno,
|
||||
pause_download, resume_download, fetch_metadata, inspect_torrent, rekey_torrent_metadata, remove_torrent_metadata, fetch_media_metadata, fetch_media_playlist_metadata,
|
||||
begin_dock_badge_session, update_dock_badge, get_platform_info, approve_download_root, set_prevent_sleep, set_power_preferences, get_free_space, perform_system_action,
|
||||
ack_schedule_trigger,
|
||||
check_automation_permission, request_automation_permission, open_automation_settings,
|
||||
set_keychain_password, get_keychain_password, delete_keychain_password,
|
||||
save_site_login, delete_site_login,
|
||||
hydrate_extension_pairing_token, get_session_pairing_token, regenerate_pairing_token, grant_keychain_access,
|
||||
get_keychain_grant_status, accept_keychain_grant, abandon_keychain_grant,
|
||||
authorize_keychain_access,
|
||||
acknowledge_pairing_token_change,
|
||||
inspect_download_target, toggle_tray_icon, set_extension_pairing_token,
|
||||
get_extension_server_port, set_extension_frontend_ready, ack_frontend_exit, ack_extension_download, set_concurrent_limit, set_queue_concurrency_limits, set_download_speed_limit, set_torrent_upload_limit, set_torrent_peer_options, get_torrent_peers, get_torrent_availability, get_torrent_file_progress, get_torrent_piece_progress, get_torrent_file_selection, set_torrent_file_selection, get_torrent_details, get_torrent_magnet_link, export_torrent_metadata, move_torrent_data, cancel_torrent_move_data, verify_torrent_data, get_torrent_web_seeds, set_torrent_web_seeds, set_torrent_max_open_files, set_torrent_overall_upload_limit, set_global_speed_limit, remove_download, get_download_primary_path,
|
||||
detach_download_for_reconfigure,
|
||||
enqueue_download, enqueue_many, cancel_enqueue_generation, move_in_queue, move_many_in_queue, remove_from_queue, get_pending_order,
|
||||
commands::reveal_in_file_manager, commands::open_downloaded_file,
|
||||
properties_window::open_download_properties_window,
|
||||
properties_window::get_properties_window_download_id,
|
||||
properties_window::properties_window_send_ready,
|
||||
properties_window::properties_window_reveal,
|
||||
properties_window::properties_window_send_action,
|
||||
properties_window::validate_properties_window_request,
|
||||
properties_window::close_download_properties_window,
|
||||
properties_window::properties_window_registry_remove_for_download,
|
||||
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media, parity::get_supported_media_domains,
|
||||
parity::create_category_directories,
|
||||
db_save_settings, db_load_settings, canonicalize_torrent_network_setting,
|
||||
db_get_all_downloads, db_replace_downloads,
|
||||
db_commit_download_state,
|
||||
clear_torrent_removal_paths, reconcile_torrent_removal_reservations,
|
||||
db_get_all_queues, db_replace_queues,
|
||||
read_logs, export_logs, toggle_log_pause, is_log_paused, clear_logs,
|
||||
set_log_stream_active
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| match event {
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from "./store/useSettingsStore";
|
||||
import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
|
||||
import { WindowControls } from "./components/WindowControls";
|
||||
import { PropertiesWindowBridgeHost } from "./components/PropertiesWindowBridgeHost";
|
||||
import { useToast } from "./contexts/ToastContext";
|
||||
import { setLogStreamActive } from './utils/logger';
|
||||
import { updateDockBadge } from './utils/dockBadge';
|
||||
@@ -1287,7 +1286,6 @@ function App() {
|
||||
|
||||
{isAddModalOpen && <AddDownloadsModal />}
|
||||
|
||||
<PropertiesWindowBridgeHost />
|
||||
{isDeleteModalOpen && (
|
||||
<Suspense fallback={null}>
|
||||
<DeleteConfirmationModal />
|
||||
|
||||
+172
-9
@@ -1,15 +1,178 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { StrictMode, type ComponentType } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "@fontsource-variable/inter/wght.css";
|
||||
import "@fontsource-variable/noto-sans-hebrew/wght.css";
|
||||
import "@fontsource-variable/noto-sans-sc/wght.css";
|
||||
import "@fontsource-variable/outfit/wght.css";
|
||||
import "@fontsource-variable/roboto/wght.css";
|
||||
import "@fontsource-variable/vazirmatn/wght.css";
|
||||
import "./index.css";
|
||||
import { i18nReady } from "./i18n";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
import { ToastProvider } from "./contexts/ToastContext";
|
||||
import { error as logError, warn as logWarn, initLogger } from "./utils/logger";
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { invokeCommand as invoke } from './ipc';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) rootElement.textContent = 'Firelink startup control';
|
||||
const isPropertiesWindow = getCurrentWindow().label.startsWith('properties-');
|
||||
|
||||
// WebView2 can overflow its native call stack when the renderer sends IPC
|
||||
// during document bootstrapping. Keep all renderer-to-native startup work
|
||||
// behind the document load boundary, not just the first logger query. This is
|
||||
// also needed for Zustand persistence, whose module initialization reads the
|
||||
// native database before React mounts.
|
||||
const documentLoaded = new Promise<void>((resolve) => {
|
||||
const releaseAfterNativeLoad = () => window.setTimeout(resolve, 0);
|
||||
if (document.readyState === 'complete') releaseAfterNativeLoad();
|
||||
else window.addEventListener('load', releaseAfterNativeLoad, { once: true });
|
||||
const releaseAfterNativeLoad = () => {
|
||||
// The load event is dispatched from WebView2's navigation callback. Move
|
||||
// renderer startup to the next task so its first IPC cannot re-enter that
|
||||
// native callback stack.
|
||||
window.setTimeout(resolve, 0);
|
||||
};
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
releaseAfterNativeLoad();
|
||||
return;
|
||||
}
|
||||
window.addEventListener('load', releaseAfterNativeLoad, { once: true });
|
||||
});
|
||||
|
||||
void documentLoaded.then(async () => {
|
||||
await invoke<boolean>('is_log_paused');
|
||||
if (rootElement) rootElement.textContent = 'Firelink post-load log read';
|
||||
void documentLoaded.then(() => {
|
||||
void initLogger();
|
||||
});
|
||||
|
||||
const serializeConsoleArguments = (values: unknown[]) => values.map(value => {
|
||||
if (value instanceof Error) return `${value.name}: ${value.message}\n${value.stack || ''}`;
|
||||
if (typeof value === 'string') return value;
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}).join(' ');
|
||||
|
||||
const redactConsoleMessage = (message: string) => message
|
||||
.replace(/(authorization|cookie|password|token|secret)\s*[:=]\s*([^\s,;]+)/gi, '$1=[redacted]')
|
||||
.replace(/(https?:\/\/[^\s?]+)\?[^\s]+/g, '$1?[redacted]');
|
||||
|
||||
const originalConsoleError = console.error.bind(console);
|
||||
const originalConsoleWarn = console.warn.bind(console);
|
||||
console.error = (...values: unknown[]) => {
|
||||
originalConsoleError(...values);
|
||||
const message = redactConsoleMessage(serializeConsoleArguments(values));
|
||||
void documentLoaded.then(() => logError(message)).catch(() => undefined);
|
||||
};
|
||||
console.warn = (...values: unknown[]) => {
|
||||
originalConsoleWarn(...values);
|
||||
const message = redactConsoleMessage(serializeConsoleArguments(values));
|
||||
void documentLoaded.then(() => logWarn(message)).catch(() => undefined);
|
||||
};
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
const renderRoot = (RootComponent: ComponentType) => {
|
||||
if (!rootElement) return;
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<ToastProvider>
|
||||
<RootComponent />
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
};
|
||||
|
||||
const PropertiesStartupFailure = () => (
|
||||
<main className="properties-window-shell flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
|
||||
<p role="alert">Download Properties could not be loaded.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="app-button app-button-primary px-3 text-xs"
|
||||
onClick={() => {
|
||||
void getCurrentWindow().close().catch(error => {
|
||||
console.error('[PropertiesStartupFailure] close failed', error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
|
||||
const MainStartupFailure = () => (
|
||||
<main className="flex h-screen min-h-0 flex-col items-center justify-center gap-4 bg-main-bg p-6 text-text-primary">
|
||||
<p role="alert">Firelink could not be loaded.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="app-button app-button-primary px-3 text-xs"
|
||||
onClick={() => {
|
||||
void getCurrentWindow().close().catch(error => {
|
||||
console.error('[MainStartupFailure] close failed', error);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</main>
|
||||
);
|
||||
|
||||
const renderMainApp = async () => {
|
||||
if (!rootElement) return;
|
||||
|
||||
await documentLoaded;
|
||||
|
||||
try {
|
||||
// Keep the child entrypoint isolated from the main application module. App
|
||||
// imports the persistent Zustand stores, whose module initialization issues
|
||||
// main-window-only IPC commands. Loading it in a Properties child creates a
|
||||
// second persistence owner and can race the bridge handshake.
|
||||
const RootComponent = (await import('./App')).default;
|
||||
renderRoot(RootComponent);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Firelink:', error);
|
||||
renderRoot(MainStartupFailure);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPropertiesApp = async () => {
|
||||
if (!rootElement) return;
|
||||
|
||||
await documentLoaded;
|
||||
|
||||
try {
|
||||
// Properties starts with the synchronous English catalog and changes locale
|
||||
// after its first paint. Waiting for a lazy locale chunk here delays the
|
||||
// loading shell and makes native window startup visible to the user.
|
||||
const RootComponent = (await import('./components/PropertiesWindowApp')).PropertiesWindowApp;
|
||||
renderRoot(RootComponent);
|
||||
} catch (error) {
|
||||
// A failed lazy chunk must not leave the native window hidden forever. Show
|
||||
// a styled, closable failure state and use the same caller-validated native
|
||||
// reveal command as the normal child path.
|
||||
console.error('Failed to initialize the Properties window:', error);
|
||||
renderRoot(PropertiesStartupFailure);
|
||||
const fallbackSessionId = crypto.randomUUID();
|
||||
void invoke('properties_window_send_ready', { sessionId: fallbackSessionId })
|
||||
.then(() => invoke('properties_window_reveal', { sessionId: fallbackSessionId }))
|
||||
.catch(revealError => {
|
||||
console.error('Failed to reveal the Properties startup error:', revealError);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isPropertiesWindow) {
|
||||
void renderPropertiesApp();
|
||||
} else {
|
||||
void i18nReady.then(renderMainApp).catch(error => {
|
||||
console.error('Failed to initialize localization:', error);
|
||||
void renderMainApp();
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent the webview's default context menu ("Reload", etc.) on right-click.
|
||||
// Individual components that provide custom context menus call preventDefault()
|
||||
// in their own onContextMenu handlers, which fires before this document-level
|
||||
// listener and is unaffected.
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user