mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-21 18:06:30 +02:00
refactor periodic update check to use github releases instead of electron-builder (#2259)
- resolves an issue related to electron-builder continuing to download and install updates despite only calling checkForUpdates - adds indicator to settings / app menu on new version found
This commit is contained in:
@@ -644,54 +644,6 @@ 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 effectiveChannel = store.get('release_channel') as string;
|
||||
let result: null | UpdateCheckResult;
|
||||
let updater: UpdaterInstance;
|
||||
|
||||
if (effectiveChannel === 'alpha') {
|
||||
const best = await checkAllChannelsAndGetBest();
|
||||
result = best.result;
|
||||
updater = best.updater;
|
||||
} else {
|
||||
updater = configureAndGetUpdater();
|
||||
result = await updater.checkForUpdates();
|
||||
}
|
||||
|
||||
const updateAvailable = result?.isUpdateAvailable ?? false;
|
||||
console.log('Update available:', updateAvailable);
|
||||
if (updateAvailable && store.get('disable_auto_updates') !== true) {
|
||||
if (isMacOS()) {
|
||||
getMainWindow()?.webContents.send(
|
||||
'update-available',
|
||||
result?.updateInfo?.version,
|
||||
);
|
||||
} else {
|
||||
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) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { UpdateAvailableButton } from '/@/renderer/features/settings/components/update-available-button';
|
||||
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||
@@ -14,9 +15,10 @@ import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
export type SettingsHeaderProps = {
|
||||
setSearch: (search: string) => void;
|
||||
showUpdateAvailable?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
|
||||
export const SettingsHeader = ({ setSearch, showUpdateAvailable }: SettingsHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { reset } = useSettingsStoreActions();
|
||||
const search = useSettingSearchContext();
|
||||
@@ -48,6 +50,7 @@ export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
|
||||
</LibraryHeaderBar.Title>
|
||||
</Group>
|
||||
<Group>
|
||||
{showUpdateAvailable && <UpdateAvailableButton />}
|
||||
<SearchInput
|
||||
defaultValue={search}
|
||||
onChange={(event) => setSearch(event.target.value.toLocaleLowerCase())}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const SettingsContextModal = () => {
|
||||
|
||||
return (
|
||||
<SettingSearchContext.Provider value={search}>
|
||||
<SettingsHeader setSearch={setSearch} />
|
||||
<SettingsHeader setSearch={setSearch} showUpdateAvailable />
|
||||
<SettingsContent />
|
||||
</SettingSearchContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { toTag } from '/@/renderer/hooks';
|
||||
import { useLatestVersion } from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
|
||||
export const UpdateAvailableButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentVersion, isUpdateAvailable, latestVersion } = useLatestVersion();
|
||||
|
||||
if (!isUpdateAvailable || !latestVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
component="a"
|
||||
href={`https://github.com/jeffvli/feishin/releases/tag/${toTag(latestVersion || currentVersion)}`}
|
||||
size="compact-sm"
|
||||
target="_blank"
|
||||
variant="filled"
|
||||
>
|
||||
{t('common.newVersionAvailable')}: v{latestVersion}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -4,11 +4,11 @@ import { Fragment, ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate } from 'react-router';
|
||||
|
||||
import packageJson from '../../../../../package.json';
|
||||
import styles from './app-menu.module.css';
|
||||
|
||||
import { isServerLock } from '/@/renderer/features/action-required/utils/window-properties';
|
||||
import { ServerList } from '/@/renderer/features/servers/components/server-list';
|
||||
import { UpdateAvailableButton } from '/@/renderer/features/settings/components/update-available-button';
|
||||
import { openSettingsModal } from '/@/renderer/features/settings/utils/open-settings-modal';
|
||||
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
|
||||
import { openReleaseNotesModal } from '/@/renderer/release-notes-modal';
|
||||
@@ -18,10 +18,12 @@ import {
|
||||
useCommandPalette,
|
||||
useCurrentServer,
|
||||
useGeneralSettings,
|
||||
useLatestVersion,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { DropdownMenu, MenuItemProps } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
@@ -138,6 +140,8 @@ export const AppMenu = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const { currentVersion } = useLatestVersion();
|
||||
|
||||
const serverHeaderMenuItems: MenuItem[] = currentServer
|
||||
? [
|
||||
{
|
||||
@@ -270,10 +274,10 @@ export const AppMenu = () => {
|
||||
{
|
||||
icon: 'brandGitHub',
|
||||
id: 'version',
|
||||
label: t('page.appMenu.version', { version: packageJson.version }),
|
||||
label: t('page.appMenu.version', { version: currentVersion }),
|
||||
onClick: () =>
|
||||
openReleaseNotesModal(
|
||||
t('common.newVersion', { version: packageJson.version }) as string,
|
||||
t('common.newVersion', { version: currentVersion }) as string,
|
||||
),
|
||||
type: 'item',
|
||||
},
|
||||
@@ -301,6 +305,15 @@ export const AppMenu = () => {
|
||||
},
|
||||
type: 'conditional-item',
|
||||
},
|
||||
{
|
||||
component: (
|
||||
<Flex align="center" justify="center" w="100%">
|
||||
<UpdateAvailableButton />
|
||||
</Flex>
|
||||
),
|
||||
id: 'update-available',
|
||||
type: 'custom',
|
||||
},
|
||||
{
|
||||
id: 'divider-5',
|
||||
type: 'divider',
|
||||
|
||||
@@ -2,6 +2,7 @@ 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-github-releases';
|
||||
export * from './use-hide-scrollbar';
|
||||
export * from './use-hotkeys';
|
||||
export * from './use-is-mounted';
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { parseVersionFromTag, useGithubLatestRelease } from '/@/renderer/hooks/use-github-releases';
|
||||
import { useAppStore } from '/@/renderer/store';
|
||||
|
||||
// 6 hours
|
||||
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);
|
||||
const setLatestVersion = useAppStore((state) => state.actions.setLatestVersion);
|
||||
|
||||
// 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'],
|
||||
const query = useGithubLatestRelease({
|
||||
refetchInterval: CHECK_FOR_UPDATES_INTERVAL_MS,
|
||||
refetchIntervalInBackground: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data) {
|
||||
setLatestVersion(parseVersionFromTag(query.data.tag_name));
|
||||
}
|
||||
}, [query.data, setLatestVersion]);
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
|
||||
export const GITHUB_RELEASES_URL = 'https://api.github.com/repos/jeffvli/feishin/releases';
|
||||
export const RELEASES_TO_FETCH = 30;
|
||||
|
||||
export interface GitHubRelease {
|
||||
body: null | string;
|
||||
name: null | string;
|
||||
prerelease: boolean;
|
||||
published_at: string;
|
||||
tag_name: string;
|
||||
}
|
||||
|
||||
export function parseVersionFromTag(tagName: string): string {
|
||||
return tagName.startsWith('v') ? tagName.slice(1) : tagName;
|
||||
}
|
||||
|
||||
export function toTag(version: string): string {
|
||||
return version.startsWith('v') ? version : `v${version}`;
|
||||
}
|
||||
|
||||
export const useGithubReleasesList = (perPage = RELEASES_TO_FETCH) => {
|
||||
return useQuery({
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<GitHubRelease[]>(GITHUB_RELEASES_URL, {
|
||||
params: { per_page: perPage },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
queryKey: ['github-releases-list', perPage],
|
||||
retry: 2,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGithubLatestRelease = (options?: {
|
||||
refetchInterval?: number;
|
||||
refetchIntervalInBackground?: boolean;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<GitHubRelease>(`${GITHUB_RELEASES_URL}/latest`);
|
||||
return response.data;
|
||||
},
|
||||
queryKey: ['github-latest-release'],
|
||||
retry: 2,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
@@ -7,6 +7,14 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import packageJson from '../../package.json';
|
||||
|
||||
import {
|
||||
GITHUB_RELEASES_URL,
|
||||
type GitHubRelease,
|
||||
parseVersionFromTag,
|
||||
RELEASES_TO_FETCH,
|
||||
toTag,
|
||||
useGithubReleasesList,
|
||||
} from '/@/renderer/hooks/use-github-releases';
|
||||
import { formatHrDateTime } from '/@/renderer/utils/format';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
@@ -19,9 +27,7 @@ import { Stack } from '/@/shared/components/stack/stack';
|
||||
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: {
|
||||
@@ -37,14 +43,6 @@ interface GitHubCompareResponse {
|
||||
total_commits: number;
|
||||
}
|
||||
|
||||
interface GitHubRelease {
|
||||
body: null | string;
|
||||
name: null | string;
|
||||
prerelease: boolean;
|
||||
published_at: string;
|
||||
tag_name: string;
|
||||
}
|
||||
|
||||
interface ReleaseNotesContentProps {
|
||||
onDismiss: () => void;
|
||||
version: string;
|
||||
@@ -54,30 +52,13 @@ 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({
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<GitHubRelease[]>(GITHUB_RELEASES_URL, {
|
||||
params: { per_page: RELEASES_TO_FETCH },
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
queryKey: ['github-releases-list'],
|
||||
retry: 2,
|
||||
});
|
||||
const { data: releasesList = [] } = useGithubReleasesList();
|
||||
|
||||
const latestStableRelease = useMemo(() => {
|
||||
return releasesList.find((r) => !r.prerelease);
|
||||
|
||||
@@ -2,11 +2,15 @@ import type { ItemListStateItem } from '/@/renderer/components/item-list/helpers
|
||||
import type { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
import merge from 'lodash/merge';
|
||||
import semverGt from 'semver/functions/gt';
|
||||
import semverValid from 'semver/functions/valid';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import packageJson from '../../../package.json';
|
||||
|
||||
import { AlbumListSort, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { Platform } from '/@/shared/types/types';
|
||||
|
||||
@@ -24,6 +28,7 @@ export interface AppSlice extends AppState {
|
||||
setGenreIdsMode: (mode: 'and' | 'or') => void;
|
||||
setGenreSelectMode: (mode: 'multi' | 'single') => void;
|
||||
setGlobalExpanded: (value: GlobalExpandedState | null) => void;
|
||||
setLatestVersion: (version: null | string) => void;
|
||||
setPageSidebar: (key: string, value: boolean) => void;
|
||||
setPrivateMode: (enabled: boolean) => void;
|
||||
setShowTimeRemaining: (enabled: boolean) => void;
|
||||
@@ -52,6 +57,7 @@ export interface AppState {
|
||||
genreSelectMode: 'multi' | 'single';
|
||||
globalExpanded: GlobalExpandedState | null;
|
||||
isReorderingQueue: boolean;
|
||||
latestVersion: null | string;
|
||||
pageSidebar: Record<string, boolean>;
|
||||
platform: Platform;
|
||||
privateMode: boolean;
|
||||
@@ -157,6 +163,11 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
||||
state.globalExpanded = value;
|
||||
});
|
||||
},
|
||||
setLatestVersion: (version) => {
|
||||
set((state) => {
|
||||
state.latestVersion = version;
|
||||
});
|
||||
},
|
||||
setPageSidebar: (key, value) => {
|
||||
set((state) => {
|
||||
state.pageSidebar[key] = value;
|
||||
@@ -219,6 +230,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
||||
genreSelectMode: 'multi',
|
||||
globalExpanded: null,
|
||||
isReorderingQueue: false,
|
||||
latestVersion: null,
|
||||
pageSidebar: {
|
||||
album: true,
|
||||
song: true,
|
||||
@@ -261,7 +273,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
||||
name: 'store_app',
|
||||
partialize: (state) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- ignore non-persisted state
|
||||
const { globalExpanded: _, ...rest } = state;
|
||||
const { globalExpanded: _, latestVersion: __, ...rest } = state;
|
||||
return rest;
|
||||
},
|
||||
version: 5,
|
||||
@@ -307,6 +319,18 @@ export const useGlobalExpanded = () => useAppStore((state) => state.globalExpand
|
||||
|
||||
export const useSetGlobalExpanded = () => useAppStore((state) => state.actions.setGlobalExpanded);
|
||||
|
||||
export const useLatestVersion = () => {
|
||||
const latestVersion = useAppStore((state) => state.latestVersion);
|
||||
const currentVersion = packageJson.version;
|
||||
const isUpdateAvailable =
|
||||
!!latestVersion &&
|
||||
!!semverValid(latestVersion) &&
|
||||
!!semverValid(currentVersion) &&
|
||||
semverGt(latestVersion, currentVersion);
|
||||
|
||||
return { currentVersion, isUpdateAvailable, latestVersion };
|
||||
};
|
||||
|
||||
export const useGlobalExpandedState = () => {
|
||||
const globalExpanded = useGlobalExpanded();
|
||||
const setGlobalExpanded = useSetGlobalExpanded();
|
||||
|
||||
Reference in New Issue
Block a user