mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
add handlers and setting for nightly release
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
});
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user