add handlers and setting for nightly release

This commit is contained in:
jeffvli
2026-02-05 23:45:32 -08:00
parent 65c215fa9c
commit cf663de2fc
11 changed files with 403 additions and 51 deletions
@@ -1,12 +1,12 @@
# Nightly builds published to Cloudflare R2 with date versioning (e.g. 1.0.0-nightly-20260205).
# Alpha builds published to Cloudflare R2 with date versioning (e.g. 1.0.0-alpha-20260205).
# Required repo secrets: R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY (from R2 API token in Cloudflare dashboard).
name: Publish Nightly
name: Publish Alpha
on:
workflow_dispatch:
inputs:
version:
description: 'Semantic version number (e.g., 1.0.0) - nightly suffix will be added automatically'
description: 'Semantic version number (e.g., 1.0.0) - alpha suffix will be added automatically'
required: false
type: string
schedule:
@@ -14,7 +14,27 @@ on:
- cron: '0 11 * * *'
jobs:
check-new-commits:
runs-on: ubuntu-latest
outputs:
has_new_commits: ${{ steps.manual.outputs.has_new_commits || steps.check.outputs['has-new-commits'] }}
steps:
- name: Set has new commits (manual trigger)
id: manual
if: github.event_name == 'workflow_dispatch'
run: echo "has_new_commits=true" >> "$GITHUB_OUTPUT"
- name: Check for new commits (24 hr interval)
id: check
if: github.event_name != 'workflow_dispatch'
uses: adriangl/check-new-commits-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
seconds: 86400
prepare:
needs: check-new-commits
if: needs.check-new-commits.outputs.has_new_commits == 'true'
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
@@ -30,7 +50,7 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Set date-based nightly version
- name: Set date-based alpha version
id: version
shell: pwsh
run: |
@@ -70,15 +90,15 @@ jobs:
$pst = [TimeZoneInfo]::FindSystemTimeZoneById('America/Los_Angeles')
$dateInPst = [TimeZoneInfo]::ConvertTimeFromUtc([DateTime]::UtcNow, $pst)
$dateStr = $dateInPst.ToString("yyyyMMdd")
$nightlyVersion = "$inputVersion-nightly-$dateStr"
Write-Host "Nightly version: $nightlyVersion"
$alphaVersion = "$inputVersion-alpha-$dateStr"
Write-Host "Alpha version: $alphaVersion"
# Update package.json
$packageJson = Get-Content package.json | ConvertFrom-Json
$packageJson.version = $nightlyVersion
$packageJson.version = $alphaVersion
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
echo "version=$nightlyVersion" >> $env:GITHUB_OUTPUT
echo "version=$alphaVersion" >> $env:GITHUB_OUTPUT
cleanup:
needs: prepare
@@ -132,7 +152,7 @@ jobs:
max_attempts: 3
retry_on: error
command: |
pnpm run publish:win:nightly
pnpm run publish:win:alpha
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (macOS)
@@ -143,7 +163,7 @@ jobs:
max_attempts: 3
retry_on: error
command: |
pnpm run publish:mac:nightly
pnpm run publish:mac:alpha
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (Linux)
@@ -154,7 +174,7 @@ jobs:
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux:nightly
pnpm run publish:linux:alpha
on_retry_command: pnpm cache delete
- name: Build and Publish to R2 (Linux ARM64)
@@ -165,5 +185,5 @@ jobs:
max_attempts: 3
retry_on: error
command: |
pnpm run publish:linux-arm64:nightly
pnpm run publish:linux-arm64:alpha
on_retry_command: pnpm cache delete
+4 -4
View File
@@ -47,16 +47,16 @@
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
"publish:linux-arm64": "pnpm run build && electron-builder --publish always --linux --arm64",
"publish:linux-arm64:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux --arm64",
"publish:linux-arm64:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux --arm64",
"publish:linux-arm64:nightly": "pnpm run build && electron-builder --config electron-builder-nightly.yml --publish always --linux --arm64",
"publish:linux:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --linux",
"publish:linux:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --linux",
"publish:linux:nightly": "pnpm run build && electron-builder --config electron-builder-nightly.yml --publish always --linux",
"publish:mac": "pnpm run build && electron-builder --publish always --mac",
"publish:mac:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --mac",
"publish:mac:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --mac",
"publish:mac:nightly": "pnpm run build && electron-builder --config electron-builder-nightly.yml --publish always --mac",
"publish:win": "pnpm run build && electron-builder --publish always --win",
"publish:win:alpha": "pnpm run build && electron-builder --config electron-builder-alpha.yml --publish always --win",
"publish:win:beta": "pnpm run build && electron-builder --config electron-builder-beta.yml --publish always --win",
"publish:win:nightly": "pnpm run build && electron-builder --config electron-builder-nightly.yml --publish always --win",
"start": "electron-vite preview",
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
+7 -1
View File
@@ -448,6 +448,11 @@
"radioList": {
"title": "radio stations"
},
"releasenotes": {
"commitsSinceStable": "commits since {{stable}}",
"noNewCommits": "no new commits in this range",
"noStableReleaseToCompare": "no stable release available to compare with"
},
"favorites": {
"title": "$t(entity.favorite, {\"count\": 2})"
},
@@ -745,10 +750,11 @@
"customFontPath_description": "sets the path to the custom font to use for the application",
"customFontPath": "custom font path",
"disableAutomaticUpdates": "disable automatic updates",
"releaseChannel_optionAlpha": "alpha (nightly)",
"releaseChannel_optionBeta": "beta",
"releaseChannel_optionLatest": "latest",
"releaseChannel": "release channel",
"releaseChannel_description": "choose between stable releases or beta releases for automatic updates",
"releaseChannel_description": "choose between stable, beta, or alpha (nightly) releases for automatic updates",
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
"discordApplicationId": "{{discord}} application id",
+127 -26
View File
@@ -18,7 +18,7 @@ import {
} from 'electron';
import electronLocalShortcut from 'electron-localshortcut';
import log from 'electron-log/main';
import { autoUpdater } from 'electron-updater';
import { AppImageUpdater, autoUpdater, MacUpdater, NsisUpdater } from 'electron-updater';
import { access, constants } from 'fs';
import path, { join } from 'path';
@@ -40,40 +40,111 @@ import './features';
import { PlayerType, TitleTheme } from '/@/shared/types/types';
export default class AppUpdater {
const ALPHA_UPDATER_CONFIG: {
bucket: string;
channel: string;
endpoint: string;
provider: 's3';
} = {
bucket: '',
channel: 'alpha',
endpoint: 'https://feishin-nightly-bucket.jeffvli.org',
provider: 's3',
};
type UpdaterInstance = AppImageUpdater | MacUpdater | NsisUpdater | typeof autoUpdater;
class AlphaAppUpdater {
constructor() {
const updater = createAlphaUpdaterInstance();
log.transports.file.level = 'info';
autoUpdater.logger = autoUpdaterLogInterface;
updater.logger = autoUpdaterLogInterface;
updater.channel = ALPHA_UPDATER_CONFIG.channel;
updater.allowPrerelease = true;
updater.disableDifferentialDownload = true;
updater.allowDowngrade = true;
updater.autoInstallOnAppQuit = true;
updater.autoRunAppAfterInstall = true;
updater.checkForUpdatesAndNotify();
}
}
const isBetaVersion = packageJson.version.includes('-beta');
const releaseChannel = store.get('release_channel');
const isNotConfigured = !releaseChannel;
console.log('Release channel: ', releaseChannel);
console.log('Is beta version: ', isBetaVersion);
if (isNotConfigured) {
console.log(
'Release channel not configured, setting to ',
isBetaVersion ? 'beta' : 'latest',
);
store.set('release_channel', isBetaVersion ? 'beta' : 'latest');
}
if (releaseChannel === 'beta') {
autoUpdater.channel = 'beta';
autoUpdater.allowPrerelease = true;
autoUpdater.disableDifferentialDownload = true;
} else if (releaseChannel === 'latest') {
autoUpdater.channel = 'latest';
autoUpdater.allowDowngrade = true;
autoUpdater.allowPrerelease = false;
class AppUpdater {
constructor() {
const effectiveChannel = store.get('release_channel') as string;
console.log('Effective update channel:', effectiveChannel);
if (effectiveChannel === 'alpha') {
return new AlphaAppUpdater();
}
configureAndGetUpdater();
autoUpdater.checkForUpdatesAndNotify();
}
}
function configureAndGetUpdater(): UpdaterInstance {
const isBetaVersion = packageJson.version.includes('-beta');
const isAlphaVersion = packageJson.version.includes('-alpha');
let releaseChannel = store.get('release_channel');
const isNotConfigured = !releaseChannel;
console.log('Release channel:', releaseChannel);
console.log('Is beta version:', isBetaVersion);
console.log('Is alpha version:', isAlphaVersion);
console.log('Is not configured:', isNotConfigured);
if (isNotConfigured) {
console.log('Release channel not configured, setting default channel');
const defaultChannel = isAlphaVersion ? 'alpha' : isBetaVersion ? 'beta' : 'latest';
store.set('release_channel', defaultChannel);
releaseChannel = defaultChannel;
}
const effectiveChannel = store.get('release_channel') as string;
if (effectiveChannel === 'alpha') {
const updater = createAlphaUpdaterInstance();
log.transports.file.level = 'info';
updater.logger = autoUpdaterLogInterface;
updater.channel = ALPHA_UPDATER_CONFIG.channel;
updater.allowPrerelease = true;
updater.disableDifferentialDownload = true;
updater.allowDowngrade = true;
updater.autoInstallOnAppQuit = true;
updater.autoRunAppAfterInstall = true;
return updater;
}
log.transports.file.level = 'info';
autoUpdater.logger = autoUpdaterLogInterface;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.autoRunAppAfterInstall = true;
if (effectiveChannel === 'beta') {
autoUpdater.channel = 'beta';
autoUpdater.allowPrerelease = true;
autoUpdater.disableDifferentialDownload = true;
} else {
autoUpdater.channel = 'latest';
autoUpdater.allowDowngrade = true;
autoUpdater.allowPrerelease = false;
}
return autoUpdater;
}
function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdater {
if (isMacOS()) {
return new MacUpdater(ALPHA_UPDATER_CONFIG);
}
if (isLinux()) {
return new AppImageUpdater(ALPHA_UPDATER_CONFIG);
}
return new NsisUpdater(ALPHA_UPDATER_CONFIG);
}
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
process.on('uncaughtException', (error: any) => {
@@ -359,6 +430,36 @@ async function createWindow(first = true): Promise<void> {
return mainWindow?.webContents.session.clearCache();
});
ipcMain.handle(
'app-check-for-updates',
async (): Promise<{ updateAvailable: boolean; version?: string }> => {
if (disableAutoUpdates()) {
console.log('Auto updates are disabled');
return { updateAvailable: false };
}
try {
console.log('Checking for updates');
const updater = configureAndGetUpdater();
const result = await updater.checkForUpdates();
const updateAvailable = result?.isUpdateAvailable ?? false;
console.log('Update available:', updateAvailable);
if (updateAvailable && store.get('disable_auto_updates') !== true) {
console.log('Downloading update');
updater.downloadUpdate();
}
return {
updateAvailable,
version: result?.updateInfo?.version,
};
} catch {
console.log('Error checking for updates');
return { updateAvailable: false };
}
},
);
ipcMain.on('app-restart', () => {
// Fix for .AppImage
if (process.env.APPIMAGE) {
+5
View File
@@ -39,6 +39,10 @@ const download = (url: string) => {
ipcRenderer.send('download-url', url);
};
const checkForUpdates = (): Promise<{ updateAvailable: boolean; version?: string }> => {
return ipcRenderer.invoke('app-check-for-updates');
};
const forceGarbageCollection = (): boolean => {
try {
if (typeof global.gc === 'function') {
@@ -58,6 +62,7 @@ const forceGarbageCollection = (): boolean => {
};
export const utils = {
checkForUpdates,
disableAutoUpdates,
download,
forceGarbageCollection,
+2
View File
@@ -11,6 +11,7 @@ import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import i18n from '/@/i18n/i18n';
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
import { useCheckForUpdates } from '/@/renderer/hooks/use-check-for-updates';
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
import { AppRouter } from '/@/renderer/router/app-router';
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
@@ -38,6 +39,7 @@ export const App = () => {
const cssRef = useRef<HTMLStyleElement | null>(null);
useSyncSettingsToMain();
useCheckForUpdates();
const [webAudio, setWebAudio] = useState<WebAudio>();
@@ -41,6 +41,13 @@ export const UpdateSettings = memo(() => {
}),
value: 'beta',
},
{
label: t('setting.releaseChannel', {
context: 'optionAlpha',
postProcess: 'titleCase',
}),
value: 'alpha',
},
]}
defaultValue={
(localSettings?.get('release_channel') as string | undefined) || 'latest'
@@ -50,7 +57,7 @@ export const UpdateSettings = memo(() => {
localSettings?.set('release_channel', value);
setSettings({
window: {
releaseChannel: value as 'beta' | 'latest',
releaseChannel: value as 'alpha' | 'beta' | 'latest',
},
});
}}
+1
View File
@@ -1,4 +1,5 @@
export * from './use-app-focus';
export * from './use-check-for-updates';
export * from './use-container-query';
export * from './use-fast-average-color';
export * from './use-hide-scrollbar';
@@ -0,0 +1,29 @@
import { useQuery } from '@tanstack/react-query';
import isElectron from 'is-electron';
import { useEffect, useState } from 'react';
const CHECK_FOR_UPDATES_INTERVAL_MS = 6 * 60 * 60 * 1000;
const utils = isElectron() ? window.api?.utils : null;
export const useCheckForUpdates = () => {
const [enablePeriodicCheck, setEnablePeriodicCheck] = useState(false);
// We want to skip the first check since it's already checked in the main process when the app is started
useEffect(() => {
const timer = setTimeout(() => setEnablePeriodicCheck(true), CHECK_FOR_UPDATES_INTERVAL_MS);
return () => clearTimeout(timer);
}, []);
const isEnabled =
enablePeriodicCheck &&
Boolean(isElectron() && utils?.checkForUpdates && !utils?.disableAutoUpdates?.());
return useQuery({
enabled: isEnabled,
queryFn: () => utils?.checkForUpdates?.(),
queryKey: ['app-check-for-updates'],
refetchInterval: CHECK_FOR_UPDATES_INTERVAL_MS,
refetchIntervalInBackground: true,
});
};
+187 -6
View File
@@ -20,11 +20,27 @@ import { Text } from '/@/shared/components/text/text';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jeffvli/feishin/releases';
const GITHUB_COMPARE_URL = 'https://api.github.com/repos/jeffvli/feishin/compare';
const RELEASES_TO_FETCH = 30;
interface GitHubCompareCommit {
commit: {
author: { date: string; name: string };
message: string;
};
html_url: string;
sha: string;
}
interface GitHubCompareResponse {
commits: GitHubCompareCommit[];
total_commits: number;
}
interface GitHubRelease {
body: null | string;
name: null | string;
prerelease: boolean;
published_at: string;
tag_name: string;
}
@@ -34,13 +50,22 @@ interface ReleaseNotesContentProps {
version: string;
}
function isAlphaVersion(version: string): boolean {
return version.includes('-alpha');
}
function parseVersionFromTag(tagName: string): string {
return tagName.startsWith('v') ? tagName.slice(1) : tagName;
}
function toTag(version: string): string {
return version.startsWith('v') ? version : `v${version}`;
}
const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) => {
const { t } = useTranslation();
const [selectedVersion, setSelectedVersion] = useState(version);
const isAlpha = isAlphaVersion(selectedVersion);
// Fetch list of recent releases for the selector
const { data: releasesList = [] } = useQuery({
@@ -54,6 +79,10 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
retry: 2,
});
const latestStableRelease = useMemo(() => {
return releasesList.find((r) => !r.prerelease);
}, [releasesList]);
const releaseOptions = useMemo(() => {
const options = releasesList.slice(0, RELEASES_TO_FETCH).map((r) => {
const v = parseVersionFromTag(r.tag_name);
@@ -70,14 +99,36 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
return options;
}, [releasesList, version]);
// For alpha: fetch commits between latest stable and current alpha
const {
data: compareData,
isError: isCompareError,
isLoading: isCompareLoading,
} = useQuery({
enabled: isAlpha && !!latestStableRelease,
queryFn: async () => {
const base = latestStableRelease!.tag_name;
const head = toTag(selectedVersion);
const response = await axios.get<GitHubCompareResponse>(
`${GITHUB_COMPARE_URL}/${base}...${head}`,
{ params: { per_page: 100 } },
);
return response.data;
},
queryKey: ['github-compare', latestStableRelease?.tag_name, selectedVersion],
retry: 2,
});
// For non-alpha: fetch release by tag
const {
data: releaseData,
isError,
isLoading,
} = useQuery({
enabled: !isAlpha,
queryFn: async () => {
const response = await axios.get<GitHubRelease>(
`${GITHUB_RELEASES_URL}/tags/v${selectedVersion}`,
`${GITHUB_RELEASES_URL}/tags/${toTag(selectedVersion)}`,
);
return response.data;
},
@@ -87,7 +138,7 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
// Convert markdown to HTML using GitHub's markdown API
const { data: htmlContent, isLoading: isConverting } = useQuery({
enabled: !!releaseData?.body,
enabled: !isAlpha && !!releaseData?.body,
queryFn: async () => {
const response = await axios.post(
'https://api.github.com/markdown',
@@ -136,7 +187,10 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
});
}, [htmlContent]);
if (isLoading || isConverting) {
const isLoadingState = isAlpha ? isCompareLoading : isLoading || isConverting;
const isErrorState = isAlpha ? isCompareError : isError || !releaseData;
if (isLoadingState) {
return (
<Center h={400}>
<Spinner />
@@ -144,7 +198,8 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
);
}
if (isError || !releaseData) {
if (isErrorState) {
const showCompareError = isAlpha && latestStableRelease;
return (
<Stack gap="md">
{releaseOptions.length > 1 && (
@@ -158,7 +213,11 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
<Group justify="flex-end">
<Button
component="a"
href={`https://github.com/jeffvli/feishin/releases/tag/v${selectedVersion}`}
href={
showCompareError
? `https://github.com/jeffvli/feishin/compare/${latestStableRelease.tag_name}...${toTag(selectedVersion)}`
: `https://github.com/jeffvli/feishin/releases/tag/${toTag(selectedVersion)}`
}
onClick={onDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
@@ -174,6 +233,128 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
);
}
if (isAlpha && !latestStableRelease) {
return (
<Stack gap="md">
{releaseOptions.length > 1 && (
<Select
data={releaseOptions}
onChange={(v) => v && setSelectedVersion(v)}
value={selectedVersion}
/>
)}
<Text isMuted size="sm">
{t('page.releasenotes.noStableReleaseToCompare', {
postProcess: 'sentenceCase',
})}
</Text>
<Group justify="flex-end">
<Button
component="a"
href={`https://github.com/jeffvli/feishin/releases/tag/${toTag(selectedVersion)}`}
onClick={onDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
variant="subtle"
>
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
<Button onClick={onDismiss} variant="filled">
{t('common.dismiss', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
);
}
if (isAlpha && compareData) {
const commits = compareData.commits ?? [];
const compareUrl = `https://github.com/jeffvli/feishin/compare/${latestStableRelease?.tag_name}...${toTag(selectedVersion)}`;
return (
<Stack gap="md">
{releaseOptions.length > 1 && (
<Select
data={releaseOptions}
onChange={(v) => v && setSelectedVersion(v)}
value={selectedVersion}
/>
)}
<Text isMuted size="sm">
{t('page.releasenotes.commitsSinceStable', {
postProcess: 'sentenceCase',
stable: latestStableRelease
? parseVersionFromTag(latestStableRelease.tag_name)
: '',
})}
</Text>
<ScrollArea
style={{
height: '400px',
}}
>
<Stack gap="xs">
{commits.length === 0 ? (
<Text isMuted size="sm">
{t('page.releasenotes.noNewCommits', {
postProcess: 'sentenceCase',
})}
</Text>
) : (
commits.map((c) => {
const firstLine = c.commit.message.split('\n')[0];
return (
<Group
gap="sm"
key={c.sha}
style={{ alignItems: 'flex-start' }}
wrap="nowrap"
>
<Text
size="sm"
style={{ flex: 1 }}
title={c.commit.message}
truncate
>
{firstLine}
</Text>
<Text isMuted size="xs">
{formatHrDateTime(c.commit.author.date)}
</Text>
<Button
component="a"
href={c.html_url}
rightSection={<Icon icon="externalLink" />}
size="compact-xs"
target="_blank"
variant="subtle"
>
{t('common.view', { postProcess: 'sentenceCase' })}
</Button>
</Group>
);
})
)}
</Stack>
</ScrollArea>
<Group justify="flex-end">
<Button
component="a"
href={compareUrl}
onClick={onDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
variant="subtle"
>
{t('action.viewMore', { postProcess: 'sentenceCase' })}
</Button>
<Button onClick={onDismiss} variant="filled">
{t('common.dismiss', { postProcess: 'titleCase' })}
</Button>
</Group>
</Stack>
);
}
return (
<Stack gap="md">
{releaseOptions.length > 1 && (
@@ -198,7 +379,7 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
<Group justify="flex-end">
<Button
component="a"
href={`https://github.com/jeffvli/feishin/releases/tag/v${selectedVersion}`}
href={`https://github.com/jeffvli/feishin/releases/tag/${toTag(selectedVersion)}`}
onClick={onDismiss}
rightSection={<Icon icon="externalLink" />}
target="_blank"
+1 -1
View File
@@ -588,7 +588,7 @@ const WindowSettingsSchema = z.object({
exitToTray: z.boolean(),
minimizeToTray: z.boolean(),
preventSleepOnPlayback: z.boolean(),
releaseChannel: z.enum(['beta', 'latest']),
releaseChannel: z.enum(['alpha', 'beta', 'latest']),
startMinimized: z.boolean(),
tray: z.boolean(),
windowBarStyle: z.nativeEnum(Platform),