release: 4.2.3 improve updates and startup UX
This commit is contained in:
parent
1005b583bd
commit
b7cd8fbec2
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.2",
|
"version": "4.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "twitch-vod-manager",
|
"name": "twitch-vod-manager",
|
||||||
"version": "4.2.2",
|
"version": "4.2.3",
|
||||||
"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.2",
|
"version": "4.2.3",
|
||||||
"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",
|
||||||
|
|||||||
@ -18,6 +18,32 @@
|
|||||||
<button id="updateButton" onclick="downloadUpdate()">Jetzt herunterladen</button>
|
<button id="updateButton" onclick="downloadUpdate()">Jetzt herunterladen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="updateModal" onclick="handleUpdateModalOverlayClick(event)">
|
||||||
|
<div class="modal update-modal">
|
||||||
|
<button class="modal-close" onclick="dismissUpdateModal()">x</button>
|
||||||
|
<div class="update-modal-eyebrow" id="updateModalEyebrow">Updates</div>
|
||||||
|
<h2 id="updateModalTitle">Update verfugbar</h2>
|
||||||
|
<p class="update-modal-message" id="updateModalMessage">Version 0.0.0 ist verfugbar. Jetzt herunterladen?</p>
|
||||||
|
<div class="update-modal-meta" id="updateModalMeta" style="display:none;"></div>
|
||||||
|
|
||||||
|
<div class="update-changelog-card" id="updateChangelogCard" style="display:none;">
|
||||||
|
<div class="update-changelog-header">
|
||||||
|
<span class="update-changelog-label" id="updateChangelogLabel">Changelog</span>
|
||||||
|
<button type="button" class="update-changelog-toggle" id="updateChangelogToggle" onclick="toggleUpdateChangelog()">Changelog anzeigen</button>
|
||||||
|
</div>
|
||||||
|
<div class="update-changelog-panel" id="updateChangelogPanel" hidden>
|
||||||
|
<div class="update-changelog-content" id="updateChangelogContent"></div>
|
||||||
|
<p class="update-changelog-empty" id="updateChangelogEmpty" hidden>Kein Changelog verfugbar.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions update-modal-actions">
|
||||||
|
<button class="btn-secondary" id="updateModalDismissBtn" type="button" onclick="dismissUpdateModal()">Nein</button>
|
||||||
|
<button class="btn-primary" id="updateModalConfirmBtn" type="button" onclick="confirmUpdateModal()">Ja, herunterladen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Clip Dialog Modal -->
|
<!-- Clip Dialog Modal -->
|
||||||
<div class="modal-overlay" id="clipModal">
|
<div class="modal-overlay" id="clipModal">
|
||||||
<div class="modal" style="background: #2b2b2b; max-width: 500px;">
|
<div class="modal" style="background: #2b2b2b; max-width: 500px;">
|
||||||
@ -182,7 +208,6 @@
|
|||||||
<div class="queue-section">
|
<div class="queue-section">
|
||||||
<div class="queue-header">
|
<div class="queue-header">
|
||||||
<span class="queue-title" id="queueTitleText">Warteschlange</span>
|
<span class="queue-title" id="queueTitleText">Warteschlange</span>
|
||||||
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
|
|
||||||
<span class="queue-count" id="queueCount">0</span>
|
<span class="queue-count" id="queueCount">0</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="queue-list" id="queueList"></div>
|
<div class="queue-list" id="queueList"></div>
|
||||||
@ -462,7 +487,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3 id="preflightTitle">System-Check</h3>
|
<div class="form-row" style="align-items:center; justify-content:space-between; margin-bottom: 10px;">
|
||||||
|
<h3 id="preflightTitle" style="margin: 0;">System-Check</h3>
|
||||||
|
<span class="health-badge unknown" id="healthBadge">System: Unbekannt</span>
|
||||||
|
</div>
|
||||||
<div class="form-row" style="margin-bottom: 10px;">
|
<div class="form-row" style="margin-bottom: 10px;">
|
||||||
<button class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
|
<button class="btn-secondary" id="btnPreflightRun" onclick="runPreflight(false)">Check ausfuhren</button>
|
||||||
<button class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
|
<button class="btn-secondary" id="btnPreflightFix" onclick="runPreflight(true)">Auto-Fix Tools</button>
|
||||||
|
|||||||
167
src/main.ts
167
src/main.ts
@ -42,7 +42,7 @@ const DEBUG_LOG_TRIM_TO_BYTES = 4 * 1024 * 1024;
|
|||||||
const AUTO_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
const AUTO_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
||||||
const AUTO_UPDATE_STARTUP_CHECK_DELAY_MS = 5000;
|
const AUTO_UPDATE_STARTUP_CHECK_DELAY_MS = 5000;
|
||||||
const AUTO_UPDATE_MIN_CHECK_GAP_MS = 45 * 1000;
|
const AUTO_UPDATE_MIN_CHECK_GAP_MS = 45 * 1000;
|
||||||
const AUTO_UPDATE_AUTO_DOWNLOAD = true;
|
const AUTO_UPDATE_AUTO_DOWNLOAD = false;
|
||||||
const AUTO_UPDATE_CHECK_TIMEOUT_MS = 30 * 1000;
|
const AUTO_UPDATE_CHECK_TIMEOUT_MS = 30 * 1000;
|
||||||
const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000;
|
||||||
const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096;
|
const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096;
|
||||||
@ -212,6 +212,14 @@ interface VideoInfo {
|
|||||||
fps: number;
|
fps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReleaseUpdateInfo {
|
||||||
|
tagName?: string;
|
||||||
|
version?: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
releaseName?: string;
|
||||||
|
releaseNotes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// CONFIG MANAGEMENT
|
// CONFIG MANAGEMENT
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -398,6 +406,8 @@ let bundledFFprobePath: string | null = null;
|
|||||||
let streamlinkPathCache: string | null = null;
|
let streamlinkPathCache: string | null = null;
|
||||||
let ffmpegPathCache: string | null = null;
|
let ffmpegPathCache: string | null = null;
|
||||||
let ffprobePathCache: string | null = null;
|
let ffprobePathCache: string | null = null;
|
||||||
|
let verifiedStreamlinkCommandKey: string | null = null;
|
||||||
|
let verifiedFfmpegCommandKey: string | null = null;
|
||||||
let bundledToolPathSignature = '';
|
let bundledToolPathSignature = '';
|
||||||
let bundledToolPathRefreshedAt = 0;
|
let bundledToolPathRefreshedAt = 0;
|
||||||
let debugLogFlushTimer: NodeJS.Timeout | null = null;
|
let debugLogFlushTimer: NodeJS.Timeout | null = null;
|
||||||
@ -411,6 +421,7 @@ let autoUpdateDownloadInProgress = false;
|
|||||||
let lastAutoUpdateCheckAt = 0;
|
let lastAutoUpdateCheckAt = 0;
|
||||||
let latestKnownUpdateVersion: string | null = null;
|
let latestKnownUpdateVersion: string | null = null;
|
||||||
let downloadedUpdateVersion: string | null = null;
|
let downloadedUpdateVersion: string | null = null;
|
||||||
|
let latestReleaseUpdateInfo: ReleaseUpdateInfo | null = null;
|
||||||
let twitchLoginInFlight: Promise<boolean> | null = null;
|
let twitchLoginInFlight: Promise<boolean> | null = null;
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@ -510,11 +521,17 @@ async function runPreflight(autoFix = false): Promise<PreflightResult> {
|
|||||||
|
|
||||||
const streamlinkCmd = getStreamlinkCommand();
|
const streamlinkCmd = getStreamlinkCommand();
|
||||||
checks.streamlink = canExecuteCommand(streamlinkCmd.command, [...streamlinkCmd.prefixArgs, '--version']);
|
checks.streamlink = canExecuteCommand(streamlinkCmd.command, [...streamlinkCmd.prefixArgs, '--version']);
|
||||||
|
if (checks.streamlink) {
|
||||||
|
cacheVerifiedStreamlinkCommand(streamlinkCmd.command, [...streamlinkCmd.prefixArgs, '--version']);
|
||||||
|
}
|
||||||
|
|
||||||
const ffmpegPath = getFFmpegPath();
|
const ffmpegPath = getFFmpegPath();
|
||||||
const ffprobePath = getFFprobePath();
|
const ffprobePath = getFFprobePath();
|
||||||
checks.ffmpeg = canExecuteCommand(ffmpegPath, ['-version']);
|
checks.ffmpeg = canExecuteCommand(ffmpegPath, ['-version']);
|
||||||
checks.ffprobe = canExecuteCommand(ffprobePath, ['-version']);
|
checks.ffprobe = canExecuteCommand(ffprobePath, ['-version']);
|
||||||
|
if (checks.ffmpeg && checks.ffprobe) {
|
||||||
|
cacheVerifiedFfmpegCommands(ffmpegPath, ffprobePath);
|
||||||
|
}
|
||||||
|
|
||||||
const messages: string[] = [];
|
const messages: string[] = [];
|
||||||
if (!checks.internet) messages.push('Keine Internetverbindung erkannt.');
|
if (!checks.internet) messages.push('Keine Internetverbindung erkannt.');
|
||||||
@ -673,6 +690,31 @@ function canExecuteCommand(command: string, args: string[]): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCommandCacheKey(command: string, args: string[]): string {
|
||||||
|
return [command, ...args].join('\u0000');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheVerifiedStreamlinkCommand(command: string, args: string[]): void {
|
||||||
|
verifiedStreamlinkCommandKey = getCommandCacheKey(command, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVerifiedStreamlinkCommand(command: string, args: string[]): boolean {
|
||||||
|
return verifiedStreamlinkCommandKey === getCommandCacheKey(command, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheVerifiedFfmpegCommands(ffmpegCommand: string, ffprobeCommand: string): void {
|
||||||
|
verifiedFfmpegCommandKey = getCommandCacheKey(ffmpegCommand, [ffprobeCommand]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVerifiedFfmpegCommands(ffmpegCommand: string, ffprobeCommand: string): boolean {
|
||||||
|
return verifiedFfmpegCommandKey === getCommandCacheKey(ffmpegCommand, [ffprobeCommand]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateVerifiedToolCaches(): void {
|
||||||
|
verifiedStreamlinkCommandKey = null;
|
||||||
|
verifiedFfmpegCommandKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
function findFileRecursive(rootDir: string, fileName: string): string | null {
|
function findFileRecursive(rootDir: string, fileName: string): string | null {
|
||||||
if (!fs.existsSync(rootDir)) return null;
|
if (!fs.existsSync(rootDir)) return null;
|
||||||
|
|
||||||
@ -729,6 +771,7 @@ function refreshBundledToolPaths(force = false): void {
|
|||||||
ffmpegPathCache = null;
|
ffmpegPathCache = null;
|
||||||
ffprobePathCache = null;
|
ffprobePathCache = null;
|
||||||
streamlinkCommandCache = null;
|
streamlinkCommandCache = null;
|
||||||
|
invalidateVerifiedToolCaches();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,7 +834,13 @@ async function ensureStreamlinkInstalled(): Promise<boolean> {
|
|||||||
refreshBundledToolPaths();
|
refreshBundledToolPaths();
|
||||||
|
|
||||||
const current = getStreamlinkCommand();
|
const current = getStreamlinkCommand();
|
||||||
if (canExecuteCommand(current.command, [...current.prefixArgs, '--version'])) {
|
const versionArgs = [...current.prefixArgs, '--version'];
|
||||||
|
if (isVerifiedStreamlinkCommand(current.command, versionArgs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canExecuteCommand(current.command, versionArgs)) {
|
||||||
|
cacheVerifiedStreamlinkCommand(current.command, versionArgs);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -833,7 +882,11 @@ async function ensureStreamlinkInstalled(): Promise<boolean> {
|
|||||||
streamlinkCommandCache = null;
|
streamlinkCommandCache = null;
|
||||||
|
|
||||||
const cmd = getStreamlinkCommand();
|
const cmd = getStreamlinkCommand();
|
||||||
const works = canExecuteCommand(cmd.command, [...cmd.prefixArgs, '--version']);
|
const installedVersionArgs = [...cmd.prefixArgs, '--version'];
|
||||||
|
const works = canExecuteCommand(cmd.command, installedVersionArgs);
|
||||||
|
if (works) {
|
||||||
|
cacheVerifiedStreamlinkCommand(cmd.command, installedVersionArgs);
|
||||||
|
}
|
||||||
appendDebugLog('streamlink-install-finished', { works, command: cmd.command, prefixArgs: cmd.prefixArgs });
|
appendDebugLog('streamlink-install-finished', { works, command: cmd.command, prefixArgs: cmd.prefixArgs });
|
||||||
return works;
|
return works;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -847,7 +900,12 @@ async function ensureFfmpegInstalled(): Promise<boolean> {
|
|||||||
|
|
||||||
const ffmpegPath = getFFmpegPath();
|
const ffmpegPath = getFFmpegPath();
|
||||||
const ffprobePath = getFFprobePath();
|
const ffprobePath = getFFprobePath();
|
||||||
|
if (isVerifiedFfmpegCommands(ffmpegPath, ffprobePath)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (canExecuteCommand(ffmpegPath, ['-version']) && canExecuteCommand(ffprobePath, ['-version'])) {
|
if (canExecuteCommand(ffmpegPath, ['-version']) && canExecuteCommand(ffprobePath, ['-version'])) {
|
||||||
|
cacheVerifiedFfmpegCommands(ffmpegPath, ffprobePath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -875,6 +933,9 @@ async function ensureFfmpegInstalled(): Promise<boolean> {
|
|||||||
const newFfmpegPath = getFFmpegPath();
|
const newFfmpegPath = getFFmpegPath();
|
||||||
const newFfprobePath = getFFprobePath();
|
const newFfprobePath = getFFprobePath();
|
||||||
const works = canExecuteCommand(newFfmpegPath, ['-version']) && canExecuteCommand(newFfprobePath, ['-version']);
|
const works = canExecuteCommand(newFfmpegPath, ['-version']) && canExecuteCommand(newFfprobePath, ['-version']);
|
||||||
|
if (works) {
|
||||||
|
cacheVerifiedFfmpegCommands(newFfmpegPath, newFfprobePath);
|
||||||
|
}
|
||||||
appendDebugLog('ffmpeg-install-finished', { works, ffmpeg: newFfmpegPath, ffprobe: newFfprobePath });
|
appendDebugLog('ffmpeg-install-finished', { works, ffmpeg: newFfmpegPath, ffprobe: newFfprobePath });
|
||||||
return works;
|
return works;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -2554,15 +2615,21 @@ async function downloadVOD(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onProgress({
|
const streamlinkCmd = getStreamlinkCommand();
|
||||||
id: item.id,
|
const streamlinkVersionArgs = [...streamlinkCmd.prefixArgs, '--version'];
|
||||||
progress: -1,
|
const streamlinkAlreadyVerified = isVerifiedStreamlinkCommand(streamlinkCmd.command, streamlinkVersionArgs);
|
||||||
speed: '',
|
|
||||||
eta: '',
|
if (!streamlinkAlreadyVerified) {
|
||||||
status: 'Prufe Download-Tools...',
|
onProgress({
|
||||||
currentPart: 0,
|
id: item.id,
|
||||||
totalParts: 0
|
progress: -1,
|
||||||
});
|
speed: '',
|
||||||
|
eta: '',
|
||||||
|
status: 'Prufe Download-Tools...',
|
||||||
|
currentPart: 0,
|
||||||
|
totalParts: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const streamlinkReady = await ensureStreamlinkInstalled();
|
const streamlinkReady = await ensureStreamlinkInstalled();
|
||||||
if (!streamlinkReady) {
|
if (!streamlinkReady) {
|
||||||
@ -2931,9 +2998,7 @@ function createWindow(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (autoUpdateReadyToInstall && downloadedUpdateVersion) {
|
if (autoUpdateReadyToInstall && downloadedUpdateVersion) {
|
||||||
mainWindow?.webContents.send('update-downloaded', {
|
mainWindow?.webContents.send('update-downloaded', buildUpdateInfoPayload(downloadedUpdateVersion));
|
||||||
version: downloadedUpdateVersion
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -2958,6 +3023,62 @@ function hasNewerKnownUpdateThanDownloaded(): boolean {
|
|||||||
return isNewerUpdateVersion(latestKnownUpdateVersion, downloadedUpdateVersion);
|
return isNewerUpdateVersion(latestKnownUpdateVersion, downloadedUpdateVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeReleaseVersionCandidate(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeUpdateVersion(trimmed) || trimmed.replace(/^v/i, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheLatestReleaseUpdateInfo(releaseData: any): void {
|
||||||
|
if (!releaseData || typeof releaseData !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = typeof releaseData.tag_name === 'string' ? releaseData.tag_name.trim() : '';
|
||||||
|
const version = normalizeReleaseVersionCandidate(tagName)
|
||||||
|
|| normalizeReleaseVersionCandidate(releaseData.name);
|
||||||
|
const releaseName = typeof releaseData.name === 'string' ? releaseData.name.trim() : '';
|
||||||
|
const releaseNotes = typeof releaseData.body === 'string' ? releaseData.body : '';
|
||||||
|
const releaseDate = typeof releaseData.published_at === 'string'
|
||||||
|
? releaseData.published_at
|
||||||
|
: (typeof releaseData.created_at === 'string' ? releaseData.created_at : undefined);
|
||||||
|
|
||||||
|
latestReleaseUpdateInfo = {
|
||||||
|
tagName: tagName || undefined,
|
||||||
|
version,
|
||||||
|
releaseDate,
|
||||||
|
releaseName: releaseName || undefined,
|
||||||
|
releaseNotes: releaseNotes.trim() ? releaseNotes : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUpdateInfoPayload(version: string, releaseDate?: string): {
|
||||||
|
version: string;
|
||||||
|
releaseDate?: string;
|
||||||
|
releaseName?: string;
|
||||||
|
releaseNotes?: string;
|
||||||
|
} {
|
||||||
|
const normalizedVersion = normalizeReleaseVersionCandidate(version) || version;
|
||||||
|
const cachedVersion = latestReleaseUpdateInfo?.version
|
||||||
|
? (normalizeReleaseVersionCandidate(latestReleaseUpdateInfo.version) || latestReleaseUpdateInfo.version)
|
||||||
|
: undefined;
|
||||||
|
const hasMatchingReleaseInfo = !cachedVersion || cachedVersion === normalizedVersion;
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: normalizedVersion,
|
||||||
|
releaseDate: releaseDate || (hasMatchingReleaseInfo ? latestReleaseUpdateInfo?.releaseDate : undefined),
|
||||||
|
releaseName: hasMatchingReleaseInfo ? latestReleaseUpdateInfo?.releaseName : undefined,
|
||||||
|
releaseNotes: hasMatchingReleaseInfo ? latestReleaseUpdateInfo?.releaseNotes : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function requestUpdateCheck(source: UpdateCheckSource, force = false): Promise<{ started: boolean; reason?: string }> {
|
async function requestUpdateCheck(source: UpdateCheckSource, force = false): Promise<{ started: boolean; reason?: string }> {
|
||||||
if (autoUpdateCheckInProgress) {
|
if (autoUpdateCheckInProgress) {
|
||||||
return { started: false, reason: 'in-progress' };
|
return { started: false, reason: 'in-progress' };
|
||||||
@ -2981,7 +3102,8 @@ async function requestUpdateCheck(source: UpdateCheckSource, force = false): Pro
|
|||||||
'User-Agent': 'Twitch-VOD-Manager'
|
'User-Agent': 'Twitch-VOD-Manager'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const tagName = giteaRes.data?.tag_name;
|
cacheLatestReleaseUpdateInfo(giteaRes.data);
|
||||||
|
const tagName = latestReleaseUpdateInfo?.tagName || giteaRes.data?.tag_name;
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
autoUpdater.setFeedURL({
|
autoUpdater.setFeedURL({
|
||||||
provider: 'generic',
|
provider: 'generic',
|
||||||
@ -3120,18 +3242,13 @@ function setupAutoUpdater() {
|
|||||||
|
|
||||||
if (hasAlreadyDownloadedThisVersion) {
|
if (hasAlreadyDownloadedThisVersion) {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('update-downloaded', {
|
mainWindow.webContents.send('update-downloaded', buildUpdateInfoPayload(displayVersion, info.releaseDate));
|
||||||
version: displayVersion
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('update-available', {
|
mainWindow.webContents.send('update-available', buildUpdateInfoPayload(displayVersion, info.releaseDate));
|
||||||
version: displayVersion,
|
|
||||||
releaseDate: info.releaseDate
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AUTO_UPDATE_AUTO_DOWNLOAD) {
|
if (AUTO_UPDATE_AUTO_DOWNLOAD) {
|
||||||
@ -3166,9 +3283,7 @@ function setupAutoUpdater() {
|
|||||||
latestKnownUpdateVersion = downloadedVersion;
|
latestKnownUpdateVersion = downloadedVersion;
|
||||||
}
|
}
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.send('update-downloaded', {
|
mainWindow.webContents.send('update-downloaded', buildUpdateInfoPayload(downloadedVersion, info.releaseDate));
|
||||||
version: downloadedVersion
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -168,7 +168,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
onUpdateChecking: (callback: () => void) => {
|
onUpdateChecking: (callback: () => void) => {
|
||||||
ipcRenderer.on('update-checking', () => callback());
|
ipcRenderer.on('update-checking', () => callback());
|
||||||
},
|
},
|
||||||
onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string }) => void) => {
|
onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string; releaseName?: string; releaseNotes?: string }) => void) => {
|
||||||
ipcRenderer.on('update-available', (_, info) => callback(info));
|
ipcRenderer.on('update-available', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
onUpdateNotAvailable: (callback: () => void) => {
|
onUpdateNotAvailable: (callback: () => void) => {
|
||||||
@ -177,7 +177,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
onUpdateDownloadProgress: (callback: (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => void) => {
|
onUpdateDownloadProgress: (callback: (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => void) => {
|
||||||
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
|
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
|
||||||
},
|
},
|
||||||
onUpdateDownloaded: (callback: (info: { version: string }) => void) => {
|
onUpdateDownloaded: (callback: (info: { version: string; releaseDate?: string; releaseName?: string; releaseNotes?: string }) => void) => {
|
||||||
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
ipcRenderer.on('update-downloaded', (_, info) => callback(info));
|
||||||
},
|
},
|
||||||
onUpdateError: (callback: (payload: { message: string }) => void) => {
|
onUpdateError: (callback: (payload: { message: string }) => void) => {
|
||||||
|
|||||||
2
src/renderer-globals.d.ts
vendored
2
src/renderer-globals.d.ts
vendored
@ -126,6 +126,8 @@ interface ClipDialogData {
|
|||||||
interface UpdateInfo {
|
interface UpdateInfo {
|
||||||
version: string;
|
version: string;
|
||||||
releaseDate?: string;
|
releaseDate?: string;
|
||||||
|
releaseName?: string;
|
||||||
|
releaseNotes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateDownloadProgress {
|
interface UpdateDownloadProgress {
|
||||||
|
|||||||
@ -208,6 +208,18 @@ const UI_TEXT_DE = {
|
|||||||
downloadNow: 'Jetzt herunterladen',
|
downloadNow: 'Jetzt herunterladen',
|
||||||
downloadLabel: 'Download',
|
downloadLabel: 'Download',
|
||||||
ready: 'bereit zur Installation!',
|
ready: 'bereit zur Installation!',
|
||||||
installNow: 'Jetzt installieren'
|
installNow: 'Jetzt installieren',
|
||||||
|
modalAvailableTitle: 'Update verfugbar',
|
||||||
|
modalAvailableMessage: 'Version {version} ist verfugbar. Jetzt herunterladen?',
|
||||||
|
modalReadyTitle: 'Update bereit',
|
||||||
|
modalReadyMessage: 'Version {version} wurde heruntergeladen. Jetzt installieren?',
|
||||||
|
modalDismiss: 'Nein',
|
||||||
|
modalDownloadConfirm: 'Ja, herunterladen',
|
||||||
|
modalInstallConfirm: 'Ja, installieren',
|
||||||
|
changelogLabel: 'Changelog',
|
||||||
|
showChangelog: 'Changelog anzeigen',
|
||||||
|
hideChangelog: 'Changelog ausblenden',
|
||||||
|
noChangelog: 'Kein Changelog verfugbar.',
|
||||||
|
releasedLabel: 'Release'
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -208,6 +208,18 @@ const UI_TEXT_EN = {
|
|||||||
downloadNow: 'Download now',
|
downloadNow: 'Download now',
|
||||||
downloadLabel: 'Download',
|
downloadLabel: 'Download',
|
||||||
ready: 'ready to install!',
|
ready: 'ready to install!',
|
||||||
installNow: 'Install now'
|
installNow: 'Install now',
|
||||||
|
modalAvailableTitle: 'Update available',
|
||||||
|
modalAvailableMessage: 'Version {version} is available. Download it now?',
|
||||||
|
modalReadyTitle: 'Update ready',
|
||||||
|
modalReadyMessage: 'Version {version} has been downloaded. Install it now?',
|
||||||
|
modalDismiss: 'No',
|
||||||
|
modalDownloadConfirm: 'Yes, download',
|
||||||
|
modalInstallConfirm: 'Yes, install',
|
||||||
|
changelogLabel: 'Changelog',
|
||||||
|
showChangelog: 'Show changelog',
|
||||||
|
hideChangelog: 'Hide changelog',
|
||||||
|
noChangelog: 'No changelog available.',
|
||||||
|
releasedLabel: 'Release'
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -300,6 +300,17 @@ function collectCredentialsPayload(): Partial<AppConfig> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPartMinutesFieldState(): void {
|
||||||
|
const downloadMode = byId<HTMLSelectElement>('downloadMode').value;
|
||||||
|
const partMinutes = byId<HTMLInputElement>('partMinutes');
|
||||||
|
const label = byId<HTMLElement>('partMinutesLabel');
|
||||||
|
const isSplitMode = downloadMode === 'parts';
|
||||||
|
|
||||||
|
partMinutes.disabled = !isSplitMode;
|
||||||
|
partMinutes.setAttribute('aria-disabled', String(!isSplitMode));
|
||||||
|
label.classList.toggle('input-disabled', !isSplitMode);
|
||||||
|
}
|
||||||
|
|
||||||
function collectDownloadSettingsPayload(): Partial<AppConfig> {
|
function collectDownloadSettingsPayload(): Partial<AppConfig> {
|
||||||
return {
|
return {
|
||||||
download_mode: byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full',
|
download_mode: byId<HTMLSelectElement>('downloadMode').value as 'parts' | 'full',
|
||||||
@ -366,6 +377,7 @@ function syncSettingsFormFromConfig(): void {
|
|||||||
byId<HTMLInputElement>('vodFilenameTemplate').value = (config.filename_template_vod as string) || '{title}.mp4';
|
byId<HTMLInputElement>('vodFilenameTemplate').value = (config.filename_template_vod as string) || '{title}.mp4';
|
||||||
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';
|
||||||
|
syncPartMinutesFieldState();
|
||||||
validateFilenameTemplates();
|
validateFilenameTemplates();
|
||||||
lastPersistedSettingsFingerprint = getSettingsFingerprint({});
|
lastPersistedSettingsFingerprint = getSettingsFingerprint({});
|
||||||
}
|
}
|
||||||
@ -489,6 +501,8 @@ function initSettingsAutoSave(): void {
|
|||||||
void flushSettingsAutoSave(false);
|
void flushSettingsAutoSave(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
byId<HTMLSelectElement>('downloadMode').addEventListener('change', syncPartMinutesFieldState);
|
||||||
|
|
||||||
for (const id of immediateSaveIds) {
|
for (const id of immediateSaveIds) {
|
||||||
const element = byId<HTMLInputElement | HTMLSelectElement>(id);
|
const element = byId<HTMLInputElement | HTMLSelectElement>(id);
|
||||||
element.addEventListener('change', triggerImmediateSave);
|
element.addEventListener('change', triggerImmediateSave);
|
||||||
|
|||||||
@ -130,6 +130,13 @@ function applyLanguageToStaticUI(): void {
|
|||||||
setText('runtimeMetricsOutput', UI_TEXT.static.runtimeMetricsLoading);
|
setText('runtimeMetricsOutput', UI_TEXT.static.runtimeMetricsLoading);
|
||||||
setText('updateText', UI_TEXT.updates.bannerDefault);
|
setText('updateText', UI_TEXT.updates.bannerDefault);
|
||||||
setText('updateButton', UI_TEXT.updates.downloadNow);
|
setText('updateButton', UI_TEXT.updates.downloadNow);
|
||||||
|
setText('updateModalEyebrow', UI_TEXT.static.updateTitle);
|
||||||
|
setText('updateModalTitle', UI_TEXT.updates.modalAvailableTitle);
|
||||||
|
setText('updateModalDismissBtn', UI_TEXT.updates.modalDismiss);
|
||||||
|
setText('updateModalConfirmBtn', UI_TEXT.updates.modalDownloadConfirm);
|
||||||
|
setText('updateChangelogLabel', UI_TEXT.updates.changelogLabel);
|
||||||
|
setText('updateChangelogToggle', UI_TEXT.updates.showChangelog);
|
||||||
|
setText('updateChangelogEmpty', UI_TEXT.updates.noChangelog);
|
||||||
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
|
setPlaceholder('newStreamer', UI_TEXT.static.streamerPlaceholder);
|
||||||
|
|
||||||
const status = document.getElementById('statusText')?.textContent?.trim() || '';
|
const status = document.getElementById('statusText')?.textContent?.trim() || '';
|
||||||
@ -141,6 +148,11 @@ function applyLanguageToStaticUI(): void {
|
|||||||
if (typeof guideRefresh === 'function') {
|
if (typeof guideRefresh === 'function') {
|
||||||
guideRefresh();
|
guideRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateRefresh = (window as unknown as { refreshUpdateUiTexts?: () => void }).refreshUpdateUiTexts;
|
||||||
|
if (typeof updateRefresh === 'function') {
|
||||||
|
updateRefresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function localizeCurrentStatusText(current: string): string {
|
function localizeCurrentStatusText(current: string): string {
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
let updateCheckInProgress = false;
|
let updateCheckInProgress = false;
|
||||||
let updateDownloadInProgress = false;
|
let updateDownloadInProgress = false;
|
||||||
let manualUpdateCheckPending = false;
|
let manualUpdateCheckPending = false;
|
||||||
|
let manualUpdateOutcomeHandled = false;
|
||||||
let latestUpdateVersion = '';
|
let latestUpdateVersion = '';
|
||||||
|
let latestUpdateInfo: UpdateInfo | null = null;
|
||||||
|
let latestDownloadProgress: UpdateDownloadProgress | null = null;
|
||||||
|
let updateBannerState: 'idle' | 'available' | 'downloading' | 'ready' = 'idle';
|
||||||
|
let updateChangelogExpanded = false;
|
||||||
|
let shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
|
||||||
function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
||||||
const toastFn = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
const toastFn = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast;
|
||||||
@ -12,6 +18,55 @@ function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rememberUpdateInfo(info?: UpdateInfo | null): UpdateInfo {
|
||||||
|
const version = info?.version || latestUpdateVersion || latestUpdateInfo?.version || '?';
|
||||||
|
latestUpdateVersion = version;
|
||||||
|
latestUpdateInfo = {
|
||||||
|
...(latestUpdateInfo || { version }),
|
||||||
|
...(info || {}),
|
||||||
|
version
|
||||||
|
};
|
||||||
|
return latestUpdateInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveUpdateInfo(): UpdateInfo {
|
||||||
|
return rememberUpdateInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUpdateTemplate(template: string, version: string): string {
|
||||||
|
return template.replace(/\{version\}/g, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatReleaseDate(dateValue?: string): string {
|
||||||
|
if (!dateValue) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = new Date(dateValue);
|
||||||
|
if (Number.isNaN(parsed.getTime())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(getIntlLocale(), { dateStyle: 'medium' }).format(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUpdateModalMetaText(info: UpdateInfo): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
const releaseName = (info.releaseName || '').trim();
|
||||||
|
const canonicalNames = new Set([info.version, `v${info.version}`]);
|
||||||
|
|
||||||
|
if (releaseName && !canonicalNames.has(releaseName)) {
|
||||||
|
parts.push(`${UI_TEXT.updates.releasedLabel}: ${releaseName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = formatReleaseDate(info.releaseDate);
|
||||||
|
if (formattedDate) {
|
||||||
|
parts.push(formattedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
function setCheckButtonCheckingState(enabled: boolean): void {
|
function setCheckButtonCheckingState(enabled: boolean): void {
|
||||||
const btn = byId<HTMLButtonElement>('checkUpdateBtn');
|
const btn = byId<HTMLButtonElement>('checkUpdateBtn');
|
||||||
btn.disabled = enabled;
|
btn.disabled = enabled;
|
||||||
@ -22,33 +77,293 @@ function showUpdateBanner(): void {
|
|||||||
byId('updateBanner').style.display = 'flex';
|
byId('updateBanner').style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideUpdateBanner(): void {
|
||||||
|
byId('updateBanner').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUpdateBannerAvailableUi(info: UpdateInfo): void {
|
||||||
|
const activeInfo = rememberUpdateInfo(info);
|
||||||
|
updateReady = false;
|
||||||
|
updateDownloadInProgress = false;
|
||||||
|
latestDownloadProgress = null;
|
||||||
|
updateBannerState = 'available';
|
||||||
|
|
||||||
|
showUpdateBanner();
|
||||||
|
byId('updateProgress').style.display = 'none';
|
||||||
|
|
||||||
|
const bar = byId('updateProgressBar');
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = '0%';
|
||||||
|
|
||||||
|
byId('updateText').textContent = `Version ${activeInfo.version} ${UI_TEXT.updates.available}`;
|
||||||
|
const button = byId<HTMLButtonElement>('updateButton');
|
||||||
|
button.textContent = UI_TEXT.updates.downloadNow;
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
function setDownloadPendingUi(): void {
|
function setDownloadPendingUi(): void {
|
||||||
|
updateReady = false;
|
||||||
|
updateBannerState = 'downloading';
|
||||||
|
|
||||||
showUpdateBanner();
|
showUpdateBanner();
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
const button = byId<HTMLButtonElement>('updateButton');
|
||||||
button.textContent = UI_TEXT.updates.downloading;
|
button.textContent = UI_TEXT.updates.downloading;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
byId('updateProgress').style.display = 'block';
|
byId('updateProgress').style.display = 'block';
|
||||||
|
|
||||||
const bar = byId('updateProgressBar');
|
const bar = byId('updateProgressBar');
|
||||||
bar.classList.add('downloading');
|
bar.classList.add('downloading');
|
||||||
bar.style.width = '30%';
|
bar.style.width = latestDownloadProgress ? `${latestDownloadProgress.percent}%` : '30%';
|
||||||
|
|
||||||
|
if (!latestDownloadProgress) {
|
||||||
|
byId('updateText').textContent = `Version ${latestUpdateVersion || '?'} ${UI_TEXT.updates.downloading}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDownloadReadyUi(version: string): void {
|
function setDownloadReadyUi(info?: UpdateInfo): void {
|
||||||
|
const activeInfo = rememberUpdateInfo(info);
|
||||||
showUpdateBanner();
|
showUpdateBanner();
|
||||||
updateReady = true;
|
updateReady = true;
|
||||||
updateDownloadInProgress = false;
|
updateDownloadInProgress = false;
|
||||||
latestUpdateVersion = version || latestUpdateVersion;
|
updateBannerState = 'ready';
|
||||||
|
latestDownloadProgress = null;
|
||||||
|
|
||||||
const bar = byId('updateProgressBar');
|
const bar = byId('updateProgressBar');
|
||||||
bar.classList.remove('downloading');
|
bar.classList.remove('downloading');
|
||||||
bar.style.width = '100%';
|
bar.style.width = '100%';
|
||||||
|
|
||||||
byId('updateText').textContent = `Version ${latestUpdateVersion || '?'} ${UI_TEXT.updates.ready}`;
|
byId('updateProgress').style.display = 'block';
|
||||||
|
byId('updateText').textContent = `Version ${activeInfo.version} ${UI_TEXT.updates.ready}`;
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
const button = byId<HTMLButtonElement>('updateButton');
|
||||||
button.textContent = UI_TEXT.updates.installNow;
|
button.textContent = UI_TEXT.updates.installNow;
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendInlineMarkdown(target: HTMLElement, text: string): void {
|
||||||
|
const parts = text.split(/(\*\*[^*]+\*\*)/g);
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const strongMatch = part.match(/^\*\*(.+)\*\*$/);
|
||||||
|
if (strongMatch) {
|
||||||
|
const strong = document.createElement('strong');
|
||||||
|
strong.textContent = strongMatch[1].trim();
|
||||||
|
target.appendChild(strong);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.appendChild(document.createTextNode(part));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUpdateChangelog(notes?: string): void {
|
||||||
|
const card = byId<HTMLElement>('updateChangelogCard');
|
||||||
|
const panel = byId<HTMLElement>('updateChangelogPanel');
|
||||||
|
const content = byId<HTMLElement>('updateChangelogContent');
|
||||||
|
const empty = byId<HTMLElement>('updateChangelogEmpty');
|
||||||
|
const normalized = (notes || '').replace(/\r/g, '').trim();
|
||||||
|
|
||||||
|
content.innerHTML = '';
|
||||||
|
empty.hidden = true;
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
card.style.display = 'none';
|
||||||
|
panel.hidden = true;
|
||||||
|
updateChangelogExpanded = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.style.display = 'block';
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
let currentList: HTMLUListElement | null = null;
|
||||||
|
let lastBlockWasHeading = false;
|
||||||
|
|
||||||
|
const flushList = (): void => {
|
||||||
|
currentList = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureList = (): HTMLUListElement => {
|
||||||
|
if (currentList) {
|
||||||
|
return currentList;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentList = document.createElement('ul');
|
||||||
|
currentList.className = 'update-changelog-list';
|
||||||
|
fragment.appendChild(currentList);
|
||||||
|
return currentList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const appendListItem = (line: string): void => {
|
||||||
|
const item = document.createElement('li');
|
||||||
|
appendInlineMarkdown(item, line);
|
||||||
|
ensureList().appendChild(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const rawLine of normalized.split('\n')) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) {
|
||||||
|
flushList();
|
||||||
|
lastBlockWasHeading = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boldHeadingMatch = line.match(/^\*\*(.+?)\*\*:?$/);
|
||||||
|
const markdownHeadingMatch = line.match(/^#{1,6}\s+(.+)$/);
|
||||||
|
if (boldHeadingMatch || markdownHeadingMatch) {
|
||||||
|
flushList();
|
||||||
|
const heading = document.createElement('h4');
|
||||||
|
heading.className = 'update-changelog-heading';
|
||||||
|
heading.textContent = (boldHeadingMatch?.[1] || markdownHeadingMatch?.[1] || '').trim();
|
||||||
|
fragment.appendChild(heading);
|
||||||
|
lastBlockWasHeading = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listMatch = line.match(/^(?:[-*+]\s+|\d+\.\s+)(.+)$/);
|
||||||
|
if (listMatch) {
|
||||||
|
appendListItem(listMatch[1].trim());
|
||||||
|
lastBlockWasHeading = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastBlockWasHeading) {
|
||||||
|
appendListItem(line);
|
||||||
|
lastBlockWasHeading = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
flushList();
|
||||||
|
const paragraph = document.createElement('p');
|
||||||
|
paragraph.className = 'update-changelog-paragraph';
|
||||||
|
appendInlineMarkdown(paragraph, line);
|
||||||
|
fragment.appendChild(paragraph);
|
||||||
|
lastBlockWasHeading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fragment.childNodes.length) {
|
||||||
|
empty.hidden = false;
|
||||||
|
} else {
|
||||||
|
content.appendChild(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.hidden = !updateChangelogExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUpdateChangelogToggleText(): void {
|
||||||
|
const toggle = byId<HTMLButtonElement>('updateChangelogToggle');
|
||||||
|
const card = byId<HTMLElement>('updateChangelogCard');
|
||||||
|
if (card.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle.textContent = updateChangelogExpanded ? UI_TEXT.updates.hideChangelog : UI_TEXT.updates.showChangelog;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUpdateModalTexts(): void {
|
||||||
|
const info = getActiveUpdateInfo();
|
||||||
|
const isReady = updateReady;
|
||||||
|
|
||||||
|
byId('updateModalTitle').textContent = isReady
|
||||||
|
? UI_TEXT.updates.modalReadyTitle
|
||||||
|
: UI_TEXT.updates.modalAvailableTitle;
|
||||||
|
byId('updateModalMessage').textContent = formatUpdateTemplate(
|
||||||
|
isReady ? UI_TEXT.updates.modalReadyMessage : UI_TEXT.updates.modalAvailableMessage,
|
||||||
|
info.version
|
||||||
|
);
|
||||||
|
byId('updateModalDismissBtn').textContent = UI_TEXT.updates.modalDismiss;
|
||||||
|
byId('updateModalConfirmBtn').textContent = isReady
|
||||||
|
? UI_TEXT.updates.modalInstallConfirm
|
||||||
|
: UI_TEXT.updates.modalDownloadConfirm;
|
||||||
|
byId('updateChangelogLabel').textContent = UI_TEXT.updates.changelogLabel;
|
||||||
|
byId('updateChangelogEmpty').textContent = UI_TEXT.updates.noChangelog;
|
||||||
|
|
||||||
|
const metaText = getUpdateModalMetaText(info);
|
||||||
|
const meta = byId('updateModalMeta');
|
||||||
|
meta.textContent = metaText;
|
||||||
|
meta.style.display = metaText ? 'block' : 'none';
|
||||||
|
|
||||||
|
renderUpdateChangelog(info.releaseNotes);
|
||||||
|
refreshUpdateChangelogToggleText();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openUpdateModal(info?: UpdateInfo): void {
|
||||||
|
rememberUpdateInfo(info);
|
||||||
|
updateChangelogExpanded = false;
|
||||||
|
byId('updateModal').classList.add('show');
|
||||||
|
refreshUpdateModalTexts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissUpdateModal(): void {
|
||||||
|
byId('updateModal').classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmUpdateModal(): void {
|
||||||
|
dismissUpdateModal();
|
||||||
|
|
||||||
|
if (updateReady) {
|
||||||
|
void window.api.installUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleUpdateChangelog(): void {
|
||||||
|
const card = byId<HTMLElement>('updateChangelogCard');
|
||||||
|
if (card.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChangelogExpanded = !updateChangelogExpanded;
|
||||||
|
byId<HTMLElement>('updateChangelogPanel').hidden = !updateChangelogExpanded;
|
||||||
|
refreshUpdateChangelogToggleText();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUpdateModalOverlayClick(event: MouseEvent): void {
|
||||||
|
if (event.target === byId('updateModal')) {
|
||||||
|
dismissUpdateModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshUpdateUiTexts(): void {
|
||||||
|
const button = byId<HTMLButtonElement>('updateButton');
|
||||||
|
const progress = byId('updateProgress');
|
||||||
|
const bar = byId('updateProgressBar');
|
||||||
|
|
||||||
|
if (updateBannerState === 'available' && latestUpdateInfo) {
|
||||||
|
setUpdateBannerAvailableUi(latestUpdateInfo);
|
||||||
|
} else if (updateBannerState === 'downloading') {
|
||||||
|
button.textContent = UI_TEXT.updates.downloading;
|
||||||
|
button.disabled = true;
|
||||||
|
progress.style.display = 'block';
|
||||||
|
if (latestDownloadProgress) {
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = `${latestDownloadProgress.percent}%`;
|
||||||
|
const mb = (latestDownloadProgress.transferred / 1024 / 1024).toFixed(1);
|
||||||
|
const totalMb = (latestDownloadProgress.total / 1024 / 1024).toFixed(1);
|
||||||
|
byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${latestDownloadProgress.percent.toFixed(0)}%)`;
|
||||||
|
} else {
|
||||||
|
setDownloadPendingUi();
|
||||||
|
}
|
||||||
|
} else if (updateBannerState === 'ready' && latestUpdateInfo) {
|
||||||
|
setDownloadReadyUi(latestUpdateInfo);
|
||||||
|
} else {
|
||||||
|
hideUpdateBanner();
|
||||||
|
progress.style.display = 'none';
|
||||||
|
bar.classList.remove('downloading');
|
||||||
|
bar.style.width = '0%';
|
||||||
|
byId('updateText').textContent = UI_TEXT.updates.bannerDefault;
|
||||||
|
button.textContent = UI_TEXT.updates.downloadNow;
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshUpdateModalTexts();
|
||||||
|
}
|
||||||
|
|
||||||
async function checkUpdateSilent(): Promise<void> {
|
async function checkUpdateSilent(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await window.api.checkUpdate();
|
await window.api.checkUpdate();
|
||||||
@ -59,12 +374,16 @@ async function checkUpdateSilent(): Promise<void> {
|
|||||||
|
|
||||||
async function checkUpdate(): Promise<void> {
|
async function checkUpdate(): Promise<void> {
|
||||||
manualUpdateCheckPending = true;
|
manualUpdateCheckPending = true;
|
||||||
|
manualUpdateOutcomeHandled = false;
|
||||||
|
shouldOpenUpdateModalOnAvailable = true;
|
||||||
setCheckButtonCheckingState(true);
|
setCheckButtonCheckingState(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.api.checkUpdate();
|
const result = await window.api.checkUpdate();
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
manualUpdateOutcomeHandled = true;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
@ -74,14 +393,22 @@ async function checkUpdate(): Promise<void> {
|
|||||||
|
|
||||||
const skippedReason = result?.skipped;
|
const skippedReason = result?.skipped;
|
||||||
if (skippedReason === 'ready-to-install') {
|
if (skippedReason === 'ready-to-install') {
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
manualUpdateOutcomeHandled = true;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
notifyUpdate(UI_TEXT.updates.readyToInstall, 'info');
|
if (latestUpdateInfo || updateReady) {
|
||||||
|
openUpdateModal(getActiveUpdateInfo());
|
||||||
|
} else {
|
||||||
|
notifyUpdate(UI_TEXT.updates.readyToInstall, 'info');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skippedReason === 'in-progress' || skippedReason === 'throttled') {
|
if (skippedReason === 'in-progress' || skippedReason === 'throttled') {
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
manualUpdateOutcomeHandled = true;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
@ -94,11 +421,14 @@ async function checkUpdate(): Promise<void> {
|
|||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
|
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
if (!updateReady && byId('updateBanner').style.display !== 'flex') {
|
if (!manualUpdateOutcomeHandled && !updateReady && byId('updateBanner').style.display !== 'flex') {
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
notifyUpdate(UI_TEXT.updates.latest, 'info');
|
notifyUpdate(UI_TEXT.updates.latest, 'info');
|
||||||
}
|
}
|
||||||
}, 2500);
|
}, 2500);
|
||||||
} catch {
|
} catch {
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
|
manualUpdateOutcomeHandled = true;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
@ -108,6 +438,7 @@ async function checkUpdate(): Promise<void> {
|
|||||||
|
|
||||||
function downloadUpdate(): void {
|
function downloadUpdate(): void {
|
||||||
if (updateReady) {
|
if (updateReady) {
|
||||||
|
dismissUpdateModal();
|
||||||
void window.api.installUpdate();
|
void window.api.installUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -118,21 +449,23 @@ function downloadUpdate(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadInProgress = true;
|
updateDownloadInProgress = true;
|
||||||
|
latestDownloadProgress = null;
|
||||||
|
dismissUpdateModal();
|
||||||
setDownloadPendingUi();
|
setDownloadPendingUi();
|
||||||
|
|
||||||
void window.api.downloadUpdate().then((result) => {
|
void window.api.downloadUpdate().then((result) => {
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
updateDownloadInProgress = false;
|
updateDownloadInProgress = false;
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
if (latestUpdateInfo) {
|
||||||
button.textContent = UI_TEXT.updates.downloadNow;
|
setUpdateBannerAvailableUi(latestUpdateInfo);
|
||||||
button.disabled = false;
|
}
|
||||||
byId('updateProgressBar').classList.remove('downloading');
|
|
||||||
notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn');
|
notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result?.skipped === 'ready-to-install') {
|
if (result?.skipped === 'ready-to-install') {
|
||||||
setDownloadReadyUi(latestUpdateVersion);
|
setDownloadReadyUi(getActiveUpdateInfo());
|
||||||
|
openUpdateModal(getActiveUpdateInfo());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,10 +474,9 @@ function downloadUpdate(): void {
|
|||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
updateDownloadInProgress = false;
|
updateDownloadInProgress = false;
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
if (latestUpdateInfo) {
|
||||||
button.textContent = UI_TEXT.updates.downloadNow;
|
setUpdateBannerAvailableUi(latestUpdateInfo);
|
||||||
button.disabled = false;
|
}
|
||||||
byId('updateProgressBar').classList.remove('downloading');
|
|
||||||
notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn');
|
notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -157,45 +489,58 @@ window.api.onUpdateChecking(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.api.onUpdateAvailable((info: UpdateInfo) => {
|
window.api.onUpdateAvailable((info: UpdateInfo) => {
|
||||||
|
const activeInfo = rememberUpdateInfo(info);
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
updateReady = false;
|
updateReady = false;
|
||||||
updateDownloadInProgress = true;
|
updateDownloadInProgress = false;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
latestUpdateVersion = info.version;
|
manualUpdateOutcomeHandled = true;
|
||||||
|
latestDownloadProgress = null;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
|
|
||||||
showUpdateBanner();
|
setUpdateBannerAvailableUi(activeInfo);
|
||||||
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`;
|
|
||||||
byId('updateButton').textContent = UI_TEXT.updates.downloading;
|
if (shouldOpenUpdateModalOnAvailable) {
|
||||||
byId<HTMLButtonElement>('updateButton').disabled = true;
|
openUpdateModal(activeInfo);
|
||||||
byId('updateProgress').style.display = 'block';
|
}
|
||||||
byId('updateProgressBar').classList.add('downloading');
|
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onUpdateNotAvailable(() => {
|
window.api.onUpdateNotAvailable(() => {
|
||||||
updateCheckInProgress = false;
|
updateCheckInProgress = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
|
manualUpdateOutcomeHandled = true;
|
||||||
|
|
||||||
if (manualUpdateCheckPending) {
|
if (manualUpdateCheckPending) {
|
||||||
notifyUpdate(UI_TEXT.updates.latest, 'info');
|
notifyUpdate(UI_TEXT.updates.latest, 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
|
||||||
updateDownloadInProgress = true;
|
updateDownloadInProgress = true;
|
||||||
|
updateBannerState = 'downloading';
|
||||||
|
latestDownloadProgress = progress;
|
||||||
|
|
||||||
const bar = byId('updateProgressBar');
|
const bar = byId('updateProgressBar');
|
||||||
bar.classList.remove('downloading');
|
bar.classList.remove('downloading');
|
||||||
bar.style.width = progress.percent + '%';
|
bar.style.width = progress.percent + '%';
|
||||||
|
|
||||||
|
showUpdateBanner();
|
||||||
|
byId('updateProgress').style.display = 'block';
|
||||||
|
|
||||||
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
const mb = (progress.transferred / 1024 / 1024).toFixed(1);
|
||||||
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
const totalMb = (progress.total / 1024 / 1024).toFixed(1);
|
||||||
byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
byId('updateText').textContent = `${UI_TEXT.updates.downloadLabel}: ${mb} / ${totalMb} MB (${progress.percent.toFixed(0)}%)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
window.api.onUpdateDownloaded((info: UpdateInfo) => {
|
||||||
setDownloadReadyUi(info.version);
|
const activeInfo = rememberUpdateInfo(info);
|
||||||
|
setDownloadReadyUi(activeInfo);
|
||||||
|
openUpdateModal(activeInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.api.onUpdateError(() => {
|
window.api.onUpdateError(() => {
|
||||||
@ -203,14 +548,19 @@ window.api.onUpdateError(() => {
|
|||||||
const wasDownloading = updateDownloadInProgress;
|
const wasDownloading = updateDownloadInProgress;
|
||||||
updateDownloadInProgress = false;
|
updateDownloadInProgress = false;
|
||||||
manualUpdateCheckPending = false;
|
manualUpdateCheckPending = false;
|
||||||
|
manualUpdateOutcomeHandled = true;
|
||||||
|
shouldOpenUpdateModalOnAvailable = false;
|
||||||
setCheckButtonCheckingState(false);
|
setCheckButtonCheckingState(false);
|
||||||
|
|
||||||
const button = byId<HTMLButtonElement>('updateButton');
|
if (!updateReady && latestUpdateInfo) {
|
||||||
if (!updateReady) {
|
setUpdateBannerAvailableUi(latestUpdateInfo);
|
||||||
button.textContent = UI_TEXT.updates.downloadNow;
|
|
||||||
button.disabled = false;
|
|
||||||
byId('updateProgressBar').classList.remove('downloading');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyUpdate(wasDownloading ? UI_TEXT.updates.downloadFailed : UI_TEXT.updates.checkFailed, 'warn');
|
notifyUpdate(wasDownloading ? UI_TEXT.updates.downloadFailed : UI_TEXT.updates.checkFailed, 'warn');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape' && byId('updateModal').classList.contains('show')) {
|
||||||
|
dismissUpdateModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
136
src/styles.css
136
src/styles.css
@ -606,6 +606,17 @@ body {
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group input:not([type="checkbox"]):not([type="radio"]):disabled,
|
||||||
|
.form-group select:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: rgba(239, 239, 241, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group input[type="checkbox"],
|
.form-group input[type="checkbox"],
|
||||||
.form-group input[type="radio"] {
|
.form-group input[type="radio"] {
|
||||||
width: auto;
|
width: auto;
|
||||||
@ -848,6 +859,131 @@ body {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.update-modal {
|
||||||
|
max-width: 680px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(145, 70, 255, 0.18) 0%, rgba(145, 70, 255, 0.05) 24%, rgba(14, 14, 16, 0.98) 100%),
|
||||||
|
var(--bg-card);
|
||||||
|
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.48);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-modal-eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(145, 70, 255, 0.16);
|
||||||
|
color: #f1e7ff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-modal-message {
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: -8px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-modal-meta {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-modal-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-card {
|
||||||
|
margin-top: 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(7, 7, 10, 0.42);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #f3ecff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-toggle:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-panel {
|
||||||
|
max-height: 320px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-heading {
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-paragraph {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
color: var(--text);
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-list li {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-content strong {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-changelog-empty {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
#updateProgressBar.downloading {
|
#updateProgressBar.downloading {
|
||||||
width: 30% !important;
|
width: 30% !important;
|
||||||
animation: indeterminate 1.5s ease-in-out infinite;
|
animation: indeterminate 1.5s ease-in-out infinite;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user