release: 4.2.2 add full settings autosave
This commit is contained in:
parent
8d52df23c7
commit
1005b583bd
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.1",
|
"version": "4.2.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.1",
|
"version": "4.2.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.1",
|
"version": "4.2.2",
|
||||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"author": "xRangerDE",
|
"author": "xRangerDE",
|
||||||
|
|||||||
196
scripts/smoke-test-settings-autosave.js
Normal file
196
scripts/smoke-test-settings-autosave.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
const { _electron: electron } = require('playwright');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
|
||||||
|
const CONFIG_FILE = path.join(APPDATA_DIR, 'config.json');
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
client_id: '',
|
||||||
|
client_secret: '',
|
||||||
|
download_path: path.join(process.env.USERPROFILE || 'C:\\Users\\ploet', 'Desktop', 'Twitch_VODs'),
|
||||||
|
streamers: [],
|
||||||
|
theme: 'twitch',
|
||||||
|
download_mode: 'full',
|
||||||
|
part_minutes: 120,
|
||||||
|
language: 'en',
|
||||||
|
filename_template_vod: '{title}.mp4',
|
||||||
|
filename_template_parts: '{date}_Part{part_padded}.mp4',
|
||||||
|
filename_template_clip: '{date}_{part}.mp4',
|
||||||
|
smart_queue_scheduler: true,
|
||||||
|
performance_mode: 'balanced',
|
||||||
|
prevent_duplicate_downloads: true,
|
||||||
|
metadata_cache_minutes: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
function backupFile(filePath) {
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
return fs.readFileSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreFile(filePath, backup) {
|
||||||
|
if (backup === null) {
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||||
|
fs.writeFileSync(filePath, backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(config) {
|
||||||
|
fs.mkdirSync(path.dirname(CONFIG_FILE), { recursive: true });
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readConfig() {
|
||||||
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function launchApp() {
|
||||||
|
const electronPath = require('electron');
|
||||||
|
return electron.launch({
|
||||||
|
executablePath: electronPath,
|
||||||
|
args: ['.'],
|
||||||
|
cwd: process.cwd()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSettingsAndBlur(win, mode, partMinutes) {
|
||||||
|
await win.evaluate(async ({ mode, partMinutes }) => {
|
||||||
|
window.showTab('settings');
|
||||||
|
const modeField = document.getElementById('downloadMode');
|
||||||
|
const partField = document.getElementById('partMinutes');
|
||||||
|
|
||||||
|
modeField.value = mode;
|
||||||
|
modeField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
|
partField.focus();
|
||||||
|
partField.value = String(partMinutes);
|
||||||
|
partField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
partField.blur();
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
}, { mode, partMinutes });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setSettingsAndCloseImmediately(win, mode, partMinutes) {
|
||||||
|
await win.evaluate(({ mode, partMinutes }) => {
|
||||||
|
window.showTab('settings');
|
||||||
|
const modeField = document.getElementById('downloadMode');
|
||||||
|
const partField = document.getElementById('partMinutes');
|
||||||
|
|
||||||
|
modeField.value = mode;
|
||||||
|
modeField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
|
||||||
|
partField.focus();
|
||||||
|
partField.value = String(partMinutes);
|
||||||
|
partField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
|
}, { mode, partMinutes });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSettingsFromUi(win) {
|
||||||
|
return win.evaluate(() => {
|
||||||
|
window.showTab('settings');
|
||||||
|
return {
|
||||||
|
downloadMode: document.getElementById('downloadMode')?.value || '',
|
||||||
|
partMinutes: document.getElementById('partMinutes')?.value || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const configBackup = backupFile(CONFIG_FILE);
|
||||||
|
const baseConfig = configBackup ? { ...DEFAULT_CONFIG, ...JSON.parse(String(configBackup)) } : { ...DEFAULT_CONFIG };
|
||||||
|
|
||||||
|
let app = null;
|
||||||
|
try {
|
||||||
|
writeConfig({
|
||||||
|
...baseConfig,
|
||||||
|
client_id: '',
|
||||||
|
client_secret: '',
|
||||||
|
download_mode: 'full',
|
||||||
|
part_minutes: 120
|
||||||
|
});
|
||||||
|
|
||||||
|
app = await launchApp();
|
||||||
|
let win = await app.firstWindow();
|
||||||
|
await win.waitForTimeout(2200);
|
||||||
|
await setSettingsAndBlur(win, 'parts', 60);
|
||||||
|
await app.close();
|
||||||
|
app = null;
|
||||||
|
|
||||||
|
const afterBlurClose = readConfig();
|
||||||
|
|
||||||
|
app = await launchApp();
|
||||||
|
win = await app.firstWindow();
|
||||||
|
await win.waitForTimeout(2200);
|
||||||
|
const reopenedAfterBlur = await readSettingsFromUi(win);
|
||||||
|
await app.close();
|
||||||
|
app = null;
|
||||||
|
|
||||||
|
writeConfig({
|
||||||
|
...baseConfig,
|
||||||
|
client_id: '',
|
||||||
|
client_secret: '',
|
||||||
|
download_mode: 'full',
|
||||||
|
part_minutes: 120
|
||||||
|
});
|
||||||
|
|
||||||
|
app = await launchApp();
|
||||||
|
win = await app.firstWindow();
|
||||||
|
await win.waitForTimeout(2200);
|
||||||
|
await setSettingsAndCloseImmediately(win, 'parts', 75);
|
||||||
|
await app.close();
|
||||||
|
app = null;
|
||||||
|
|
||||||
|
const afterDirectClose = readConfig();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
afterBlurClose: {
|
||||||
|
config: {
|
||||||
|
download_mode: afterBlurClose.download_mode,
|
||||||
|
part_minutes: afterBlurClose.part_minutes
|
||||||
|
},
|
||||||
|
ui: reopenedAfterBlur
|
||||||
|
},
|
||||||
|
afterDirectClose: {
|
||||||
|
config: {
|
||||||
|
download_mode: afterDirectClose.download_mode,
|
||||||
|
part_minutes: afterDirectClose.part_minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
|
||||||
|
const blurCaseOk =
|
||||||
|
afterBlurClose.download_mode === 'parts' &&
|
||||||
|
afterBlurClose.part_minutes === 60 &&
|
||||||
|
reopenedAfterBlur.downloadMode === 'parts' &&
|
||||||
|
reopenedAfterBlur.partMinutes === '60';
|
||||||
|
|
||||||
|
const directCloseOk =
|
||||||
|
afterDirectClose.download_mode === 'parts' &&
|
||||||
|
afterDirectClose.part_minutes === 75;
|
||||||
|
|
||||||
|
process.exit(blurCaseOk && directCloseOk ? 0 : 1);
|
||||||
|
} finally {
|
||||||
|
if (app) {
|
||||||
|
try {
|
||||||
|
await app.close();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreFile(CONFIG_FILE, configBackup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@ -1,5 +1,11 @@
|
|||||||
let lastRuntimeMetricsOutput = '';
|
let lastRuntimeMetricsOutput = '';
|
||||||
let lastDebugLogOutput = '';
|
let lastDebugLogOutput = '';
|
||||||
|
let settingsAutoSaveBound = false;
|
||||||
|
let settingsAutoSaveInFlight = false;
|
||||||
|
let pendingSettingsAutoSave = false;
|
||||||
|
let settingsAutoSaveTimer: number | null = null;
|
||||||
|
let pendingCredentialsReconnect = false;
|
||||||
|
let lastPersistedSettingsFingerprint = '';
|
||||||
|
|
||||||
function canRunSettingsAutoRefresh(): boolean {
|
function canRunSettingsAutoRefresh(): boolean {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
@ -287,39 +293,72 @@ function toggleDebugAutoRefresh(enabled: boolean): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings(): Promise<void> {
|
function collectCredentialsPayload(): Partial<AppConfig> {
|
||||||
const clientId = byId<HTMLInputElement>('clientId').value.trim();
|
return {
|
||||||
const clientSecret = byId<HTMLInputElement>('clientSecret').value.trim();
|
client_id: byId<HTMLInputElement>('clientId').value.trim(),
|
||||||
const downloadPath = byId<HTMLInputElement>('downloadPath').value;
|
client_secret: byId<HTMLInputElement>('clientSecret').value.trim()
|
||||||
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';
|
|
||||||
|
|
||||||
if (!validateFilenameTemplates(true)) {
|
function collectDownloadSettingsPayload(): Partial<AppConfig> {
|
||||||
return;
|
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({
|
return {
|
||||||
client_id: clientId,
|
filename_template_vod: byId<HTMLInputElement>('vodFilenameTemplate').value.trim() || '{title}.mp4',
|
||||||
client_secret: clientSecret,
|
filename_template_parts: byId<HTMLInputElement>('partsFilenameTemplate').value.trim() || '{date}_Part{part_padded}.mp4',
|
||||||
download_path: downloadPath,
|
filename_template_clip: byId<HTMLInputElement>('defaultClipFilenameTemplate').value.trim() || '{date}_{part}.mp4'
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
|
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<HTMLSelectElement>('performanceMode').value = (config.performance_mode as string) || 'balanced';
|
||||||
byId<HTMLInputElement>('smartSchedulerToggle').checked = (config.smart_queue_scheduler as boolean) !== false;
|
byId<HTMLInputElement>('smartSchedulerToggle').checked = (config.smart_queue_scheduler as boolean) !== false;
|
||||||
byId<HTMLInputElement>('duplicatePreventionToggle').checked = (config.prevent_duplicate_downloads 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>('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';
|
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || '{date}_{part}.mp4';
|
||||||
validateFilenameTemplates();
|
validateFilenameTemplates();
|
||||||
|
lastPersistedSettingsFingerprint = getSettingsFingerprint({});
|
||||||
|
}
|
||||||
|
|
||||||
await connect();
|
async function persistSettings(options: {
|
||||||
await refreshRuntimeMetrics();
|
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> {
|
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>('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>('partsFilenameTemplate').value = (config.filename_template_parts as string) || DEFAULT_PARTS_TEMPLATE;
|
||||||
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE;
|
byId<HTMLInputElement>('defaultClipFilenameTemplate').value = (config.filename_template_clip as string) || DEFAULT_CLIP_TEMPLATE;
|
||||||
|
initSettingsAutoSave();
|
||||||
|
|
||||||
changeTheme(config.theme ?? 'twitch');
|
changeTheme(config.theme ?? 'twitch');
|
||||||
renderStreamers();
|
renderStreamers();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user