mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +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).
|
# 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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
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
|
required: false
|
||||||
type: string
|
type: string
|
||||||
schedule:
|
schedule:
|
||||||
@@ -14,7 +14,27 @@ on:
|
|||||||
- cron: '0 11 * * *'
|
- cron: '0 11 * * *'
|
||||||
|
|
||||||
jobs:
|
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:
|
prepare:
|
||||||
|
needs: check-new-commits
|
||||||
|
if: needs.check-new-commits.outputs.has_new_commits == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.version.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
@@ -30,7 +50,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
- name: Set date-based nightly version
|
- name: Set date-based alpha version
|
||||||
id: version
|
id: version
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
@@ -70,15 +90,15 @@ jobs:
|
|||||||
$pst = [TimeZoneInfo]::FindSystemTimeZoneById('America/Los_Angeles')
|
$pst = [TimeZoneInfo]::FindSystemTimeZoneById('America/Los_Angeles')
|
||||||
$dateInPst = [TimeZoneInfo]::ConvertTimeFromUtc([DateTime]::UtcNow, $pst)
|
$dateInPst = [TimeZoneInfo]::ConvertTimeFromUtc([DateTime]::UtcNow, $pst)
|
||||||
$dateStr = $dateInPst.ToString("yyyyMMdd")
|
$dateStr = $dateInPst.ToString("yyyyMMdd")
|
||||||
$nightlyVersion = "$inputVersion-nightly-$dateStr"
|
$alphaVersion = "$inputVersion-alpha-$dateStr"
|
||||||
Write-Host "Nightly version: $nightlyVersion"
|
Write-Host "Alpha version: $alphaVersion"
|
||||||
|
|
||||||
# Update package.json
|
# Update package.json
|
||||||
$packageJson = Get-Content package.json | ConvertFrom-Json
|
$packageJson = Get-Content package.json | ConvertFrom-Json
|
||||||
$packageJson.version = $nightlyVersion
|
$packageJson.version = $alphaVersion
|
||||||
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
|
$packageJson | ConvertTo-Json -Depth 10 | Set-Content package.json
|
||||||
|
|
||||||
echo "version=$nightlyVersion" >> $env:GITHUB_OUTPUT
|
echo "version=$alphaVersion" >> $env:GITHUB_OUTPUT
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
needs: prepare
|
needs: prepare
|
||||||
@@ -132,7 +152,7 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
pnpm run publish:win:nightly
|
pnpm run publish:win:alpha
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish to R2 (macOS)
|
- name: Build and Publish to R2 (macOS)
|
||||||
@@ -143,7 +163,7 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
pnpm run publish:mac:nightly
|
pnpm run publish:mac:alpha
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish to R2 (Linux)
|
- name: Build and Publish to R2 (Linux)
|
||||||
@@ -154,7 +174,7 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
pnpm run publish:linux:nightly
|
pnpm run publish:linux:alpha
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
|
|
||||||
- name: Build and Publish to R2 (Linux ARM64)
|
- name: Build and Publish to R2 (Linux ARM64)
|
||||||
@@ -165,5 +185,5 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
pnpm run publish:linux-arm64:nightly
|
pnpm run publish:linux-arm64:alpha
|
||||||
on_retry_command: pnpm cache delete
|
on_retry_command: pnpm cache delete
|
||||||
+4
-4
@@ -47,16 +47,16 @@
|
|||||||
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
|
"package:win:pr": "pnpm run build && electron-builder --win --publish never",
|
||||||
"publish:linux": "pnpm run build && electron-builder --publish always --linux",
|
"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": "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: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: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": "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: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": "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: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",
|
"start": "electron-vite preview",
|
||||||
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
"typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
|
|||||||
@@ -448,6 +448,11 @@
|
|||||||
"radioList": {
|
"radioList": {
|
||||||
"title": "radio stations"
|
"title": "radio stations"
|
||||||
},
|
},
|
||||||
|
"releasenotes": {
|
||||||
|
"commitsSinceStable": "commits since {{stable}}",
|
||||||
|
"noNewCommits": "no new commits in this range",
|
||||||
|
"noStableReleaseToCompare": "no stable release available to compare with"
|
||||||
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "$t(entity.favorite, {\"count\": 2})"
|
"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_description": "sets the path to the custom font to use for the application",
|
||||||
"customFontPath": "custom font path",
|
"customFontPath": "custom font path",
|
||||||
"disableAutomaticUpdates": "disable automatic updates",
|
"disableAutomaticUpdates": "disable automatic updates",
|
||||||
|
"releaseChannel_optionAlpha": "alpha (nightly)",
|
||||||
"releaseChannel_optionBeta": "beta",
|
"releaseChannel_optionBeta": "beta",
|
||||||
"releaseChannel_optionLatest": "latest",
|
"releaseChannel_optionLatest": "latest",
|
||||||
"releaseChannel": "release channel",
|
"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",
|
"disableLibraryUpdateOnStartup": "disable checking for new versions on startup",
|
||||||
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
"discordApplicationId_description": "the application id for {{discord}} rich presence (defaults to {{defaultId}})",
|
||||||
"discordApplicationId": "{{discord}} application id",
|
"discordApplicationId": "{{discord}} application id",
|
||||||
|
|||||||
+127
-26
@@ -18,7 +18,7 @@ import {
|
|||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronLocalShortcut from 'electron-localshortcut';
|
import electronLocalShortcut from 'electron-localshortcut';
|
||||||
import log from 'electron-log/main';
|
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 { access, constants } from 'fs';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
|
|
||||||
@@ -40,40 +40,111 @@ import './features';
|
|||||||
|
|
||||||
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
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() {
|
constructor() {
|
||||||
|
const updater = createAlphaUpdaterInstance();
|
||||||
log.transports.file.level = 'info';
|
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');
|
class AppUpdater {
|
||||||
const releaseChannel = store.get('release_channel');
|
constructor() {
|
||||||
const isNotConfigured = !releaseChannel;
|
const effectiveChannel = store.get('release_channel') as string;
|
||||||
|
console.log('Effective update channel:', effectiveChannel);
|
||||||
console.log('Release channel: ', releaseChannel);
|
if (effectiveChannel === 'alpha') {
|
||||||
console.log('Is beta version: ', isBetaVersion);
|
return new AlphaAppUpdater();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configureAndGetUpdater();
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
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' }]);
|
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||||
|
|
||||||
process.on('uncaughtException', (error: any) => {
|
process.on('uncaughtException', (error: any) => {
|
||||||
@@ -359,6 +430,36 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
return mainWindow?.webContents.session.clearCache();
|
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', () => {
|
ipcMain.on('app-restart', () => {
|
||||||
// Fix for .AppImage
|
// Fix for .AppImage
|
||||||
if (process.env.APPIMAGE) {
|
if (process.env.APPIMAGE) {
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ const download = (url: string) => {
|
|||||||
ipcRenderer.send('download-url', url);
|
ipcRenderer.send('download-url', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkForUpdates = (): Promise<{ updateAvailable: boolean; version?: string }> => {
|
||||||
|
return ipcRenderer.invoke('app-check-for-updates');
|
||||||
|
};
|
||||||
|
|
||||||
const forceGarbageCollection = (): boolean => {
|
const forceGarbageCollection = (): boolean => {
|
||||||
try {
|
try {
|
||||||
if (typeof global.gc === 'function') {
|
if (typeof global.gc === 'function') {
|
||||||
@@ -58,6 +62,7 @@ const forceGarbageCollection = (): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const utils = {
|
export const utils = {
|
||||||
|
checkForUpdates,
|
||||||
disableAutoUpdates,
|
disableAutoUpdates,
|
||||||
download,
|
download,
|
||||||
forceGarbageCollection,
|
forceGarbageCollection,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
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 { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
import { useCssSettings, useHotkeySettings, useLanguage } from '/@/renderer/store';
|
||||||
@@ -38,6 +39,7 @@ export const App = () => {
|
|||||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||||
|
|
||||||
useSyncSettingsToMain();
|
useSyncSettingsToMain();
|
||||||
|
useCheckForUpdates();
|
||||||
|
|
||||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ export const UpdateSettings = memo(() => {
|
|||||||
}),
|
}),
|
||||||
value: 'beta',
|
value: 'beta',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('setting.releaseChannel', {
|
||||||
|
context: 'optionAlpha',
|
||||||
|
postProcess: 'titleCase',
|
||||||
|
}),
|
||||||
|
value: 'alpha',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
(localSettings?.get('release_channel') as string | undefined) || 'latest'
|
(localSettings?.get('release_channel') as string | undefined) || 'latest'
|
||||||
@@ -50,7 +57,7 @@ export const UpdateSettings = memo(() => {
|
|||||||
localSettings?.set('release_channel', value);
|
localSettings?.set('release_channel', value);
|
||||||
setSettings({
|
setSettings({
|
||||||
window: {
|
window: {
|
||||||
releaseChannel: value as 'beta' | 'latest',
|
releaseChannel: value as 'alpha' | 'beta' | 'latest',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './use-app-focus';
|
export * from './use-app-focus';
|
||||||
|
export * from './use-check-for-updates';
|
||||||
export * from './use-container-query';
|
export * from './use-container-query';
|
||||||
export * from './use-fast-average-color';
|
export * from './use-fast-average-color';
|
||||||
export * from './use-hide-scrollbar';
|
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';
|
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
|
||||||
|
|
||||||
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jeffvli/feishin/releases';
|
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;
|
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 {
|
interface GitHubRelease {
|
||||||
body: null | string;
|
body: null | string;
|
||||||
name: null | string;
|
name: null | string;
|
||||||
|
prerelease: boolean;
|
||||||
published_at: string;
|
published_at: string;
|
||||||
tag_name: string;
|
tag_name: string;
|
||||||
}
|
}
|
||||||
@@ -34,13 +50,22 @@ interface ReleaseNotesContentProps {
|
|||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAlphaVersion(version: string): boolean {
|
||||||
|
return version.includes('-alpha');
|
||||||
|
}
|
||||||
|
|
||||||
function parseVersionFromTag(tagName: string): string {
|
function parseVersionFromTag(tagName: string): string {
|
||||||
return tagName.startsWith('v') ? tagName.slice(1) : tagName;
|
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 ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedVersion, setSelectedVersion] = useState(version);
|
const [selectedVersion, setSelectedVersion] = useState(version);
|
||||||
|
const isAlpha = isAlphaVersion(selectedVersion);
|
||||||
|
|
||||||
// Fetch list of recent releases for the selector
|
// Fetch list of recent releases for the selector
|
||||||
const { data: releasesList = [] } = useQuery({
|
const { data: releasesList = [] } = useQuery({
|
||||||
@@ -54,6 +79,10 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
retry: 2,
|
retry: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const latestStableRelease = useMemo(() => {
|
||||||
|
return releasesList.find((r) => !r.prerelease);
|
||||||
|
}, [releasesList]);
|
||||||
|
|
||||||
const releaseOptions = useMemo(() => {
|
const releaseOptions = useMemo(() => {
|
||||||
const options = releasesList.slice(0, RELEASES_TO_FETCH).map((r) => {
|
const options = releasesList.slice(0, RELEASES_TO_FETCH).map((r) => {
|
||||||
const v = parseVersionFromTag(r.tag_name);
|
const v = parseVersionFromTag(r.tag_name);
|
||||||
@@ -70,14 +99,36 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
return options;
|
return options;
|
||||||
}, [releasesList, version]);
|
}, [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 {
|
const {
|
||||||
data: releaseData,
|
data: releaseData,
|
||||||
isError,
|
isError,
|
||||||
isLoading,
|
isLoading,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
|
enabled: !isAlpha,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await axios.get<GitHubRelease>(
|
const response = await axios.get<GitHubRelease>(
|
||||||
`${GITHUB_RELEASES_URL}/tags/v${selectedVersion}`,
|
`${GITHUB_RELEASES_URL}/tags/${toTag(selectedVersion)}`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -87,7 +138,7 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
|
|
||||||
// Convert markdown to HTML using GitHub's markdown API
|
// Convert markdown to HTML using GitHub's markdown API
|
||||||
const { data: htmlContent, isLoading: isConverting } = useQuery({
|
const { data: htmlContent, isLoading: isConverting } = useQuery({
|
||||||
enabled: !!releaseData?.body,
|
enabled: !isAlpha && !!releaseData?.body,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
'https://api.github.com/markdown',
|
'https://api.github.com/markdown',
|
||||||
@@ -136,7 +187,10 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
});
|
});
|
||||||
}, [htmlContent]);
|
}, [htmlContent]);
|
||||||
|
|
||||||
if (isLoading || isConverting) {
|
const isLoadingState = isAlpha ? isCompareLoading : isLoading || isConverting;
|
||||||
|
const isErrorState = isAlpha ? isCompareError : isError || !releaseData;
|
||||||
|
|
||||||
|
if (isLoadingState) {
|
||||||
return (
|
return (
|
||||||
<Center h={400}>
|
<Center h={400}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -144,7 +198,8 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError || !releaseData) {
|
if (isErrorState) {
|
||||||
|
const showCompareError = isAlpha && latestStableRelease;
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{releaseOptions.length > 1 && (
|
{releaseOptions.length > 1 && (
|
||||||
@@ -158,7 +213,11 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
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}
|
onClick={onDismiss}
|
||||||
rightSection={<Icon icon="externalLink" />}
|
rightSection={<Icon icon="externalLink" />}
|
||||||
target="_blank"
|
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 (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{releaseOptions.length > 1 && (
|
{releaseOptions.length > 1 && (
|
||||||
@@ -198,7 +379,7 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
|||||||
<Group justify="flex-end">
|
<Group justify="flex-end">
|
||||||
<Button
|
<Button
|
||||||
component="a"
|
component="a"
|
||||||
href={`https://github.com/jeffvli/feishin/releases/tag/v${selectedVersion}`}
|
href={`https://github.com/jeffvli/feishin/releases/tag/${toTag(selectedVersion)}`}
|
||||||
onClick={onDismiss}
|
onClick={onDismiss}
|
||||||
rightSection={<Icon icon="externalLink" />}
|
rightSection={<Icon icon="externalLink" />}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ const WindowSettingsSchema = z.object({
|
|||||||
exitToTray: z.boolean(),
|
exitToTray: z.boolean(),
|
||||||
minimizeToTray: z.boolean(),
|
minimizeToTray: z.boolean(),
|
||||||
preventSleepOnPlayback: z.boolean(),
|
preventSleepOnPlayback: z.boolean(),
|
||||||
releaseChannel: z.enum(['beta', 'latest']),
|
releaseChannel: z.enum(['alpha', 'beta', 'latest']),
|
||||||
startMinimized: z.boolean(),
|
startMinimized: z.boolean(),
|
||||||
tray: z.boolean(),
|
tray: z.boolean(),
|
||||||
windowBarStyle: z.nativeEnum(Platform),
|
windowBarStyle: z.nativeEnum(Platform),
|
||||||
|
|||||||
Reference in New Issue
Block a user