diff --git a/package.json b/package.json index afc1c03d..1db3d749 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "station-project", "private": true, "productName": "Station", - "version": "3.2.0-b3", + "version": "3.3.0-b1", "description": "Station", "homepage": "https://getstation.com", "author": { diff --git a/packages/app/codegen-local.yml b/packages/app/codegen-local.yml index bd508443..9f672511 100644 --- a/packages/app/codegen-local.yml +++ b/packages/app/codegen-local.yml @@ -30,6 +30,13 @@ generates: - typescript-operations - typescript-react-apollo + ./src/settings/components/SettingsMinimizeToTray/queries@local.gql.generated.tsx: + documents: ./src/settings/components/SettingsMinimizeToTray/queries@local.gql + plugins: + - typescript + - typescript-operations + - typescript-react-apollo + ./src/settings/components/SettingsDownloads/queries@local.gql.generated.tsx: documents: ./src/settings/components/SettingsDownloads/queries@local.gql plugins: diff --git a/packages/app/electron-builder.yml b/packages/app/electron-builder.yml index 1586e8d3..de30666d 100644 --- a/packages/app/electron-builder.yml +++ b/packages/app/electron-builder.yml @@ -62,8 +62,8 @@ linux: extraResources: - from: '../../.env.production' to: '.env.production' - - from: 'build/icon_512x512.png' - to: 'icon.png' + - from: 'src/static/icon-app.png' + to: 'icon-app.png' files: - '!**/*.map' diff --git a/packages/app/package.json b/packages/app/package.json index 029e40dd..a3cf8139 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "station-desktop-app", "productName": "Station", - "version": "3.2.0-b3", + "version": "3.3.0-b1", "description": "Station", "homepage": "https://getstation.com", "author": { @@ -69,7 +69,7 @@ "electron-chrome-extension": "https://github.com/viktor44/electron-chrome-extension/releases/download/v6.0.3/release.tar.gz", "electron-debug": "^3.2.0", "electron-log": "^2.2.14", - "electron-process-manager": "https://github.com/viktor44/electron-process-manager/releases/download/v1.1.0/release.tar.gz", + "electron-process-manager": "^1.2.0", "electron-updater": "^6.2.1", "electron-window-state": "5.0.3", "emoji-js": "^3.4.1", diff --git a/packages/app/src/app-main.js b/packages/app/src/app-main.js index ed9eca12..df3da3fb 100644 --- a/packages/app/src/app-main.js +++ b/packages/app/src/app-main.js @@ -1,7 +1,8 @@ import { EventEmitter } from 'events'; import { parse } from 'url'; -import { app, session, webContents } from 'electron'; +import { app, webContents, BrowserWindow, Menu, Tray, nativeImage } from 'electron'; import log from 'electron-log'; + import services from './services/servicesManager'; import { observer } from './services/lib/helpers'; import { handleError } from './services/api/helpers'; @@ -9,6 +10,7 @@ import { start } from './webui/webUIHandler'; import { enhanceSession } from './session'; export default class BrowserXAppMain extends EventEmitter { + init() { this.initAppLifeCycle(); this.initProcessManagerAnalytics().catch(handleError()); diff --git a/packages/app/src/app-worker.ts b/packages/app/src/app-worker.ts index 73382f89..9013529e 100644 --- a/packages/app/src/app-worker.ts +++ b/packages/app/src/app-worker.ts @@ -58,6 +58,7 @@ import MainWindowManager from './windows/utils/MainWindowManager'; import URLRouter from './urlrouter/URLRouter'; import { closeCurrentTab } from './tabs/duck'; import { BrowserWindowManagerProviderServiceImpl } from './services/services/browser-window/worker'; +import { ElectronAppServiceProviderServiceImpl } from './services/services/electron-app/worker'; export class BrowserXAppWorker { public store: StationStoreWorker; @@ -342,13 +343,16 @@ export class BrowserXAppWorker { } private async initAppLifeCycle() { + services.electronApp.setProvider(new ElectronAppServiceProviderServiceImpl(this.store)) // Don't wrap this in this.store.ready() // because we want the window to be created as soon as possible await this.mainWindowManager.create(); const onActivate = async () => { const isReady = await services.electronApp.isReady(); - if (!isReady) return; + if (!isReady) { + return; + } // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. await this.mainWindowManager.create(); diff --git a/packages/app/src/app/duck.js b/packages/app/src/app/duck.js index cc3a20d0..ed7958fd 100644 --- a/packages/app/src/app/duck.js +++ b/packages/app/src/app/duck.js @@ -12,6 +12,8 @@ export const SET_AUTO_LAUNCH_ENABLED = 'browserX/app/SET_AUTO_LAUNCH_ENABLED'; export const ENABLE_AUTO_LAUNCH = 'browserX/app/ENABLE_AUTO_LAUNCH'; export const SET_HIDE_MAIN_MENU = 'browserX/app/SET_HIDE_MAIN_MENU'; export const HIDE_MAIN_MENU = 'browserX/app/HIDE_MAIN_MENU'; +export const SET_MINIMIZE_TO_TRAY = 'browserX/app/SET_MINIMIZE_TO_TRAY'; +export const MINIMIZE_TO_TRAY = 'browserX/app/MINIMIZE_TO_TRAY'; export const INCLUDE_BETA_IN_UPDATES = 'browserX/app/INCLUDE_BETA_IN_UPDATES'; export const SET_INCLUDES_BETA_IN_UPDATES = 'browserX/app/SET_INCLUDES_BETA_IN_UPDATES'; export const SET_FULL_SCREEN_STATE = 'browserX/app/SET_FULL_SCREEN_STATE'; @@ -58,6 +60,14 @@ export const hideMainMenu = (hide) => ({ type: HIDE_MAIN_MENU, hide }); +export const setMinimizeToTray = (enabled) => ({ + type: SET_MINIMIZE_TO_TRAY, enabled +}); + +export const minimizeToTray = (enable) => ({ + type: MINIMIZE_TO_TRAY, enable +}); + export const includeBetaInUpdates = (include = true) => ({ type: INCLUDE_BETA_IN_UPDATES, include }); @@ -151,6 +161,9 @@ export default function app(state = new Map(), action) { case SET_HIDE_MAIN_MENU: return state.set('hideMainMenu', action.hide); + case SET_MINIMIZE_TO_TRAY: + return state.set('minimizeToTray', action.enabled); + case SET_INCLUDES_BETA_IN_UPDATES: return state.set('includesBetaInUpdates', action.included); diff --git a/packages/app/src/app/resolvers.ts b/packages/app/src/app/resolvers.ts index 2793dfce..de63af94 100644 --- a/packages/app/src/app/resolvers.ts +++ b/packages/app/src/app/resolvers.ts @@ -5,9 +5,9 @@ import { distinctUntilChanged, map } from 'rxjs/operators'; import { subscribeStore } from '../utils/observable'; import { Resolvers } from '../graphql/resolvers-types.generated'; -import { getAppAutoLaunchEnabledStatus, getAppHideMainMenuStatus, getPromptDownloadEnabled } from './selectors'; +import { getAppAutoLaunchEnabledStatus, getAppHideMainMenuStatus, getAppMinimizeToTrayStatus, getPromptDownloadEnabled } from './selectors'; import { getStationStatus } from '../app/selectors'; -import { enableAutoLaunch, hideMainMenu, togglePromptDownload } from './duck'; +import { enableAutoLaunch, hideMainMenu, minimizeToTray, togglePromptDownload } from './duck'; const resolvers: Resolvers = { Query: { @@ -19,6 +19,10 @@ const resolvers: Resolvers = { return subscribeStore(context.store, getAppHideMainMenuStatus) .pipe(map(Boolean), distinctUntilChanged()); }, + minimizeToTray: (_obj, _args, context) => { + return subscribeStore(context.store, getAppMinimizeToTrayStatus) + .pipe(map(Boolean), distinctUntilChanged()); + }, promptDownloadEnabled: (_obj, _args, context) => { return subscribeStore(context.store, getPromptDownloadEnabled) .pipe(map(Boolean), distinctUntilChanged()); @@ -40,6 +44,10 @@ const resolvers: Resolvers = { context.store.dispatch(hideMainMenu(args.hide)); return true; }, + setMinimizeToTray: (_obj, args, context) => { + context.store.dispatch(minimizeToTray(args.enabled)); + return true; + }, setPromptDownload: (_obj, args, context) => { context.store.dispatch(togglePromptDownload(args.enabled)); return true; diff --git a/packages/app/src/app/sagas.js b/packages/app/src/app/sagas.js index d15d76d4..406c80ad 100644 --- a/packages/app/src/app/sagas.js +++ b/packages/app/src/app/sagas.js @@ -26,6 +26,7 @@ import { CHANGE_APP_FOCUS_STATE, ENABLE_AUTO_LAUNCH, enableAutoLaunch, HIDE_MAIN_MENU, hideMainMenu, + MINIMIZE_TO_TRAY, minimizeToTray, INCLUDE_BETA_IN_UPDATES, keyboardLayoutChanged, OPEN_PROCESS_MANAGER, @@ -45,6 +46,7 @@ import { getWindow } from '../windows/selectors'; import { getAppAutoLaunchEnabledStatus, getAppHideMainMenuStatus, + getAppMinimizeToTrayStatus, getPromptDownloadEnabled, isFullyReady, isKbdShortcutsOverlayVisible @@ -101,6 +103,24 @@ function* sagaHideMainMenu({ hide }) { yield call([services.browserWindow, services.browserWindow.hideMainMenu], hide); } +function* sagaSyncMinimizeToTray() { + let isEnabled = yield select(getAppMinimizeToTrayStatus); + if (typeof isEnabled === 'undefined') { + isEnabled = false; + } + + yield put(minimizeToTray(isEnabled)); +} + +function* sagaMinimizeToTray({ enable }) { + if (enable) { + yield call([services.electronApp, services.electronApp.showTrayIcon]); + } + else { + yield call([services.electronApp, services.electronApp.hideTrayIcon]); + } +} + function* sagaTogglePromptDownload({ promptDownload }) { yield call( [services.download, services.download.setShouldShowPromptPathOnDownload], @@ -271,9 +291,11 @@ export default function* main(bxApp) { takeEveryWitness(READY, sagaPrepareQuit, bxApp), takeEveryWitness(REHYDRATION_COMPLETE, sagaSyncAutoLaunch), takeEveryWitness(REHYDRATION_COMPLETE, sagaSyncHideMainMenu), + takeEveryWitness(REHYDRATION_COMPLETE, sagaSyncMinimizeToTray), takeEveryWitness(REHYDRATION_COMPLETE, sagaSyncPromptDownload), takeEveryWitness(ENABLE_AUTO_LAUNCH, sagaEnableAutoLaunch), takeEveryWitness(HIDE_MAIN_MENU, sagaHideMainMenu), + takeEveryWitness(MINIMIZE_TO_TRAY, sagaMinimizeToTray), takeEveryWitness(TOGGLE_PROMPT_DOWNLOAD, sagaTogglePromptDownload), takeEveryWitness(INCLUDE_BETA_IN_UPDATES, sagaIncludeBetaInUpdates), takeEveryWitness(TOGGLE_MAXIMIZE, sagaSendToggleMaximize), diff --git a/packages/app/src/app/selectors.ts b/packages/app/src/app/selectors.ts index 75f4b055..89d99960 100644 --- a/packages/app/src/app/selectors.ts +++ b/packages/app/src/app/selectors.ts @@ -20,6 +20,9 @@ export const getAppAutoLaunchEnabledStatus = (state: StationState): boolean | un export const getAppHideMainMenuStatus = (state: StationState): boolean | undefined => getStationStatus(state).get('hideMainMenu'); +export const getAppMinimizeToTrayStatus = (state: StationState): boolean | undefined => + getStationStatus(state).get('minimizeToTray'); + export const areBetaIncludedInUpdates = (state: StationState): boolean => state.getIn(['app', 'includesBetaInUpdates'], false); diff --git a/packages/app/src/app/types.ts b/packages/app/src/app/types.ts index db72f6d6..e1df91a7 100644 --- a/packages/app/src/app/types.ts +++ b/packages/app/src/app/types.ts @@ -1,11 +1,13 @@ import { RecursiveImmutableMap } from '../types'; +// Global application properties export type StationApp = { appName: string, appVersion: string, includesBetaInUpdates: boolean, autoLaunchEnabled: boolean, hideMainMenu: boolean, + minimizeToTray: boolean, isFullScreen: boolean, defaultDownloadFolder: string, downloadFolder: string, diff --git a/packages/app/src/database/model.ts b/packages/app/src/database/model.ts index 002f1ddc..4c4ce96b 100644 --- a/packages/app/src/database/model.ts +++ b/packages/app/src/database/model.ts @@ -20,6 +20,10 @@ export const App = db.define( type: Sequelize.BOOLEAN, defaultValue: false, }, + minimizeToTray: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, downloadFolder: { type: Sequelize.STRING, }, diff --git a/packages/app/src/graphql/schema.graphql b/packages/app/src/graphql/schema.graphql index ab5306ae..bd6c1310 100644 --- a/packages/app/src/graphql/schema.graphql +++ b/packages/app/src/graphql/schema.graphql @@ -3,6 +3,8 @@ type Query { autoLaunchEnabled: Boolean, """ Hide main menu (Windows, Linux) """ hideMainMenu: Boolean, + """ Minimize application to tray icon """ + minimizeToTray: Boolean, promptDownloadEnabled: Boolean, @@ -49,6 +51,8 @@ type Mutation { """ Will set the value of hideMainMenu property. """ setHideMainMenu(hide: Boolean!): Boolean, + setMinimizeToTray(enabled: Boolean!): Boolean, + setPromptDownload(enabled: Boolean!):Boolean """Trigger an action to check for updates""" diff --git a/packages/app/src/persistence/local.backend.ts b/packages/app/src/persistence/local.backend.ts index 81696231..c5eb6c6d 100644 --- a/packages/app/src/persistence/local.backend.ts +++ b/packages/app/src/persistence/local.backend.ts @@ -181,6 +181,7 @@ export class AppProxy extends SingletonProxyMixin({ version: state.get('version'), autoLaunchEnabled: state.get('autoLaunchEnabled'), hideMainMenu: state.get('hideMainMenu'), + minimizeToTray: state.get('minimizeToTray'), downloadFolder: state.get('downloadFolder'), promptDownload: state.get('promptDownload'), }), @@ -188,6 +189,7 @@ export class AppProxy extends SingletonProxyMixin({ version: obj.version, autoLaunchEnabled: obj.autoLaunchEnabled, hideMainMenu: obj.hideMainMenu, + minimizeToTray: obj.minimizeToTray, downloadFolder: obj.downloadFolder, promptDownload: Boolean(obj.promptDownload), }), diff --git a/packages/app/src/persistence/umzug-runs/20241113000000-add-minimize-to-tray.js b/packages/app/src/persistence/umzug-runs/20241113000000-add-minimize-to-tray.js new file mode 100644 index 00000000..120b92f4 --- /dev/null +++ b/packages/app/src/persistence/umzug-runs/20241113000000-add-minimize-to-tray.js @@ -0,0 +1,11 @@ +/* eslint-disable no-unused-vars */ + +export default { + up(query, DataTypes) { + return query.addColumn('app', 'minimizeToTray', DataTypes.BOOLEAN); + }, + + down(query, DataTypes) { + return query.removeColumn('app', 'minimizeToTray'); + } +}; diff --git a/packages/app/src/services/services/browser-window/interface.ts b/packages/app/src/services/services/browser-window/interface.ts index 0f39f018..e660d2c7 100644 --- a/packages/app/src/services/services/browser-window/interface.ts +++ b/packages/app/src/services/services/browser-window/interface.ts @@ -84,6 +84,8 @@ export class BrowserWindowServiceObserver extends ServiceBase implements RPC.Int onContextMenu(params: Electron.ContextMenuParams): void {} // @ts-ignore onNewNotification(notificationId: string, props: NotificationProps): void {} + // @ts-ignore + onMinimize(): void {} } @service('browser-window') @@ -111,4 +113,6 @@ export class BrowserWindowManagerService extends ServiceBase implements RPC.Inte setProvider(provider: RPC.Node): Promise {} // @ts-ignore hideMainMenu(hide: boolean): Promise {} + // @ts-ignore + hideAllWindows(): Promise {} } diff --git a/packages/app/src/services/services/browser-window/main.ts b/packages/app/src/services/services/browser-window/main.ts index 966fcb62..408d1fd0 100644 --- a/packages/app/src/services/services/browser-window/main.ts +++ b/packages/app/src/services/services/browser-window/main.ts @@ -1,4 +1,4 @@ -import { BrowserWindow, ipcMain, screen, webContents, WebContents, WebPreferences, app } from 'electron'; +import { BrowserWindow, ipcMain, screen } from 'electron'; import * as remoteMain from '@electron/remote/main'; import * as windowStateKeeper from 'electron-window-state'; import { fromEvent } from 'rxjs'; @@ -162,6 +162,7 @@ export class BrowserWindowServiceImpl extends BrowserWindowService implements RP this.onAny('closed', obs.onClosed), this.onAny('enter-full-screen', obs.onEnterFullScreen), this.onAny('leave-full-screen', obs.onLeaveFullScreen), + this.onAny('minimize', obs.onMinimize), this.onAnyWebContents('did-finish-load', obs.onDidFinishLoad), this.onReadyToShow(obs.onReadyToShow), this.onContextMenu(obs.onContextMenu), diff --git a/packages/app/src/services/services/browser-window/manager.ts b/packages/app/src/services/services/browser-window/manager.ts index 3b67f7f9..1fead8c4 100644 --- a/packages/app/src/services/services/browser-window/manager.ts +++ b/packages/app/src/services/services/browser-window/manager.ts @@ -114,4 +114,10 @@ export class BrowserWindowManagerServiceImpl extends BrowserWindowManagerService }); await this.provider.setHideMainMenu(hide); } + + async hideAllWindows() { + BrowserWindow.getAllWindows().forEach((bw) => { + bw.hide(); + }); + } } diff --git a/packages/app/src/services/services/electron-app/interface.ts b/packages/app/src/services/services/electron-app/interface.ts index c71755dc..4ca5b147 100644 --- a/packages/app/src/services/services/electron-app/interface.ts +++ b/packages/app/src/services/services/electron-app/interface.ts @@ -25,9 +25,14 @@ export class ElectronAppService extends ServiceBase implements RPC.Interface {} // @ts-ignore getAppMetadata(): Promise {} - + // @ts-ignore + showTrayIcon(): Promise {} + // @ts-ignore + hideTrayIcon(): Promise {} // @ts-ignore addObserver(obs: RPC.ObserverNode): Promise {} + // @ts-ignore + setProvider(provider: RPC.Node): Promise {} } @service('electron-app') @@ -41,3 +46,11 @@ export interface AppMetadata { name: string, version: string, } + +@service('browser-window') +export class ElectronAppServiceProviderService extends ServiceBase implements RPC.Interface { + // @ts-ignore + showTrayIcon(): Promise {} + // @ts-ignore + hideTrayIcon(): Promise {} +} diff --git a/packages/app/src/services/services/electron-app/main.ts b/packages/app/src/services/services/electron-app/main.ts index 3953a62d..39e86b59 100644 --- a/packages/app/src/services/services/electron-app/main.ts +++ b/packages/app/src/services/services/electron-app/main.ts @@ -1,10 +1,13 @@ -import { app, session } from 'electron'; +import * as path from 'path'; +import { BrowserWindow, Menu, Tray, app, nativeImage, session } from 'electron'; import log from 'electron-log'; import * as globalTunnel from 'global-tunnel-ng'; import { fromEvent, Subject, Subscription } from 'rxjs'; + +import { isPackaged } from '../../../utils/env'; import { ServiceSubscription } from '../../lib/class'; import { RPC } from '../../lib/types'; -import { ElectronAppService, ElectronAppServiceObserver } from './interface'; +import { ElectronAppService, ElectronAppServiceObserver, ElectronAppServiceProviderService } from './interface'; import { ElectronAppPath } from './types'; const RESUME_QUIT_RECOVERY_DELAY = 5000; @@ -34,6 +37,8 @@ function initProxyResolver() { export class ElectronAppServiceImpl extends ElectronAppService implements RPC.Interface { private prepareQuitSubject: Subject; private appCanQuit: boolean; + private provider?: RPC.Node; + private tray?: Tray; constructor(uuid?: string) { super(uuid, { ready: false }); @@ -128,6 +133,25 @@ export class ElectronAppServiceImpl extends ElectronAppService implements RPC.In return new ServiceSubscription(subscriptions, obs); } + async showTrayIcon() { + if (!this.provider) { + throw new Error('missing provider service'); + } + this.tray = this.createTray(); + await this.provider.showTrayIcon(); + } + + async hideTrayIcon() { + if (!this.provider) { + throw new Error('missing provider service'); + } + if (this.tray) { + this.tray.destroy(); + this.tray = undefined; + } + await this.provider.hideTrayIcon(); + } + private initPrepareQuit() { app.on('before-quit', (event) => { if (!this.appCanQuit) { @@ -137,4 +161,54 @@ export class ElectronAppServiceImpl extends ElectronAppService implements RPC.In } }); } + + async setProvider(provider: RPC.Node) { + this.provider = provider; + } + + private getTrayIcon() { + const result = nativeImage.createFromPath( + isPackaged + ? path.resolve(process.resourcesPath, 'icon-app.png') + : path.resolve(__dirname, '../../../static/icon-app.png') + ); + result.setTemplateImage(true); + + return result; + } + + private showAllWindows() { + BrowserWindow.getAllWindows() + .reverse() + .forEach(win => { + if (win.webContents.id !== 1) { + win.show(); + } + }); + } + + private createTray() { + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Open', + type: 'normal', + click: () => { + this.showAllWindows(); + }, + }, + { + label: 'Exit', + type: 'normal', + click: () => { + app.quit() + } + }, + ]); + + const tray = new Tray(this.getTrayIcon()); + tray.addListener('double-click', () => this.showAllWindows()); + tray.setToolTip('Station'); + tray.setContextMenu(contextMenu); + return tray; + } } diff --git a/packages/app/src/services/services/electron-app/worker.ts b/packages/app/src/services/services/electron-app/worker.ts new file mode 100644 index 00000000..03b65605 --- /dev/null +++ b/packages/app/src/services/services/electron-app/worker.ts @@ -0,0 +1,22 @@ +// @ts-ignore: no declaration file +import { setMinimizeToTray } from '../../../app/duck'; +import { StationStoreWorker } from '../../../types'; +import { RPC } from '../../lib/types'; +import { ElectronAppServiceProviderService } from './interface'; + +export class ElectronAppServiceProviderServiceImpl extends ElectronAppServiceProviderService implements RPC.Interface { + store: StationStoreWorker; + + constructor(store: StationStoreWorker, uuid?: string) { + super(uuid); + this.store = store; + } + + async showTrayIcon() { + this.store.dispatch(setMinimizeToTray(true)); + } + + async hideTrayIcon() { + this.store.dispatch(setMinimizeToTray(false)); + } +} diff --git a/packages/app/src/settings/components/SettingsMinimizeToTray/SettingsMinimizeToTray.tsx b/packages/app/src/settings/components/SettingsMinimizeToTray/SettingsMinimizeToTray.tsx new file mode 100644 index 00000000..88f45342 --- /dev/null +++ b/packages/app/src/settings/components/SettingsMinimizeToTray/SettingsMinimizeToTray.tsx @@ -0,0 +1,89 @@ +import { Switcher } from '@getstation/theme'; +import * as React from 'react'; +// @ts-ignore: no declaration file +import injectSheet from 'react-jss'; +import { compose } from 'redux'; +import { withGetMinimizeToTrayStatus, withEnableMinimizeToTray } from './queries@local.gql.generated'; + +export interface Classes { + container: string, + item: string, + title: string, + settingName: string, + checkbox: string, + label: string, +} + +export interface Props { + classes?: Classes + isMinimizeToTray: boolean, + onMinimizeToTray: (enabled: boolean) => any + loading: boolean, +} + +const styles = { + container: { + maxWidth: '600px', + paddingTop: '10px', + paddingBottom: '10px', + }, + item: { + }, + settingName: { + marginBottom: 8, + textTransform: 'uppercase', + fontSize: 14, + fontWeight: 'bold', + }, + checkbox: { + '-webkit-appearance': 'checkbox', + marginRight: 10, + marginBottom: 20, + borderRadius: 2, + width: 14, + height: 14, + }, + label: { + }, +}; + +@injectSheet(styles) +class SettingsMinimizeToTray extends React.Component { + render() { + const { classes, loading, isMinimizeToTray } = this.props; + + const handleSwitcherChange = (e: React.ChangeEvent) => + this.props.onMinimizeToTray(e.target.checked); + return ( +
+
+

TRAY ICON

+ +
+ Minimize application to tray +
+
+
+ ); + } +} + +const connect = compose( + withGetMinimizeToTrayStatus({ + props: ({ data }) => ({ + loading: !data || data.loading, + isMinimizeToTray: !!data && Boolean(data.minimizeToTray), + }), + }), + withEnableMinimizeToTray({ + props: ({ mutate }) => ({ + onMinimizeToTray: (enabled: boolean) => mutate && mutate({ variables: { enabled } }), + }), + }), +); + +export default connect(SettingsMinimizeToTray); diff --git a/packages/app/src/settings/components/SettingsMinimizeToTray/queries@local.gql b/packages/app/src/settings/components/SettingsMinimizeToTray/queries@local.gql new file mode 100644 index 00000000..bca419bd --- /dev/null +++ b/packages/app/src/settings/components/SettingsMinimizeToTray/queries@local.gql @@ -0,0 +1,8 @@ + query GetMinimizeToTrayStatus @live @local { + minimizeToTray + } + + mutation EnableMinimizeToTray($enabled: Boolean!) @local{ + setMinimizeToTray(enabled: $enabled) + } + \ No newline at end of file diff --git a/packages/app/src/settings/components/SettingsPanelGeneral.tsx b/packages/app/src/settings/components/SettingsPanelGeneral.tsx index f62b4736..1354c34e 100644 --- a/packages/app/src/settings/components/SettingsPanelGeneral.tsx +++ b/packages/app/src/settings/components/SettingsPanelGeneral.tsx @@ -17,7 +17,8 @@ import SettingsDownloadFolder from './SettingsDownloads/SettingsDownloadFolder'; import SettingsOpenSourceInfo from './SettingsOpenSourceInfo'; import SettingsUpdatesButton from './SettingsUpdatesButton/SettingsUpdatesButton'; import SettingsHideMainMenu from './SettingsHideMainMenu/SettingsHideMainMenu'; -import { isDarwin } from '../../utils/process'; +import SettingsMinimizeToTray from './SettingsMinimizeToTray/SettingsMinimizeToTray'; +import { isDarwin, isWindows } from '../../utils/process'; export interface Classes { container: string, @@ -82,6 +83,8 @@ class SettingsPanelGeneralImpl extends React.PureComponent { { !isDarwin && } + { isWindows && } + { + services.browserWindow.hideAllWindows(); + }); + await super.load(); return this.window!; diff --git a/packages/app/test/jest/persistence/test-app.ts b/packages/app/test/jest/persistence/test-app.ts index 2fd8976f..7c6980f4 100644 --- a/packages/app/test/jest/persistence/test-app.ts +++ b/packages/app/test/jest/persistence/test-app.ts @@ -7,6 +7,7 @@ const appData = Immutable.Map({ version: 1, autoLaunchEnabled: true, hideMainMenu: true, + minimizeToTray: false, downloadFolder: '/any/path', promptDownload: false }); @@ -15,6 +16,7 @@ const appData2 = Immutable.Map({ version: 2, autoLaunchEnabled: true, hideMainMenu: true, + minimizeToTray: false, downloadFolder: '/any/path', promptDownload: true }); @@ -24,6 +26,7 @@ const wrongData = Immutable.Map({ unaivalableProp: true, autoLaunchEnabled: true, hideMainMenu: true, + minimizeToTray: false, downloadFolder: '/any/path', promptDownload: false }); @@ -36,6 +39,7 @@ const correctedData = { version: 1, autoLaunchEnabled: true, hideMainMenu: true, + minimizeToTray: false, downloadFolder: '/any/path', promptDownload: false }; diff --git a/yarn.lock b/yarn.lock index 31f02840..b3cc6fd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2809,6 +2809,18 @@ __metadata: languageName: node linkType: hard +"@getstation/electron-process-reporter@npm:^2.0.0": + version: 2.0.0 + resolution: "@getstation/electron-process-reporter@npm:2.0.0" + dependencies: + memoizee: "npm:^0.4.17" + pidtree: "npm:^0.6.0" + pidusage: "npm:3.0.2" + rxjs: "npm:^6.6.7" + checksum: 10c0/af4afd13f81104efe1ee14fdb4a86bea8ed8c8aae8514e9794948b3440e479b2977226817201c3261cc4f2f5e6c58544858867e990943dfc95d5cb8c33b9496d + languageName: node + linkType: hard + "@getstation/fetch-favicon@npm:^0.0.4-rc.2": version: 0.0.4-rc.2 resolution: "@getstation/fetch-favicon@npm:0.0.4-rc.2" @@ -14070,24 +14082,12 @@ __metadata: languageName: node linkType: hard -"electron-process-manager@https://github.com/viktor44/electron-process-manager/releases/download/v1.1.0/release.tar.gz": - version: 1.1.0 - resolution: "electron-process-manager@https://github.com/viktor44/electron-process-manager/releases/download/v1.1.0/release.tar.gz" - dependencies: - electron-process-reporter: "https://github.com/viktor44/electron-process-reporter/releases/download/v1.5.0/release.tar.gz" - checksum: 10c0/7bff2d2e3b19fe73599c7bd514709b460939b5e8c1819b3cbe8541422d9cfc197779f539f504e8f3718545dad092f1de9bc4b1371bd9b254fc76a6f4ede853f5 - languageName: node - linkType: hard - -"electron-process-reporter@https://github.com/viktor44/electron-process-reporter/releases/download/v1.5.0/release.tar.gz": - version: 1.5.0 - resolution: "electron-process-reporter@https://github.com/viktor44/electron-process-reporter/releases/download/v1.5.0/release.tar.gz" +"electron-process-manager@npm:^1.2.0": + version: 1.2.0 + resolution: "electron-process-manager@npm:1.2.0" dependencies: - memoizee: "npm:^0.4.17" - pidtree: "npm:^0.6.0" - pidusage: "npm:3.0.2" - rxjs: "npm:^6.6.7" - checksum: 10c0/5bdabfd46f3026af71df4128f13d2d395788931afa7dab23370252c6b3f4d417f3b561db72f136db606096bb531076a22e237019ec94486301d3668b015fcd25 + "@getstation/electron-process-reporter": "npm:^2.0.0" + checksum: 10c0/1e6baeac7e28ddbdd04b109420a9f61e9f002e54ecb0afc007fa3441e5b29f40c86e547849743e76949063b7c13416407712b782a6084190e8bd1897c794128d languageName: node linkType: hard @@ -29387,7 +29387,7 @@ __metadata: electron-debug: "npm:^3.2.0" electron-log: "npm:^2.2.14" electron-mocha: "npm:^12.1.0" - electron-process-manager: "https://github.com/viktor44/electron-process-manager/releases/download/v1.1.0/release.tar.gz" + electron-process-manager: "npm:^1.2.0" electron-updater: "npm:^6.2.1" electron-webpack: "npm:^2.8.2" electron-window-state: "npm:5.0.3"