Compare commits
No commits in common. "f7cf1b8cd91926456680da864a92b005e713210d" and "97d8cc10ef4227e71fd8fc48ac594b2c52228dd6" have entirely different histories.
f7cf1b8cd9
...
97d8cc10ef
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.5",
|
"version": "4.6.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.6.5",
|
"version": "4.6.4",
|
||||||
"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.6.5",
|
"version": "4.6.4",
|
||||||
"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",
|
||||||
|
|||||||
@ -596,16 +596,6 @@
|
|||||||
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
|
<pre id="debugLogOutput" class="log-panel">Lade...</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-card">
|
|
||||||
<div class="form-row" style="align-items:center; justify-content:space-between; margin-bottom: 10px;">
|
|
||||||
<h3 id="storageCardTitle" style="margin:0;">Storage</h3>
|
|
||||||
<button class="btn-secondary" id="btnRefreshStorage" onclick="refreshStorageStats()">Aktualisieren</button>
|
|
||||||
</div>
|
|
||||||
<p id="storageCardIntro" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">Disk-Verbrauch pro Streamer im aktuellen Download-Ordner. Live-Aufnahmen werden separat ausgewiesen.</p>
|
|
||||||
<div id="storageSummary" style="color: var(--text-secondary); font-size:12px; margin-bottom:8px;"></div>
|
|
||||||
<div id="storageList"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="discordCardTitle">Discord-Webhook</h3>
|
<h3 id="discordCardTitle">Discord-Webhook</h3>
|
||||||
<p id="discordCardIntro" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">Sende Benachrichtigungen an einen Discord-Channel via Webhook — nuetzlich fuer Multi-Device-Setups oder eine dedizierte Archiv-Maschine.</p>
|
<p id="discordCardIntro" style="color: var(--text-secondary); font-size:13px; margin-bottom:12px; line-height:1.5;">Sende Benachrichtigungen an einen Discord-Channel via Webhook — nuetzlich fuer Multi-Device-Setups oder eine dedizierte Archiv-Maschine.</p>
|
||||||
|
|||||||
124
src/main.ts
124
src/main.ts
@ -3090,126 +3090,6 @@ function chatReplayPathFor(vodFilePath: string): string {
|
|||||||
return `${base}.chat.json`;
|
return `${base}.chat.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// STORAGE STATS
|
|
||||||
// ==========================================
|
|
||||||
// Walks the download folder once on demand and reports per-streamer disk
|
|
||||||
// usage so the user can see which streamers are eating their archive
|
|
||||||
// budget. Only enumerates direct subfolders that match a known streamer
|
|
||||||
// name (from config.streamers) plus a special "Clips" bucket. Refusing
|
|
||||||
// to recurse the entire filesystem means a user with a huge unrelated
|
|
||||||
// download_path doesn't pay for it here.
|
|
||||||
interface StreamerStorageEntry {
|
|
||||||
name: string;
|
|
||||||
fileCount: number;
|
|
||||||
totalBytes: number;
|
|
||||||
liveBytes: number;
|
|
||||||
chatBytes: number;
|
|
||||||
folderPath: string;
|
|
||||||
}
|
|
||||||
interface StorageStatsResult {
|
|
||||||
downloadPath: string;
|
|
||||||
rootExists: boolean;
|
|
||||||
freeBytes: number | null;
|
|
||||||
totalFiles: number;
|
|
||||||
totalBytes: number;
|
|
||||||
streamers: StreamerStorageEntry[];
|
|
||||||
extras: StreamerStorageEntry[];
|
|
||||||
scannedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function walkFolderForStats(folderPath: string): { files: number; bytes: number; liveBytes: number; chatBytes: number } {
|
|
||||||
const result = { files: 0, bytes: 0, liveBytes: 0, chatBytes: 0 };
|
|
||||||
let entries: fs.Dirent[];
|
|
||||||
try {
|
|
||||||
entries = fs.readdirSync(folderPath, { withFileTypes: true });
|
|
||||||
} catch {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
for (const entry of entries) {
|
|
||||||
const full = path.join(folderPath, entry.name);
|
|
||||||
try {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const sub = walkFolderForStats(full);
|
|
||||||
result.files += sub.files;
|
|
||||||
result.bytes += sub.bytes;
|
|
||||||
if (entry.name === 'live') {
|
|
||||||
result.liveBytes += sub.bytes;
|
|
||||||
}
|
|
||||||
} else if (entry.isFile()) {
|
|
||||||
const st = fs.statSync(full);
|
|
||||||
result.files += 1;
|
|
||||||
result.bytes += st.size;
|
|
||||||
if (/\.chat\.json(l)?$/i.test(entry.name)) {
|
|
||||||
result.chatBytes += st.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Symlink / permissions blip — skip the entry, continue.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function computeStorageStats(): StorageStatsResult {
|
|
||||||
const root = config.download_path;
|
|
||||||
const result: StorageStatsResult = {
|
|
||||||
downloadPath: root,
|
|
||||||
rootExists: false,
|
|
||||||
freeBytes: null,
|
|
||||||
totalFiles: 0,
|
|
||||||
totalBytes: 0,
|
|
||||||
streamers: [],
|
|
||||||
extras: [],
|
|
||||||
scannedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!root || !fs.existsSync(root)) return result;
|
|
||||||
result.rootExists = true;
|
|
||||||
result.freeBytes = getFreeDiskBytes(root);
|
|
||||||
|
|
||||||
const knownStreamers = new Set<string>(
|
|
||||||
((config.streamers as string[]) || []).map((s) => s.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
let topEntries: fs.Dirent[];
|
|
||||||
try {
|
|
||||||
topEntries = fs.readdirSync(root, { withFileTypes: true });
|
|
||||||
} catch {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of topEntries) {
|
|
||||||
if (!entry.isDirectory()) continue;
|
|
||||||
const full = path.join(root, entry.name);
|
|
||||||
const safeName = entry.name.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
||||||
const isKnownStreamer = knownStreamers.has(safeName.toLowerCase());
|
|
||||||
// Treat Clips/ + anything that matches known streamers as a tracked
|
|
||||||
// bucket; everything else (random user folders) lives in `extras`.
|
|
||||||
const sub = walkFolderForStats(full);
|
|
||||||
const stats: StreamerStorageEntry = {
|
|
||||||
name: entry.name,
|
|
||||||
fileCount: sub.files,
|
|
||||||
totalBytes: sub.bytes,
|
|
||||||
liveBytes: sub.liveBytes,
|
|
||||||
chatBytes: sub.chatBytes,
|
|
||||||
folderPath: full
|
|
||||||
};
|
|
||||||
if (isKnownStreamer || entry.name === 'Clips') {
|
|
||||||
result.streamers.push(stats);
|
|
||||||
} else {
|
|
||||||
result.extras.push(stats);
|
|
||||||
}
|
|
||||||
result.totalFiles += sub.files;
|
|
||||||
result.totalBytes += sub.bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Largest first — that's what the user wants to see.
|
|
||||||
result.streamers.sort((a, b) => b.totalBytes - a.totalBytes);
|
|
||||||
result.extras.sort((a, b) => b.totalBytes - a.totalBytes);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// DISCORD WEBHOOK NOTIFICATIONS
|
// DISCORD WEBHOOK NOTIFICATIONS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -5260,10 +5140,6 @@ ipcMain.handle('open-debug-log-file', (): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-storage-stats', (): StorageStatsResult => {
|
|
||||||
return computeStorageStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('check-folder-writable', (_, folderPath: string): boolean => {
|
ipcMain.handle('check-folder-writable', (_, folderPath: string): boolean => {
|
||||||
if (typeof folderPath !== 'string' || !folderPath) return false;
|
if (typeof folderPath !== 'string' || !folderPath) return false;
|
||||||
return isDownloadPathWritable(folderPath);
|
return isDownloadPathWritable(folderPath);
|
||||||
|
|||||||
@ -89,7 +89,6 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
showInFolder: (path: string) => ipcRenderer.invoke('show-in-folder', path),
|
showInFolder: (path: string) => ipcRenderer.invoke('show-in-folder', path),
|
||||||
openDebugLogFile: () => ipcRenderer.invoke('open-debug-log-file'),
|
openDebugLogFile: () => ipcRenderer.invoke('open-debug-log-file'),
|
||||||
checkFolderWritable: (path: string) => ipcRenderer.invoke('check-folder-writable', path),
|
checkFolderWritable: (path: string) => ipcRenderer.invoke('check-folder-writable', path),
|
||||||
getStorageStats: () => ipcRenderer.invoke('get-storage-stats'),
|
|
||||||
|
|
||||||
// Video Cutter
|
// Video Cutter
|
||||||
getVideoInfo: (filePath: string): Promise<VideoInfo | null> => ipcRenderer.invoke('get-video-info', filePath),
|
getVideoInfo: (filePath: string): Promise<VideoInfo | null> => ipcRenderer.invoke('get-video-info', filePath),
|
||||||
|
|||||||
20
src/renderer-globals.d.ts
vendored
20
src/renderer-globals.d.ts
vendored
@ -189,25 +189,6 @@ interface PreflightResult {
|
|||||||
timestamp: string;
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StreamerStorageEntry {
|
|
||||||
name: string;
|
|
||||||
fileCount: number;
|
|
||||||
totalBytes: number;
|
|
||||||
liveBytes: number;
|
|
||||||
chatBytes: number;
|
|
||||||
folderPath: string;
|
|
||||||
}
|
|
||||||
interface StorageStatsResult {
|
|
||||||
downloadPath: string;
|
|
||||||
rootExists: boolean;
|
|
||||||
freeBytes: number | null;
|
|
||||||
totalFiles: number;
|
|
||||||
totalBytes: number;
|
|
||||||
streamers: StreamerStorageEntry[];
|
|
||||||
extras: StreamerStorageEntry[];
|
|
||||||
scannedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiBridge {
|
interface ApiBridge {
|
||||||
getConfig(): Promise<AppConfig>;
|
getConfig(): Promise<AppConfig>;
|
||||||
saveConfig(config: Partial<AppConfig>): Promise<AppConfig>;
|
saveConfig(config: Partial<AppConfig>): Promise<AppConfig>;
|
||||||
@ -237,7 +218,6 @@ interface ApiBridge {
|
|||||||
showInFolder(path: string): Promise<boolean>;
|
showInFolder(path: string): Promise<boolean>;
|
||||||
openDebugLogFile(): Promise<boolean>;
|
openDebugLogFile(): Promise<boolean>;
|
||||||
checkFolderWritable(path: string): Promise<boolean>;
|
checkFolderWritable(path: string): Promise<boolean>;
|
||||||
getStorageStats(): Promise<StorageStatsResult>;
|
|
||||||
getVideoInfo(filePath: string): Promise<VideoInfo | null>;
|
getVideoInfo(filePath: string): Promise<VideoInfo | null>;
|
||||||
extractFrame(filePath: string, timeSeconds: number): Promise<string | null>;
|
extractFrame(filePath: string, timeSeconds: number): Promise<string | null>;
|
||||||
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;
|
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;
|
||||||
|
|||||||
@ -54,19 +54,6 @@ const UI_TEXT_DE = {
|
|||||||
apiHelpIntro: 'Du brauchst eine Client-ID und ein Client-Secret von Twitch.',
|
apiHelpIntro: 'Du brauchst eine Client-ID und ein Client-Secret von Twitch.',
|
||||||
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
||||||
openDebugLogFile: 'Log-Datei oeffnen',
|
openDebugLogFile: 'Log-Datei oeffnen',
|
||||||
storageCardTitle: 'Speicher',
|
|
||||||
storageCardIntro: 'Disk-Verbrauch pro Streamer im aktuellen Download-Ordner. Live-Aufnahmen werden separat ausgewiesen.',
|
|
||||||
storageRefresh: 'Aktualisieren',
|
|
||||||
storageEmpty: 'Download-Ordner ist leer oder nicht lesbar.',
|
|
||||||
storageScanning: 'Scanne...',
|
|
||||||
storageSummary: 'Gesamt: {files} Dateien, {size} — Freier Speicher: {free}',
|
|
||||||
storageColumnFolder: 'Ordner',
|
|
||||||
storageColumnFiles: 'Dateien',
|
|
||||||
storageColumnTotal: 'Gesamt',
|
|
||||||
storageColumnLive: 'Live',
|
|
||||||
storageColumnChat: 'Chat',
|
|
||||||
storageOpen: 'Oeffnen',
|
|
||||||
storageOtherFolders: 'Andere Ordner im Download-Pfad',
|
|
||||||
discordCardTitle: 'Discord-Webhook',
|
discordCardTitle: 'Discord-Webhook',
|
||||||
discordCardIntro: 'Sende Benachrichtigungen an einen Discord-Channel via Webhook - nuetzlich fuer Multi-Device-Setups oder eine dedizierte Archiv-Maschine.',
|
discordCardIntro: 'Sende Benachrichtigungen an einen Discord-Channel via Webhook - nuetzlich fuer Multi-Device-Setups oder eine dedizierte Archiv-Maschine.',
|
||||||
discordWebhookUrlLabel: 'Webhook-URL',
|
discordWebhookUrlLabel: 'Webhook-URL',
|
||||||
|
|||||||
@ -54,19 +54,6 @@ const UI_TEXT_EN = {
|
|||||||
apiHelpIntro: 'You need a Client ID and Client Secret from Twitch.',
|
apiHelpIntro: 'You need a Client ID and Client Secret from Twitch.',
|
||||||
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
apiHelpLinkText: 'dev.twitch.tv/console/apps',
|
||||||
openDebugLogFile: 'Open log file',
|
openDebugLogFile: 'Open log file',
|
||||||
storageCardTitle: 'Storage',
|
|
||||||
storageCardIntro: 'Per-streamer disk usage in the current download folder. Live recordings are surfaced separately.',
|
|
||||||
storageRefresh: 'Refresh',
|
|
||||||
storageEmpty: 'Download folder is empty or unreadable.',
|
|
||||||
storageScanning: 'Scanning...',
|
|
||||||
storageSummary: 'Total: {files} files, {size} — Free disk: {free}',
|
|
||||||
storageColumnFolder: 'Folder',
|
|
||||||
storageColumnFiles: 'Files',
|
|
||||||
storageColumnTotal: 'Total',
|
|
||||||
storageColumnLive: 'Live',
|
|
||||||
storageColumnChat: 'Chat',
|
|
||||||
storageOpen: 'Open',
|
|
||||||
storageOtherFolders: 'Other folders in download path',
|
|
||||||
discordCardTitle: 'Discord webhook',
|
discordCardTitle: 'Discord webhook',
|
||||||
discordCardIntro: 'Send notifications to a Discord channel via webhook — handy for multi-device setups or a dedicated archive machine.',
|
discordCardIntro: 'Send notifications to a Discord channel via webhook — handy for multi-device setups or a dedicated archive machine.',
|
||||||
discordWebhookUrlLabel: 'Webhook URL',
|
discordWebhookUrlLabel: 'Webhook URL',
|
||||||
|
|||||||
@ -265,125 +265,6 @@ async function runPreflight(autoFix = false): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshStorageStats(): Promise<void> {
|
|
||||||
const summary = byId('storageSummary');
|
|
||||||
const list = byId('storageList');
|
|
||||||
const btn = byId<HTMLButtonElement>('btnRefreshStorage');
|
|
||||||
const old = btn.textContent || '';
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = UI_TEXT.static.storageScanning;
|
|
||||||
summary.textContent = UI_TEXT.static.storageScanning;
|
|
||||||
list.replaceChildren();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stats = await window.api.getStorageStats();
|
|
||||||
renderStorageStats(stats);
|
|
||||||
} catch {
|
|
||||||
summary.textContent = UI_TEXT.static.storageEmpty;
|
|
||||||
} finally {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = old || UI_TEXT.static.storageRefresh;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStorageStats(stats: StorageStatsResult): void {
|
|
||||||
const summary = byId('storageSummary');
|
|
||||||
const list = byId('storageList');
|
|
||||||
|
|
||||||
if (!stats.rootExists) {
|
|
||||||
summary.textContent = UI_TEXT.static.storageEmpty;
|
|
||||||
list.replaceChildren();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.textContent = UI_TEXT.static.storageSummary
|
|
||||||
.replace('{files}', String(stats.totalFiles))
|
|
||||||
.replace('{size}', formatBytesForMetrics(stats.totalBytes))
|
|
||||||
.replace('{free}', stats.freeBytes !== null ? formatBytesForMetrics(stats.freeBytes) : '-');
|
|
||||||
|
|
||||||
list.replaceChildren();
|
|
||||||
if (stats.streamers.length === 0 && stats.extras.length === 0) return;
|
|
||||||
|
|
||||||
const buildTable = (rows: StreamerStorageEntry[]): HTMLTableElement => {
|
|
||||||
const table = document.createElement('table');
|
|
||||||
table.style.width = '100%';
|
|
||||||
table.style.borderCollapse = 'collapse';
|
|
||||||
table.style.fontSize = '12px';
|
|
||||||
|
|
||||||
const thead = document.createElement('thead');
|
|
||||||
const headRow = document.createElement('tr');
|
|
||||||
const headers = [
|
|
||||||
UI_TEXT.static.storageColumnFolder,
|
|
||||||
UI_TEXT.static.storageColumnFiles,
|
|
||||||
UI_TEXT.static.storageColumnTotal,
|
|
||||||
UI_TEXT.static.storageColumnLive,
|
|
||||||
UI_TEXT.static.storageColumnChat,
|
|
||||||
''
|
|
||||||
];
|
|
||||||
for (const h of headers) {
|
|
||||||
const th = document.createElement('th');
|
|
||||||
th.textContent = h;
|
|
||||||
th.style.textAlign = 'left';
|
|
||||||
th.style.padding = '4px 8px';
|
|
||||||
th.style.color = 'var(--text-secondary)';
|
|
||||||
th.style.borderBottom = '1px solid var(--border-soft)';
|
|
||||||
th.style.fontWeight = '500';
|
|
||||||
headRow.appendChild(th);
|
|
||||||
}
|
|
||||||
thead.appendChild(headRow);
|
|
||||||
table.appendChild(thead);
|
|
||||||
|
|
||||||
const tbody = document.createElement('tbody');
|
|
||||||
for (const row of rows) {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
const cells: Array<string | HTMLElement> = [
|
|
||||||
row.name,
|
|
||||||
String(row.fileCount),
|
|
||||||
formatBytesForMetrics(row.totalBytes),
|
|
||||||
row.liveBytes > 0 ? formatBytesForMetrics(row.liveBytes) : '-',
|
|
||||||
row.chatBytes > 0 ? formatBytesForMetrics(row.chatBytes) : '-'
|
|
||||||
];
|
|
||||||
for (const c of cells) {
|
|
||||||
const td = document.createElement('td');
|
|
||||||
if (typeof c === 'string') td.textContent = c;
|
|
||||||
else td.appendChild(c);
|
|
||||||
td.style.padding = '4px 8px';
|
|
||||||
td.style.borderBottom = '1px solid var(--border-soft)';
|
|
||||||
tr.appendChild(td);
|
|
||||||
}
|
|
||||||
const openCell = document.createElement('td');
|
|
||||||
openCell.style.padding = '4px 8px';
|
|
||||||
openCell.style.borderBottom = '1px solid var(--border-soft)';
|
|
||||||
const openBtn = document.createElement('button');
|
|
||||||
openBtn.textContent = UI_TEXT.static.storageOpen;
|
|
||||||
openBtn.className = 'btn-secondary';
|
|
||||||
openBtn.style.fontSize = '11px';
|
|
||||||
openBtn.style.padding = '2px 8px';
|
|
||||||
openBtn.addEventListener('click', () => {
|
|
||||||
void window.api.openFolder(row.folderPath);
|
|
||||||
});
|
|
||||||
openCell.appendChild(openBtn);
|
|
||||||
tr.appendChild(openCell);
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
}
|
|
||||||
table.appendChild(tbody);
|
|
||||||
return table;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stats.streamers.length > 0) {
|
|
||||||
list.appendChild(buildTable(stats.streamers));
|
|
||||||
}
|
|
||||||
if (stats.extras.length > 0) {
|
|
||||||
const heading = document.createElement('div');
|
|
||||||
heading.textContent = UI_TEXT.static.storageOtherFolders;
|
|
||||||
heading.style.color = 'var(--text-secondary)';
|
|
||||||
heading.style.fontSize = '12px';
|
|
||||||
heading.style.margin = '12px 0 4px';
|
|
||||||
list.appendChild(heading);
|
|
||||||
list.appendChild(buildTable(stats.extras));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function exportConfigToFile(): Promise<void> {
|
async function exportConfigToFile(): Promise<void> {
|
||||||
const result = await window.api.exportConfig();
|
const result = await window.api.exportConfig();
|
||||||
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
const toast = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
||||||
|
|||||||
@ -173,9 +173,6 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
|
setText('debugLogTitle', UI_TEXT.static.debugLogTitle);
|
||||||
setText('btnRefreshLog', UI_TEXT.static.refreshLog);
|
setText('btnRefreshLog', UI_TEXT.static.refreshLog);
|
||||||
setText('btnOpenDebugLogFile', UI_TEXT.static.openDebugLogFile);
|
setText('btnOpenDebugLogFile', UI_TEXT.static.openDebugLogFile);
|
||||||
setText('storageCardTitle', UI_TEXT.static.storageCardTitle);
|
|
||||||
setText('storageCardIntro', UI_TEXT.static.storageCardIntro);
|
|
||||||
setText('btnRefreshStorage', UI_TEXT.static.storageRefresh);
|
|
||||||
setText('discordCardTitle', UI_TEXT.static.discordCardTitle);
|
setText('discordCardTitle', UI_TEXT.static.discordCardTitle);
|
||||||
setText('discordCardIntro', UI_TEXT.static.discordCardIntro);
|
setText('discordCardIntro', UI_TEXT.static.discordCardIntro);
|
||||||
setText('discordWebhookUrlLabel', UI_TEXT.static.discordWebhookUrlLabel);
|
setText('discordWebhookUrlLabel', UI_TEXT.static.discordWebhookUrlLabel);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user