mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-20 08:02:16 +00:00
feat(settings): stream engine check results
This commit is contained in:
+27
-3
@@ -1754,6 +1754,29 @@ async fn get_engine_status(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_aria2_engine_status(
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
state: tauri::State<'_, AppState>,
|
||||||
|
) -> Result<EngineStatusItem, String> {
|
||||||
|
Ok(check_aria2(&app_handle, state.aria2_port, &state.aria2_secret).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_ytdlp_engine_status(app_handle: tauri::AppHandle) -> Result<EngineStatusItem, String> {
|
||||||
|
Ok(check_ytdlp(&app_handle).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_ffmpeg_engine_status(app_handle: tauri::AppHandle) -> Result<EngineStatusItem, String> {
|
||||||
|
Ok(check_ffmpeg(&app_handle).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_deno_engine_status(app_handle: tauri::AppHandle) -> Result<EngineStatusItem, String> {
|
||||||
|
Ok(check_deno(&app_handle).await)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str) -> Result<std::path::PathBuf, String> {
|
fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str) -> Result<std::path::PathBuf, String> {
|
||||||
let full_name = format!(
|
let full_name = format!(
|
||||||
@@ -3419,9 +3442,10 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
|
get_engine_status, get_aria2_engine_status, get_ytdlp_engine_status, get_ffmpeg_engine_status,
|
||||||
pause_download, resume_download, fetch_metadata, fetch_media_metadata,
|
get_deno_engine_status, test_ytdlp, test_aria2c, test_ffmpeg, test_deno, open_file, show_in_folder,
|
||||||
|
pause_download, resume_download, fetch_metadata, fetch_media_metadata,
|
||||||
update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action,
|
update_dock_badge, set_prevent_sleep, get_free_space, perform_system_action,
|
||||||
request_automation_permission, open_automation_settings,
|
request_automation_permission, open_automation_settings,
|
||||||
set_keychain_password, get_keychain_password, delete_keychain_password,
|
set_keychain_password, get_keychain_password, delete_keychain_password,
|
||||||
|
|||||||
+134
-20
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
type AppFontSize,
|
type AppFontSize,
|
||||||
type ListRowDensity,
|
type ListRowDensity,
|
||||||
@@ -29,15 +29,83 @@ const settingsTabs: { type: SettingsTab; label: string; icon: typeof Download }[
|
|||||||
{ type: 'about', label: 'About', icon: Info },
|
{ type: 'about', label: 'About', icon: Info },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const engineChecks = [
|
||||||
|
{ kind: 'aria2', name: 'Aria2', command: 'get_aria2_engine_status' },
|
||||||
|
{ kind: 'ytdlp', name: 'yt-dlp', command: 'get_ytdlp_engine_status' },
|
||||||
|
{ kind: 'ffmpeg', name: 'FFmpeg', command: 'get_ffmpeg_engine_status' },
|
||||||
|
{ kind: 'deno', name: 'Deno', command: 'get_deno_engine_status' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type EngineCheck = typeof engineChecks[number];
|
||||||
|
|
||||||
|
const engineStatusCache = new Map<string, EngineStatusItem>();
|
||||||
|
const engineStatusInFlight = new Map<string, Promise<EngineStatusItem>>();
|
||||||
|
|
||||||
|
const upsertEngineStatus = (items: EngineStatusItem[], item: EngineStatusItem) => {
|
||||||
|
const next = items.filter(existing => existing.kind !== item.kind);
|
||||||
|
next.push(item);
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildEngineStatusError = (check: EngineCheck, error: unknown): EngineStatusItem => ({
|
||||||
|
name: check.name,
|
||||||
|
kind: check.kind,
|
||||||
|
expected_sidecar: '',
|
||||||
|
resolved_path: null,
|
||||||
|
version: null,
|
||||||
|
ready: false,
|
||||||
|
error: String(error),
|
||||||
|
stderr_tail: null,
|
||||||
|
remediation_hint: null,
|
||||||
|
rpc_port: null,
|
||||||
|
daemon_alive: null,
|
||||||
|
rpc_ready: null,
|
||||||
|
last_stderr_tail: null,
|
||||||
|
expects_internal_dir: null,
|
||||||
|
has_internal_dir: null,
|
||||||
|
has_python_framework: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const runEngineStatusCheck = (check: EngineCheck, force: boolean) => {
|
||||||
|
if (!force) {
|
||||||
|
const cached = engineStatusCache.get(check.kind);
|
||||||
|
if (cached) return Promise.resolve(cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
const inFlight = engineStatusInFlight.get(check.kind);
|
||||||
|
if (inFlight) return inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (force) engineStatusCache.delete(check.kind);
|
||||||
|
|
||||||
|
const promise = invoke(check.command)
|
||||||
|
.then(item => {
|
||||||
|
if (item.ready) engineStatusCache.set(item.kind, item);
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.catch(error => buildEngineStatusError(check, error))
|
||||||
|
.finally(() => {
|
||||||
|
if (engineStatusInFlight.get(check.kind) === promise) {
|
||||||
|
engineStatusInFlight.delete(check.kind);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
engineStatusInFlight.set(check.kind, promise);
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsView() {
|
export default function SettingsView() {
|
||||||
const settings = useSettingsStore();
|
const settings = useSettingsStore();
|
||||||
const { pickDirectory } = useDirectoryPicker();
|
const { pickDirectory } = useDirectoryPicker();
|
||||||
const activeTab = settings.activeSettingsTab;
|
const activeTab = settings.activeSettingsTab;
|
||||||
|
|
||||||
// Local state for engine diagnostics
|
// Local state for engine diagnostics
|
||||||
const [engineStatus, setEngineStatus] = useState<EngineStatusItem[] | null>(null);
|
const [engineStatus, setEngineStatus] = useState<EngineStatusItem[] | null>(null);
|
||||||
const [expandedEngine, setExpandedEngine] = useState<string | null>(null);
|
const [expandedEngine, setExpandedEngine] = useState<string | null>(null);
|
||||||
const [appVersion, setAppVersion] = useState('0.7.3');
|
const [isRecheckingEngines, setIsRecheckingEngines] = useState(false);
|
||||||
|
const engineRunId = useRef(0);
|
||||||
|
const [appVersion, setAppVersion] = useState('0.7.3');
|
||||||
|
|
||||||
// Local state for adding site login
|
// Local state for adding site login
|
||||||
const [loginPattern, setLoginPattern] = useState('');
|
const [loginPattern, setLoginPattern] = useState('');
|
||||||
@@ -56,20 +124,52 @@ export default function SettingsView() {
|
|||||||
}
|
}
|
||||||
}, [toastMessage]);
|
}, [toastMessage]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getVersion().then(setAppVersion).catch(() => undefined);
|
getVersion().then(setAppVersion).catch(() => undefined);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch engine status when Engine tab is opened
|
const runEngineChecks = useCallback((force = false) => {
|
||||||
useEffect(() => {
|
const runId = ++engineRunId.current;
|
||||||
if (settings.activeView === 'settings' && activeTab === 'engine') {
|
const cached = engineChecks
|
||||||
setEngineStatus(null);
|
.map(check => engineStatusCache.get(check.kind))
|
||||||
setExpandedEngine(null);
|
.filter((item): item is EngineStatusItem => Boolean(item));
|
||||||
invoke('get_engine_status')
|
setEngineStatus(force ? [] : cached);
|
||||||
.then(r => setEngineStatus(r.engines))
|
setExpandedEngine(null);
|
||||||
.catch(() => setEngineStatus([]));
|
|
||||||
}
|
const checksToRun = force
|
||||||
}, [settings.activeView, activeTab]);
|
? engineChecks
|
||||||
|
: engineChecks.filter(check => !engineStatusCache.has(check.kind));
|
||||||
|
|
||||||
|
if (checksToRun.length === 0) {
|
||||||
|
setIsRecheckingEngines(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRecheckingEngines(true);
|
||||||
|
let remaining = checksToRun.length;
|
||||||
|
|
||||||
|
checksToRun.forEach(check => {
|
||||||
|
runEngineStatusCheck(check, force)
|
||||||
|
.then(item => {
|
||||||
|
if (engineRunId.current === runId) {
|
||||||
|
setEngineStatus(current => upsertEngineStatus(current ?? [], item));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
remaining -= 1;
|
||||||
|
if (remaining === 0 && engineRunId.current === runId) {
|
||||||
|
setIsRecheckingEngines(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Fetch engine status when Engine tab is opened
|
||||||
|
useEffect(() => {
|
||||||
|
if (settings.activeView === 'settings' && activeTab === 'engine') {
|
||||||
|
runEngineChecks(false);
|
||||||
|
}
|
||||||
|
}, [settings.activeView, activeTab, runEngineChecks]);
|
||||||
|
|
||||||
const showToast = (msg: string) => {
|
const showToast = (msg: string) => {
|
||||||
setToastMessage(msg);
|
setToastMessage(msg);
|
||||||
@@ -767,10 +867,24 @@ export default function SettingsView() {
|
|||||||
|
|
||||||
{/* Engine Pane */}
|
{/* Engine Pane */}
|
||||||
{activeTab === 'engine' && (
|
{activeTab === 'engine' && (
|
||||||
<div className="settings-pane space-y-6 max-w-[760px]">
|
<div className="settings-pane space-y-6 max-w-[760px]">
|
||||||
<h3 className="text-base font-bold text-text-primary border-b border-border-color/30 pb-2">Media Downloader & Engines</h3>
|
<div className="flex items-center justify-between gap-3 border-b border-border-color/30 pb-2">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-bold text-text-primary">Media Downloader & Engines</h3>
|
||||||
|
<p className="text-[11px] text-text-muted mt-0.5">Successful results are reused for this app session. Recheck runs real validation again.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => runEngineChecks(true)}
|
||||||
|
disabled={isRecheckingEngines}
|
||||||
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md bg-bg-modal hover:bg-item-hover disabled:opacity-60 text-[12px] text-text-primary"
|
||||||
|
>
|
||||||
|
<RefreshCw size={13} className={isRecheckingEngines ? 'animate-spin' : ''} />
|
||||||
|
{isRecheckingEngines ? 'Checking…' : 'Recheck engines'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const a2 = findEngine('aria2'); const yt = findEngine('ytdlp');
|
const a2 = findEngine('aria2'); const yt = findEngine('ytdlp');
|
||||||
const ff = findEngine('ffmpeg'); const dn = findEngine('deno');
|
const ff = findEngine('ffmpeg'); const dn = findEngine('deno');
|
||||||
return (
|
return (
|
||||||
|
|||||||
+6
-1
@@ -10,6 +10,7 @@ import type { ExtensionDownload } from './bindings/ExtensionDownload';
|
|||||||
import type { MediaCookieSource } from './bindings/MediaCookieSource';
|
import type { MediaCookieSource } from './bindings/MediaCookieSource';
|
||||||
import type { MediaMetadata } from './bindings/MediaMetadata';
|
import type { MediaMetadata } from './bindings/MediaMetadata';
|
||||||
import type { MetadataResponse } from './bindings/MetadataResponse';
|
import type { MetadataResponse } from './bindings/MetadataResponse';
|
||||||
|
import type { EngineStatusItem } from './bindings/EngineStatusItem';
|
||||||
import type { EngineStatusResult } from './bindings/EngineStatusResult';
|
import type { EngineStatusResult } from './bindings/EngineStatusResult';
|
||||||
import type { PostQueueAction } from './bindings/PostQueueAction';
|
import type { PostQueueAction } from './bindings/PostQueueAction';
|
||||||
import type { ReleaseCheckOutcome } from './bindings/ReleaseCheckOutcome';
|
import type { ReleaseCheckOutcome } from './bindings/ReleaseCheckOutcome';
|
||||||
@@ -57,7 +58,11 @@ type CommandMap = {
|
|||||||
args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null };
|
args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null };
|
||||||
result: MediaMetadata;
|
result: MediaMetadata;
|
||||||
};
|
};
|
||||||
get_engine_status: { args: undefined; result: EngineStatusResult };
|
get_engine_status: { args: undefined; result: EngineStatusResult };
|
||||||
|
get_aria2_engine_status: { args: undefined; result: EngineStatusItem };
|
||||||
|
get_ytdlp_engine_status: { args: undefined; result: EngineStatusItem };
|
||||||
|
get_ffmpeg_engine_status: { args: undefined; result: EngineStatusItem };
|
||||||
|
get_deno_engine_status: { args: undefined; result: EngineStatusItem };
|
||||||
open_file: { args: { path: string }; result: void };
|
open_file: { args: { path: string }; result: void };
|
||||||
show_in_folder: { args: { path: string }; result: void };
|
show_in_folder: { args: { path: string }; result: void };
|
||||||
reveal_in_file_manager: { args: { path: string }; result: void };
|
reveal_in_file_manager: { args: { path: string }; result: void };
|
||||||
|
|||||||
Reference in New Issue
Block a user