release: 4.2.2 add full settings autosave
This commit is contained in:
+241
-31
@@ -1,5 +1,11 @@
|
||||
let lastRuntimeMetricsOutput = '';
|
||||
let lastDebugLogOutput = '';
|
||||
let settingsAutoSaveBound = false;
|
||||
let settingsAutoSaveInFlight = false;
|
||||
let pendingSettingsAutoSave = false;
|
||||
let settingsAutoSaveTimer: number | null = null;
|
||||
let pendingCredentialsReconnect = false;
|
||||
let lastPersistedSettingsFingerprint = '';
|
||||
|
||||
function canRunSettingsAutoRefresh(): boolean {
|
||||
if (document.hidden) {
|
||||
@@ -287,39 +293,72 @@ function toggleDebugAutoRefresh(enabled: boolean): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
const clientId = byId<HTMLInputElement>('clientId').value.trim();
|
||||
const clientSecret = byId<HTMLInputElement>('clientSecret').value.trim();
|
||||
const downloadPath = byId<HTMLInputElement>('downloadPath').value;
|
||||
const downloadMode = byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full';
|
||||
const partMinutes = parseInt(byId<HTMLInputElement>('partMinutes').value, 10) || 120;
|
||||
const performanceMode = byId<HTMLSelectElement>('performanceMode').value as 'stability' | 'balanced' | 'speed';
|
||||
const smartQueueScheduler = byId<HTMLInputElement>('smartSchedulerToggle').checked;
|
||||
const duplicatePrevention = byId<HTMLInputElement>('duplicatePreventionToggle').checked;
|
||||
const metadataCacheMinutes = parseInt(byId<HTMLInputElement>('metadataCacheMinutes').value, 10) || 10;
|
||||
const vodFilenameTemplate = byId<HTMLInputElement>('vodFilenameTemplate').value.trim() || '{title}.mp4';
|
||||
const partsFilenameTemplate = byId<HTMLInputElement>('partsFilenameTemplate').value.trim() || '{date}_Part{part_padded}.mp4';
|
||||
const defaultClipFilenameTemplate = byId<HTMLInputElement>('defaultClipFilenameTemplate').value.trim() || '{date}_{part}.mp4';
|
||||
function collectCredentialsPayload(): Partial<AppConfig> {
|
||||
return {
|
||||
client_id: byId<HTMLInputElement>('clientId').value.trim(),
|
||||
client_secret: byId<HTMLInputElement>('clientSecret').value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
if (!validateFilenameTemplates(true)) {
|
||||
return;
|
||||
function collectDownloadSettingsPayload(): Partial<AppConfig> {
|
||||
return {
|
||||
download_mode: byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full',
|
||||
part_minutes: parseInt(byId<HTMLInputElement>('partMinutes').value, 10) || 120,
|
||||
performance_mode: byId<HTMLSelectElement>('performanceMode').value as 'stability' | 'balanced' | 'speed',
|
||||
smart_queue_scheduler: byId<HTMLInputElement>('smartSchedulerToggle').checked,
|
||||
prevent_duplicate_downloads: byId<HTMLInputElement>('duplicatePreventionToggle').checked,
|
||||
metadata_cache_minutes: parseInt(byId<HTMLInputElement>('metadataCacheMinutes').value, 10) || 10
|
||||
};
|
||||
}
|
||||
|
||||
function collectFilenameTemplatePayload(showAlert = false): Partial<AppConfig> | null {
|
||||
if (!validateFilenameTemplates(showAlert)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
config = await window.api.saveConfig({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
download_path: downloadPath,
|
||||
download_mode: downloadMode,
|
||||
part_minutes: partMinutes,
|
||||
performance_mode: performanceMode,
|
||||
smart_queue_scheduler: smartQueueScheduler,
|
||||
prevent_duplicate_downloads: duplicatePrevention,
|
||||
metadata_cache_minutes: metadataCacheMinutes,
|
||||
filename_template_vod: vodFilenameTemplate,
|
||||
filename_template_parts: partsFilenameTemplate,
|
||||
filename_template_clip: defaultClipFilenameTemplate
|
||||
});
|
||||
return {
|
||||
filename_template_vod: byId<HTMLInputElement>('vodFilenameTemplate').value.trim() || '{title}.mp4',
|
||||
filename_template_parts: byId<HTMLInputElement>('partsFilenameTemplate').value.trim() || '{date}_Part{part_padded}.mp4',
|
||||
filename_template_clip: byId<HTMLInputElement>('defaultClipFilenameTemplate').value.trim() || '{date}_{part}.mp4'
|
||||
};
|
||||
}
|
||||
|
||||
function collectAutoSavePayload(): Partial<AppConfig> {
|
||||
const payload: Partial<AppConfig> = {
|
||||
...collectCredentialsPayload(),
|
||||
...collectDownloadSettingsPayload()
|
||||
};
|
||||
|
||||
const templatePayload = collectFilenameTemplatePayload(false);
|
||||
if (templatePayload) {
|
||||
Object.assign(payload, templatePayload);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function getSettingsFingerprint(payload: Partial<AppConfig>): string {
|
||||
const effective = { ...config, ...payload };
|
||||
return JSON.stringify([
|
||||
effective.client_id ?? '',
|
||||
effective.client_secret ?? '',
|
||||
effective.download_mode ?? 'full',
|
||||
effective.part_minutes ?? 120,
|
||||
effective.performance_mode ?? 'balanced',
|
||||
effective.smart_queue_scheduler !== false,
|
||||
effective.prevent_duplicate_downloads !== false,
|
||||
effective.metadata_cache_minutes ?? 10,
|
||||
effective.filename_template_vod ?? '{title}.mp4',
|
||||
effective.filename_template_parts ?? '{date}_Part{part_padded}.mp4',
|
||||
effective.filename_template_clip ?? '{date}_{part}.mp4'
|
||||
]);
|
||||
}
|
||||
|
||||
function syncSettingsFormFromConfig(): void {
|
||||
byId<HTMLInputElement>('clientId').value = config.client_id ?? '';
|
||||
byId<HTMLInputElement>('clientSecret').value = config.client_secret ?? '';
|
||||
byId<HTMLSelectElement>('downloadMode').value = (config.download_mode as 'parts' | 'full') ?? 'full';
|
||||
byId<HTMLInputElement>('partMinutes').value = String((config.part_minutes as number) || 120);
|
||||
byId<HTMLSelectElement>('performanceMode').value = (config.performance_mode as string) || 'balanced';
|
||||
byId<HTMLInputElement>('smartSchedulerToggle').checked = (config.smart_queue_scheduler as boolean) !== false;
|
||||
byId<HTMLInputElement>('duplicatePreventionToggle').checked = (config.prevent_duplicate_downloads as boolean) !== false;
|
||||
@@ -328,9 +367,180 @@ async function saveSettings(): Promise<void> {
|
||||
byId<HTMLInputElement>('partsFilenameTemplate').value = (config.filename_template_parts as string) || '{date}_Part{part_padded}.mp4';
|
||||
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || '{date}_{part}.mp4';
|
||||
validateFilenameTemplates();
|
||||
lastPersistedSettingsFingerprint = getSettingsFingerprint({});
|
||||
}
|
||||
|
||||
await connect();
|
||||
await refreshRuntimeMetrics();
|
||||
async function persistSettings(options: {
|
||||
includeCredentials?: boolean;
|
||||
includeTemplates?: boolean;
|
||||
reconnectAfterSave?: boolean;
|
||||
showTemplateAlert?: boolean;
|
||||
} = {}): Promise<boolean> {
|
||||
const payload: Partial<AppConfig> = {
|
||||
...collectDownloadSettingsPayload()
|
||||
};
|
||||
|
||||
if (options.includeCredentials) {
|
||||
Object.assign(payload, collectCredentialsPayload());
|
||||
}
|
||||
|
||||
if (options.includeTemplates !== false) {
|
||||
const templatePayload = collectFilenameTemplatePayload(options.showTemplateAlert);
|
||||
if (!templatePayload) {
|
||||
return false;
|
||||
}
|
||||
Object.assign(payload, templatePayload);
|
||||
}
|
||||
|
||||
config = await window.api.saveConfig(payload);
|
||||
syncSettingsFormFromConfig();
|
||||
pendingCredentialsReconnect = false;
|
||||
|
||||
if (options.reconnectAfterSave) {
|
||||
await connect();
|
||||
}
|
||||
|
||||
if (canRunSettingsAutoRefresh()) {
|
||||
await refreshRuntimeMetrics(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function flushSettingsAutoSave(reconnectAfterSave = false): Promise<void> {
|
||||
if (settingsAutoSaveTimer) {
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
settingsAutoSaveTimer = null;
|
||||
}
|
||||
|
||||
const payload = collectAutoSavePayload();
|
||||
const fingerprint = getSettingsFingerprint(payload);
|
||||
|
||||
if (fingerprint === lastPersistedSettingsFingerprint) {
|
||||
if (reconnectAfterSave && pendingCredentialsReconnect) {
|
||||
pendingCredentialsReconnect = false;
|
||||
await connect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (settingsAutoSaveInFlight) {
|
||||
pendingSettingsAutoSave = true;
|
||||
return;
|
||||
}
|
||||
|
||||
settingsAutoSaveInFlight = true;
|
||||
try {
|
||||
config = await window.api.saveConfig(payload);
|
||||
lastPersistedSettingsFingerprint = getSettingsFingerprint({});
|
||||
if (reconnectAfterSave && pendingCredentialsReconnect) {
|
||||
pendingCredentialsReconnect = false;
|
||||
await connect();
|
||||
}
|
||||
} finally {
|
||||
settingsAutoSaveInFlight = false;
|
||||
if (pendingSettingsAutoSave) {
|
||||
pendingSettingsAutoSave = false;
|
||||
void flushSettingsAutoSave(pendingCredentialsReconnect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSettingsAutoSave(delayMs = 450): void {
|
||||
if (settingsAutoSaveTimer) {
|
||||
clearTimeout(settingsAutoSaveTimer);
|
||||
}
|
||||
|
||||
settingsAutoSaveTimer = window.setTimeout(() => {
|
||||
settingsAutoSaveTimer = null;
|
||||
void flushSettingsAutoSave(false);
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function initSettingsAutoSave(): void {
|
||||
if (settingsAutoSaveBound) {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsAutoSaveBound = true;
|
||||
syncSettingsFormFromConfig();
|
||||
|
||||
const immediateSaveIds = [
|
||||
'downloadMode',
|
||||
'performanceMode',
|
||||
'smartSchedulerToggle',
|
||||
'duplicatePreventionToggle'
|
||||
] as const;
|
||||
|
||||
const debouncedSaveIds = [
|
||||
'partMinutes',
|
||||
'metadataCacheMinutes',
|
||||
'vodFilenameTemplate',
|
||||
'partsFilenameTemplate',
|
||||
'defaultClipFilenameTemplate'
|
||||
] as const;
|
||||
|
||||
const credentialIds = [
|
||||
'clientId',
|
||||
'clientSecret'
|
||||
] as const;
|
||||
|
||||
const triggerImmediateSave = () => {
|
||||
void flushSettingsAutoSave(false);
|
||||
};
|
||||
|
||||
for (const id of immediateSaveIds) {
|
||||
const element = byId<HTMLInputElement | HTMLSelectElement>(id);
|
||||
element.addEventListener('change', triggerImmediateSave);
|
||||
element.addEventListener('blur', triggerImmediateSave);
|
||||
}
|
||||
|
||||
for (const id of debouncedSaveIds) {
|
||||
const element = byId<HTMLInputElement>(id);
|
||||
element.addEventListener('input', () => {
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
element.addEventListener('blur', () => {
|
||||
void flushSettingsAutoSave(false);
|
||||
});
|
||||
}
|
||||
|
||||
for (const id of credentialIds) {
|
||||
const element = byId<HTMLInputElement>(id);
|
||||
element.addEventListener('input', () => {
|
||||
pendingCredentialsReconnect = true;
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
element.addEventListener('blur', () => {
|
||||
pendingCredentialsReconnect = true;
|
||||
void flushSettingsAutoSave(true);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('blur', () => {
|
||||
if (settingsAutoSaveTimer || pendingCredentialsReconnect) {
|
||||
void flushSettingsAutoSave(pendingCredentialsReconnect);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && (settingsAutoSaveTimer || pendingCredentialsReconnect)) {
|
||||
void flushSettingsAutoSave(pendingCredentialsReconnect);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSettings(): Promise<void> {
|
||||
const saved = await persistSettings({
|
||||
includeCredentials: true,
|
||||
includeTemplates: true,
|
||||
reconnectAfterSave: true,
|
||||
showTemplateAlert: true
|
||||
});
|
||||
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFolder(): Promise<void> {
|
||||
|
||||
@@ -33,6 +33,7 @@ async function init(): Promise<void> {
|
||||
byId<HTMLInputElement>('vodFilenameTemplate').value = (config.filename_template_vod as string) || DEFAULT_VOD_TEMPLATE;
|
||||
byId<HTMLInputElement>('partsFilenameTemplate').value = (config.filename_template_parts as string) || DEFAULT_PARTS_TEMPLATE;
|
||||
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE;
|
||||
initSettingsAutoSave();
|
||||
|
||||
changeTheme(config.theme ?? 'twitch');
|
||||
renderStreamers();
|
||||
|
||||
Reference in New Issue
Block a user