Skip to content

Commit

Permalink
Merge pull request #450 from viktor44/feature/tray_icon
Browse files Browse the repository at this point in the history
Minimize to System Tray
  • Loading branch information
viktor44 authored Nov 17, 2024
2 parents aaf367b + e584f4f commit e251d5e
Show file tree
Hide file tree
Showing 29 changed files with 351 additions and 33 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
7 changes: 7 additions & 0 deletions packages/app/codegen-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ generates:
- typescript-operations
- typescript-react-apollo

./src/settings/components/SettingsMinimizeToTray/[email protected]:
documents: ./src/settings/components/SettingsMinimizeToTray/[email protected]
plugins:
- typescript
- typescript-operations
- typescript-react-apollo

./src/settings/components/SettingsDownloads/[email protected]:
documents: ./src/settings/components/SettingsDownloads/[email protected]
plugins:
Expand Down
4 changes: 2 additions & 2 deletions packages/app/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
4 changes: 2 additions & 2 deletions packages/app/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/app-main.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
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';
import { start } from './webui/webUIHandler';
import { enhanceSession } from './session';

export default class BrowserXAppMain extends EventEmitter {

init() {
this.initAppLifeCycle();
this.initProcessManagerAnalytics().catch(handleError());
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/app-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions packages/app/src/app/duck.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
});
Expand Down Expand Up @@ -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);

Expand Down
12 changes: 10 additions & 2 deletions packages/app/src/app/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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());
Expand All @@ -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;
Expand Down
22 changes: 22 additions & 0 deletions packages/app/src/app/sagas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -45,6 +46,7 @@ import { getWindow } from '../windows/selectors';
import {
getAppAutoLaunchEnabledStatus,
getAppHideMainMenuStatus,
getAppMinimizeToTrayStatus,
getPromptDownloadEnabled,
isFullyReady,
isKbdShortcutsOverlayVisible
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/app/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/app/types.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/database/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export const App = db.define(
type: Sequelize.BOOLEAN,
defaultValue: false,
},
minimizeToTray: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
downloadFolder: {
type: Sequelize.STRING,
},
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ type Query {
autoLaunchEnabled: Boolean,
""" Hide main menu (Windows, Linux) """
hideMainMenu: Boolean,
""" Minimize application to tray icon """
minimizeToTray: Boolean,

promptDownloadEnabled: Boolean,

Expand Down Expand Up @@ -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"""
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/persistence/local.backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,15 @@ 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'),
}),
mapObjectToState: async obj => Immutable.Map({
version: obj.version,
autoLaunchEnabled: obj.autoLaunchEnabled,
hideMainMenu: obj.hideMainMenu,
minimizeToTray: obj.minimizeToTray,
downloadFolder: obj.downloadFolder,
promptDownload: Boolean(obj.promptDownload),
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -111,4 +113,6 @@ export class BrowserWindowManagerService extends ServiceBase implements RPC.Inte
setProvider(provider: RPC.Node<BrowserWindowManagerProviderService>): Promise<void> {}
// @ts-ignore
hideMainMenu(hide: boolean): Promise<void> {}
// @ts-ignore
hideAllWindows(): Promise<void> {}
}
3 changes: 2 additions & 1 deletion packages/app/src/services/services/browser-window/main.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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),
Expand Down
6 changes: 6 additions & 0 deletions packages/app/src/services/services/browser-window/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,10 @@ export class BrowserWindowManagerServiceImpl extends BrowserWindowManagerService
});
await this.provider.setHideMainMenu(hide);
}

async hideAllWindows() {
BrowserWindow.getAllWindows().forEach((bw) => {
bw.hide();
});
}
}
15 changes: 14 additions & 1 deletion packages/app/src/services/services/electron-app/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ export class ElectronAppService extends ServiceBase implements RPC.Interface<Ele
dockSetBadge(badge: string): Promise<void> {}
// @ts-ignore
getAppMetadata(): Promise<AppMetadata> {}

// @ts-ignore
showTrayIcon(): Promise<void> {}
// @ts-ignore
hideTrayIcon(): Promise<void> {}
// @ts-ignore
addObserver(obs: RPC.ObserverNode<ElectronAppServiceObserver>): Promise<RPC.Subscription> {}
// @ts-ignore
setProvider(provider: RPC.Node<ElectronAppServiceProviderService>): Promise<void> {}
}

@service('electron-app')
Expand All @@ -41,3 +46,11 @@ export interface AppMetadata {
name: string,
version: string,
}

@service('browser-window')
export class ElectronAppServiceProviderService extends ServiceBase implements RPC.Interface<ElectronAppServiceProviderService> {
// @ts-ignore
showTrayIcon(): Promise<void> {}
// @ts-ignore
hideTrayIcon(): Promise<void> {}
}
Loading

0 comments on commit e251d5e

Please sign in to comment.