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
+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),