mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-23 18:53:36 +02:00
Desktop: Seamless-Updates: triggering updates (#11079)
This commit is contained in:
parent
99696637b9
commit
a56f104fe8
@ -332,6 +332,10 @@ export default class ElectronAppWrapper {
|
|||||||
this.updaterService_.updateApp();
|
this.updaterService_.updateApp();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on('check-for-updates', () => {
|
||||||
|
void this.updaterService_.checkForUpdates(true);
|
||||||
|
});
|
||||||
|
|
||||||
// Let us register listeners on the window, so we can update the state
|
// Let us register listeners on the window, so we can update the state
|
||||||
// automatically (the listeners will be removed when the window is closed)
|
// automatically (the listeners will be removed when the window is closed)
|
||||||
// and restore the maximized or full screen state
|
// and restore the maximized or full screen state
|
||||||
@ -470,7 +474,7 @@ export default class ElectronAppWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Electron's autoUpdater has to be init from the main process
|
// Electron's autoUpdater has to be init from the main process
|
||||||
public async initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
public initializeAutoUpdaterService(logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
||||||
if (shim.isWindows() || shim.isMac()) {
|
if (shim.isWindows() || shim.isMac()) {
|
||||||
if (!this.updaterService_) {
|
if (!this.updaterService_) {
|
||||||
this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases);
|
this.updaterService_ = new AutoUpdaterService(this.win_, logger, devMode, includePreReleases);
|
||||||
@ -482,7 +486,7 @@ export default class ElectronAppWrapper {
|
|||||||
private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => {
|
private startPeriodicUpdateCheck = (updateInterval: number = defaultUpdateInterval): void => {
|
||||||
this.stopPeriodicUpdateCheck();
|
this.stopPeriodicUpdateCheck();
|
||||||
this.updatePollInterval_ = setInterval(() => {
|
this.updatePollInterval_ = setInterval(() => {
|
||||||
void this.updaterService_.checkForUpdates();
|
void this.updaterService_.checkForUpdates(false);
|
||||||
}, updateInterval);
|
}, updateInterval);
|
||||||
setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup);
|
setTimeout(this.updaterService_.checkForUpdates, initialUpdateStartup);
|
||||||
};
|
};
|
||||||
@ -491,6 +495,7 @@ export default class ElectronAppWrapper {
|
|||||||
if (this.updatePollInterval_) {
|
if (this.updatePollInterval_) {
|
||||||
clearInterval(this.updatePollInterval_);
|
clearInterval(this.updatePollInterval_);
|
||||||
this.updatePollInterval_ = null;
|
this.updatePollInterval_ = null;
|
||||||
|
this.updaterService_ = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -404,9 +404,9 @@ class Application extends BaseApplication {
|
|||||||
eventManager.on(EventName.ResourceChange, handleResourceChange);
|
eventManager.on(EventName.ResourceChange, handleResourceChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupAutoUpdaterService() {
|
private setupAutoUpdaterService() {
|
||||||
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||||
await bridge().electronApp().initializeAutoUpdaterService(
|
bridge().electronApp().initializeAutoUpdaterService(
|
||||||
Logger.create('AutoUpdaterService'),
|
Logger.create('AutoUpdaterService'),
|
||||||
Setting.value('env') === 'dev',
|
Setting.value('env') === 'dev',
|
||||||
Setting.value('autoUpdate.includePreReleases'),
|
Setting.value('autoUpdate.includePreReleases'),
|
||||||
@ -449,6 +449,8 @@ class Application extends BaseApplication {
|
|||||||
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
|
||||||
await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP));
|
await injectCustomStyles('appStyles', Setting.customCssFilePath(Setting.customCssFilenames.JOPLIN_APP));
|
||||||
|
|
||||||
|
this.setupAutoUpdaterService();
|
||||||
|
|
||||||
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
|
||||||
AlarmService.setLogger(reg.logger());
|
AlarmService.setLogger(reg.logger());
|
||||||
|
|
||||||
@ -698,8 +700,6 @@ class Application extends BaseApplication {
|
|||||||
SearchEngine.instance().scheduleSyncTables();
|
SearchEngine.instance().scheduleSyncTables();
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.setupAutoUpdaterService();
|
|
||||||
|
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// void populateDatabase(reg.db(), {
|
// void populateDatabase(reg.db(), {
|
||||||
// clearDatabase: true,
|
// clearDatabase: true,
|
||||||
|
@ -26,6 +26,7 @@ import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/Plug
|
|||||||
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
|
import { getListRendererById, getListRendererIds } from '@joplin/lib/services/noteList/renderers';
|
||||||
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
import useAsyncEffect from '@joplin/lib/hooks/useAsyncEffect';
|
||||||
import { EventName } from '@joplin/lib/eventManager';
|
import { EventName } from '@joplin/lib/eventManager';
|
||||||
|
import { ipcRenderer } from 'electron';
|
||||||
const packageInfo: PackageInfo = require('../packageInfo.js');
|
const packageInfo: PackageInfo = require('../packageInfo.js');
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
@ -575,9 +576,14 @@ function useMenu(props: Props) {
|
|||||||
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
|
toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.languages'], props['spellChecker.enabled']));
|
||||||
|
|
||||||
function _checkForUpdates() {
|
function _checkForUpdates() {
|
||||||
|
if (Setting.value('featureFlag.autoUpdaterServiceEnabled')) {
|
||||||
|
ipcRenderer.send('check-for-updates');
|
||||||
|
} else {
|
||||||
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function _showAbout() {
|
function _showAbout() {
|
||||||
const v = versionInfo(packageInfo, PluginService.instance().enabledPlugins(props.pluginSettings));
|
const v = versionInfo(packageInfo, PluginService.instance().enabledPlugins(props.pluginSettings));
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useContext, useCallback, useMemo } from 'react';
|
import { useContext, useCallback, useMemo, useRef } from 'react';
|
||||||
import { StateLastDeletion } from '@joplin/lib/reducer';
|
import { StateLastDeletion } from '@joplin/lib/reducer';
|
||||||
import { _, _n } from '@joplin/lib/locale';
|
import { _, _n } from '@joplin/lib/locale';
|
||||||
import NotyfContext from '../NotyfContext';
|
import NotyfContext from '../NotyfContext';
|
||||||
@ -9,6 +9,7 @@ import restoreItems from '@joplin/lib/services/trash/restoreItems';
|
|||||||
import { ModelType } from '@joplin/lib/BaseModel';
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
import { themeStyle } from '@joplin/lib/theme';
|
import { themeStyle } from '@joplin/lib/theme';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
|
import { NotyfNotification } from 'notyf';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
lastDeletion: StateLastDeletion;
|
lastDeletion: StateLastDeletion;
|
||||||
@ -19,6 +20,7 @@ interface Props {
|
|||||||
|
|
||||||
export default (props: Props) => {
|
export default (props: Props) => {
|
||||||
const notyfContext = useContext(NotyfContext);
|
const notyfContext = useContext(NotyfContext);
|
||||||
|
const notificationRef = useRef<NotyfNotification | null>(null);
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => {
|
||||||
return themeStyle(props.themeId);
|
return themeStyle(props.themeId);
|
||||||
@ -39,7 +41,8 @@ export default (props: Props) => {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
const onCancelClick = useCallback(async (event: any) => {
|
const onCancelClick = useCallback(async (event: any) => {
|
||||||
notyf.dismissAll();
|
notyf.dismiss(notificationRef.current);
|
||||||
|
notificationRef.current = null;
|
||||||
|
|
||||||
const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion'));
|
const lastDeletion: StateLastDeletion = JSON.parse(event.currentTarget.getAttribute('data-lastDeletion'));
|
||||||
|
|
||||||
@ -70,7 +73,8 @@ export default (props: Props) => {
|
|||||||
const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`;
|
const linkId = `deletion-notification-cancel-${Math.floor(Math.random() * 1000000)}`;
|
||||||
const cancelLabel = _('Cancel');
|
const cancelLabel = _('Cancel');
|
||||||
|
|
||||||
notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
|
const notification = notyf.success(`${msg} <a href="#" class="cancel" data-lastDeletion="${htmlentities(JSON.stringify(props.lastDeletion))}" id="${linkId}">${cancelLabel}</a>`);
|
||||||
|
notificationRef.current = notification;
|
||||||
|
|
||||||
const element: HTMLAnchorElement = await waitForElement(document, linkId);
|
const element: HTMLAnchorElement = await waitForElement(document, linkId);
|
||||||
if (event.cancelled) return;
|
if (event.cancelled) return;
|
||||||
|
@ -5,7 +5,7 @@ import NotyfContext from '../NotyfContext';
|
|||||||
import { UpdateInfo } from 'electron-updater';
|
import { UpdateInfo } from 'electron-updater';
|
||||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
|
import { AutoUpdaterEvents } from '../../services/autoUpdater/AutoUpdaterService';
|
||||||
import { NotyfNotification } from 'notyf';
|
import { NotyfEvent, NotyfNotification } from 'notyf';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { htmlentities } from '@joplin/utils/html';
|
import { htmlentities } from '@joplin/utils/html';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
@ -16,6 +16,7 @@ interface UpdateNotificationProps {
|
|||||||
|
|
||||||
export enum UpdateNotificationEvents {
|
export enum UpdateNotificationEvents {
|
||||||
ApplyUpdate = 'apply-update',
|
ApplyUpdate = 'apply-update',
|
||||||
|
UpdateNotAvailable = 'update-not-available',
|
||||||
Dismiss = 'dismiss-update-notification',
|
Dismiss = 'dismiss-update-notification',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,17 +87,46 @@ const UpdateNotification = ({ themeId }: UpdateNotificationProps) => {
|
|||||||
notificationRef.current = notification;
|
notificationRef.current = notification;
|
||||||
}, [notyf, theme]);
|
}, [notyf, theme]);
|
||||||
|
|
||||||
|
const handleUpdateNotAvailable = useCallback(() => {
|
||||||
|
if (notificationRef.current) return;
|
||||||
|
|
||||||
|
const noUpdateMessageHtml = htmlentities(_('No updates available'));
|
||||||
|
|
||||||
|
const messageHtml = `
|
||||||
|
<div class="update-notification" style="color: ${theme.color2};">
|
||||||
|
${noUpdateMessageHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const notification: NotyfNotification = notyf.open({
|
||||||
|
type: 'success',
|
||||||
|
message: messageHtml,
|
||||||
|
position: {
|
||||||
|
x: 'right',
|
||||||
|
y: 'bottom',
|
||||||
|
},
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.on(NotyfEvent.Dismiss, () => {
|
||||||
|
notificationRef.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationRef.current = notification;
|
||||||
|
}, [notyf, theme]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
ipcRenderer.on(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||||
|
ipcRenderer.on(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
|
||||||
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
document.addEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||||
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
document.addEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateDownloaded, handleUpdateDownloaded);
|
||||||
|
ipcRenderer.removeListener(AutoUpdaterEvents.UpdateNotAvailable, handleUpdateNotAvailable);
|
||||||
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
document.removeEventListener(UpdateNotificationEvents.ApplyUpdate, handleApplyUpdate);
|
||||||
document.removeEventListener(UpdateNotificationEvents.Dismiss, handleDismissNotification);
|
|
||||||
};
|
};
|
||||||
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded]);
|
}, [handleApplyUpdate, handleDismissNotification, handleUpdateDownloaded, handleUpdateNotAvailable]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -37,7 +37,7 @@ const supportedPlatformAssets: PlatformAssets = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface AutoUpdaterServiceInterface {
|
export interface AutoUpdaterServiceInterface {
|
||||||
checkForUpdates(): void;
|
checkForUpdates(isManualCheck: boolean): void;
|
||||||
updateApp(): void;
|
updateApp(): void;
|
||||||
fetchLatestRelease(includePreReleases: boolean): Promise<GitHubRelease>;
|
fetchLatestRelease(includePreReleases: boolean): Promise<GitHubRelease>;
|
||||||
getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string;
|
getDownloadUrlForPlatform(release: GitHubRelease, platform: string, arch: string): string;
|
||||||
@ -48,10 +48,11 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
|||||||
private logger_: LoggerWrapper;
|
private logger_: LoggerWrapper;
|
||||||
private devMode_: boolean;
|
private devMode_: boolean;
|
||||||
private enableDevMode = true; // force the updater to work in "dev" mode
|
private enableDevMode = true; // force the updater to work in "dev" mode
|
||||||
private enableAutoDownload = false; // automatically download an update when it is found
|
private enableAutoDownload = true; // automatically download an update when it is found
|
||||||
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
|
private autoInstallOnAppQuit = false; // automatically install the downloaded update once the user closes the application
|
||||||
private includePreReleases_ = false;
|
private includePreReleases_ = false;
|
||||||
private allowDowngrade = false;
|
private allowDowngrade = false;
|
||||||
|
private isManualCheckInProgress = false;
|
||||||
|
|
||||||
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
public constructor(mainWindow: BrowserWindow, logger: LoggerWrapper, devMode: boolean, includePreReleases: boolean) {
|
||||||
this.window_ = mainWindow;
|
this.window_ = mainWindow;
|
||||||
@ -61,8 +62,9 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
|||||||
this.configureAutoUpdater();
|
this.configureAutoUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
public checkForUpdates = async (): Promise<void> => {
|
public checkForUpdates = async (isManualCheck = false): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
this.isManualCheckInProgress = isManualCheck;
|
||||||
await this.checkForLatestRelease();
|
await this.checkForLatestRelease();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger_.error('Failed to check for updates:', error);
|
this.logger_.error('Failed to check for updates:', error);
|
||||||
@ -132,6 +134,7 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
|||||||
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
|
assetUrl = assetUrl.substring(0, assetUrl.lastIndexOf('/'));
|
||||||
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
|
autoUpdater.setFeedURL({ provider: 'generic', url: assetUrl });
|
||||||
await autoUpdater.checkForUpdates();
|
await autoUpdater.checkForUpdates();
|
||||||
|
this.isManualCheckInProgress = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger_.error(`Update download url failed: ${error.message}`);
|
this.logger_.error(`Update download url failed: ${error.message}`);
|
||||||
}
|
}
|
||||||
@ -167,6 +170,10 @@ export default class AutoUpdaterService implements AutoUpdaterServiceInterface {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
|
private onUpdateNotAvailable = (_info: UpdateInfo): void => {
|
||||||
|
if (this.isManualCheckInProgress) {
|
||||||
|
this.window_.webContents.send(AutoUpdaterEvents.UpdateNotAvailable);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger_.info('Update not available.');
|
this.logger_.info('Update not available.');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1127,7 +1127,7 @@ const builtInMetadata = (Setting: typeof SettingType) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
autoUpdateEnabled: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: false, appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
||||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') },
|
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/help/about/prereleases') },
|
||||||
|
|
||||||
'autoUploadCrashDumps': {
|
'autoUploadCrashDumps': {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user