diff --git a/src/browser/ipc/ipc-renderer.ts b/src/browser/ipc/ipc-renderer.ts index 5060ce2..879bf57 100644 --- a/src/browser/ipc/ipc-renderer.ts +++ b/src/browser/ipc/ipc-renderer.ts @@ -8,7 +8,7 @@ import { SettingService, SettingServiceToken } from '../../common/services/setti import { ClipboardService, ClipboardServiceToken } from '../../common/services/clipboard-service'; import logger from '../../electron-shared/logger'; import { GlobalHistoryService, GlobalHistoryServiceToken } from '../../common/services/global-history-service'; -import { WindowEvents } from '../../common/window-events'; +import { WindowEvent } from '../../common/window-event'; import { SettingChannel } from '../../electron-shared/ipc/ipc-channel-setting'; import { SearchChannel } from '../../electron-shared/ipc/ipc-channel-search'; import { AutoUpdaterChannel } from '../../electron-shared/ipc/ipc-channel-auto-updater'; @@ -16,6 +16,7 @@ import { AppChannel } from '../../electron-shared/ipc/ipc-channel-app'; import { GlobalShotCutChannel } from '../../electron-shared/ipc/ipc-channel-global-shot-cut'; import { LoginChannel } from '../../electron-shared/ipc/ipc-channel-login'; import { settingChanged } from '../pages/setting/setting-store'; +import { appHotKeyPressed, handleWindowEvent } from '../pages/transient-store'; function registerStorageEventListener(store: Store) { logger.debug('register storage event listener'); @@ -24,7 +25,7 @@ function registerStorageEventListener(store: Store) { // listen setting change ipcRenderer.answerMain(SettingChannel.SETTING_CHANGE, async (data: any) => { - if (getCurrentWindowId()===WindowId.SEARCH) { + if (getCurrentWindowId()===WindowId.HOME) { const settingService = rendererContainer.get(SettingServiceToken); await settingService.handleSettingChange(data.newValue, data.oldValue); } @@ -37,7 +38,7 @@ function registerStorageEventListener(store: Store) { return data.newValue; }); - if (getCurrentWindowId()===WindowId.SEARCH) { + if (getCurrentWindowId()===WindowId.HOME) { // listen clipboard event const clipboardService = rendererContainer.get(ClipboardServiceToken); clipboardService.onClipboardTextChange(((newText, oldText) => { @@ -49,9 +50,7 @@ function registerStorageEventListener(store: Store) { // listen global shot cut event ipcRenderer.answerMain(GlobalShotCutChannel.APP_HOT_KEY_PRESSED, async () => { logger.log(GlobalShotCutChannel.APP_HOT_KEY_PRESSED, new Date()); - store.dispatch({ - type: '_transient/appHotKeyPressed', - }); + appHotKeyPressed() }); // listen app event ipcRenderer.answerMain(AppChannel.APP_QUITING, async () => { @@ -110,11 +109,12 @@ function listenAutoUpdaterEvent(store) { function listenWindowEvents(store: Store) { WindowId.values().forEach(windowId => { - WindowEvents.values().forEach(event => { - const channelName = windowId.getEventChannelName(event); + WindowEvent.values().forEach(event => { + const channelName = event.getIpcChannelName(windowId) ipcRenderer.answerMain( channelName, async () => { logger.log(channelName); + handleWindowEvent(windowId, event) store.dispatch({ type: '_transient/' + channelName }); }); }); diff --git a/src/browser/pages/popup/popup-page.tsx b/src/browser/pages/popup/popup-page.tsx index 30c3046..b899c96 100644 --- a/src/browser/pages/popup/popup-page.tsx +++ b/src/browser/pages/popup/popup-page.tsx @@ -6,7 +6,7 @@ import { SearchState } from '../search/search-model'; import { Button, Icon } from 'antd'; import logger from '../../../electron-shared/logger'; import { rendererContainer } from "../../../common/container/renderer-container"; -import { SearchUiService, SearchUiServiceToken } from "../../../common/services/search-ui-service"; +import { HomeUiService, SearchUiServiceToken } from "../../../common/services/home-ui-service"; import { ClipboardService, ClipboardServiceToken } from "../../../common/services/clipboard-service"; import { PopupUiService, PopupUiServiceToken } from "../../../common/services/popup-ui-service"; import { removeNormalizeStyle } from '../../utils/style-utils'; @@ -33,7 +33,7 @@ const PopupPage = () => { // const searchState: SearchState = useSelector((state: any) => state.search); // const { pinned } = searchState; - const searchUiService = rendererContainer.get(SearchUiServiceToken); + const searchUiService = rendererContainer.get(SearchUiServiceToken); const clipboardService = rendererContainer.get(ClipboardServiceToken); const popupUiService = rendererContainer.get(PopupUiServiceToken); diff --git a/src/browser/pages/root-model.ts b/src/browser/pages/root-model.ts index a94bc3e..416a11c 100644 --- a/src/browser/pages/root-model.ts +++ b/src/browser/pages/root-model.ts @@ -73,7 +73,7 @@ const effects = { }, * login() { console.log('login'); - yield call([loginUiService, loginUiService.open]); + yield call([loginUiService, loginUiService.show]); }, * [LoginChannel.LOGIN_CODE_RECEIVED](action) { const { payload } = action; diff --git a/src/browser/pages/search/input/search-input.tsx b/src/browser/pages/search/input/search-input.tsx index 9719a8f..3807a72 100644 --- a/src/browser/pages/search/input/search-input.tsx +++ b/src/browser/pages/search/input/search-input.tsx @@ -6,6 +6,7 @@ import { SearchInputState } from './search-input-model'; import { usePrevious } from '../../../hooks/use-previous'; import styled from 'styled-components'; import { useDispatch, useSelector } from 'react-redux'; +import { focusSearchInput } from '../../transient-store'; const Suggestion = styled.div` width: 100%; @@ -68,17 +69,11 @@ export default () => { text, }); } - dispatch({ - type: '_transient/mergeState', - payload: { focusInput: true }, - }); + focusSearchInput() }} onBlur={() => { setOpen(false); - dispatch({ - type: '_transient/mergeState', - payload: { focusInput: false }, - }); + focusSearchInput() }} value={text} open={open} diff --git a/src/browser/pages/search/search-model.ts b/src/browser/pages/search/search-model.ts index 36ef678..216d9e9 100644 --- a/src/browser/pages/search/search-model.ts +++ b/src/browser/pages/search/search-model.ts @@ -1,6 +1,6 @@ import { call, cancel, delay, fork, put, select, take } from '@redux-saga/core/effects'; import { Model } from '../../redux/common/redux-model'; -import { SearchUiService, SearchUiServiceToken } from '../../../common/services/search-ui-service'; +import { HomeUiService, SearchUiServiceToken } from '../../../common/services/home-ui-service'; import { rendererContainer } from '../../../common/container/renderer-container'; import { TransientState } from '../transient-model'; import { UserProfile } from '../../../electron-shared/user-profile/user-profile'; @@ -43,8 +43,8 @@ interface SyncHistoryPageAction { const effects = { * togglePinned() { - const searchUiService = rendererContainer.get(SearchUiServiceToken); - const pinned = yield call([searchUiService, searchUiService.togglePin]); + const homeUIService = rendererContainer.get(SearchUiServiceToken); + const pinned = yield call([homeUIService, homeUIService.togglePin]); yield put({ type: 'search/mergeState', payload: { pinned }, diff --git a/src/browser/pages/search/search-page.tsx b/src/browser/pages/search/search-page.tsx index 87f421e..9361361 100644 --- a/src/browser/pages/search/search-page.tsx +++ b/src/browser/pages/search/search-page.tsx @@ -17,6 +17,7 @@ import { useParams } from 'react-router-dom'; import { SearchResultType } from '@noob9527/noob-dict-core'; import { NetworkEngineId } from '@noob9527/noob-dict-net-engines'; import { TransientState } from '../transient-model'; +import { useTransientStore } from '../transient-store'; const SearchPage = styled.div` height: 100vh; @@ -55,7 +56,7 @@ export default () => { const routerState = useSelector((state: any) => state.router); const searchState: SearchState = useSelector((state: any) => state.search); const searchPanelState: SearchPanelState = useSelector((state: any) => state.searchPanel); - const transientState: TransientState = useSelector((state: any) => state._transient); + const ecDictAvailable = useTransientStore.use.ecDictAvailable() const matched = /engine_view\/(\w+)/ .exec(routerState.location.pathname); @@ -106,7 +107,7 @@ export default () => { - {transientState.ecDictAvailable + {ecDictAvailable ? () :null } diff --git a/src/browser/pages/transient-model.ts b/src/browser/pages/transient-model.ts index 3111d22..8c3ec23 100644 --- a/src/browser/pages/transient-model.ts +++ b/src/browser/pages/transient-model.ts @@ -1,19 +1,19 @@ import { Model } from '../redux/common/redux-model'; import { call, put, select } from '@redux-saga/core/effects'; import { rendererContainer } from '../../common/container/renderer-container'; -import { SearchUiService, SearchUiServiceToken } from '../../common/services/search-ui-service'; +import { HomeUiService, SearchUiServiceToken } from '../../common/services/home-ui-service'; import { ClipboardService, ClipboardServiceToken } from '../../common/services/clipboard-service'; import { WindowId } from '../../common/window-id'; import { getCurrentWindowId } from '../utils/window-utils'; import logger from '../../electron-shared/logger'; import { AppService, AppServiceToken } from '../../common/services/app-service'; import { UserProfile } from '../../electron-shared/user-profile/user-profile'; -import { WindowEvents } from '../../common/window-events'; +import { WindowEvent } from '../../common/window-event'; import { EcDictSearchService } from '../../electron-renderer/services/ecdict-search-service'; import { EcDictSearchServiceToken } from '../../common/services/search-service'; import { LocalDbService, LocalDbServiceToken } from '../../common/services/db/local-db-service'; -const searchUiService = rendererContainer.get(SearchUiServiceToken); +const searchUiService = rendererContainer.get(SearchUiServiceToken); const appService = rendererContainer.get(AppServiceToken); export interface TransientState { @@ -46,7 +46,7 @@ interface ShowSearchWindowAction { const effects = { * showSearchWindow(action: ShowSearchWindowAction) { logger.log(action); - yield call([searchUiService, searchUiService.showSearchWindow]); + yield call([searchUiService, searchUiService.show]); yield put({ type: '_transient/mergeState', payload: { @@ -55,10 +55,10 @@ const effects = { }); }, * hideSearchWindow() { - yield call([searchUiService, searchUiService.hideSearchWindow]); + yield call([searchUiService, searchUiService.hide]); }, * topSearchWindow() { - yield call([searchUiService, searchUiService.showSearchWindow]); + yield call([searchUiService, searchUiService.show]); // yield put({ // type: '_transient/mergeState', // payload: { @@ -132,8 +132,8 @@ const effects = { }, }; -[WindowEvents.show, WindowEvents.restore].forEach(event => - effects[WindowId.SEARCH.getEventChannelName(event)] = function * () { +[WindowEvent.show, WindowEvent.restore].forEach(event => + effects[event.getIpcChannelName(WindowId.HOME)] = function * () { yield put({ type: '_transient/searchWindowOpened', }); @@ -149,8 +149,8 @@ const reducers = { }, }; -[WindowEvents.hide, WindowEvents.minimize].forEach(event => - reducers[WindowId.SEARCH.getEventChannelName(event)] = function (state, action: any) { +[WindowEvent.hide, WindowEvent.minimize].forEach(event => + reducers[event.getIpcChannelName(WindowId.HOME)] = function (state, action: any) { return { ...state, isSearchWindowOpen: false, diff --git a/src/browser/pages/transient-store.ts b/src/browser/pages/transient-store.ts index bfbe6bd..0223fdd 100644 --- a/src/browser/pages/transient-store.ts +++ b/src/browser/pages/transient-store.ts @@ -10,15 +10,21 @@ import { LocalDbService, LocalDbServiceToken, } from '../../common/services/db/local-db-service' -import { call } from '@redux-saga/core/effects' -import { createSelectors } from '../zustand/create-selectors'; +import { createSelectors } from '../zustand/create-selectors' +import { WindowEvent } from '../../common/window-event' +import { + HomeUiService, + SearchUiServiceToken, +} from '../../common/services/home-ui-service' const appService = rendererContainer.get(AppServiceToken) const ecDictSearchService = rendererContainer.get( - EcDictSearchServiceToken + EcDictSearchServiceToken, ) const localDbService = rendererContainer.get(LocalDbServiceToken) +const searchUiService = + rendererContainer.get(SearchUiServiceToken) interface TransientState { focusInput: boolean @@ -45,9 +51,66 @@ export async function setEcDictAvailable() { ecDictAvailable: available, }) } + export async function setLocalDbAvailable() { const available = await localDbService.fetchAvailable() useTransientStoreBase.setState({ localDbAvailable: available, }) } + +export function handleWindowEvent(windowId: WindowId, event: WindowEvent) { + if (windowId !== WindowId.HOME) return + switch (event) { + case WindowEvent.hide: + case WindowEvent.minimize: + useTransientStoreBase.setState({ + isSearchWindowOpen: false, + }) + break + case WindowEvent.show: + case WindowEvent.restore: + useTransientStoreBase.setState({ + isSearchWindowOpen: true, + }) + break + } +} + +export function showSearchWindow(focusInput?: boolean) { + searchUiService.show() + useTransientStoreBase.setState({ + focusInput: focusInput ?? true, + }) +} + +export function hideSearchWindow() { + searchUiService.hide() +} + +export function topSearchWindow() { + searchUiService.top() + focusSearchInput() +} + +export function appHotKeyPressed() { + const state = useTransientStoreBase.getState() + if (state.isSearchWindowOpen) { + if (state.focusInput) { + // toggle + // if search input is focused, we hide the window + hideSearchWindow() + } else { + // else, we top then focus on input + topSearchWindow() + } + } else { + showSearchWindow() + } +} + +export function focusSearchInput() { + useTransientStoreBase.setState({ + focusInput: true, + }) +} diff --git a/src/browser/utils/window-utils.ts b/src/browser/utils/window-utils.ts index e68fedd..bd733a0 100644 --- a/src/browser/utils/window-utils.ts +++ b/src/browser/utils/window-utils.ts @@ -3,7 +3,7 @@ import { WindowId } from '../../common/window-id'; function getCurrentWindowId() { switch (window?.location?.hash) { case '#/search': - return WindowId.SEARCH; + return WindowId.HOME; case '#/setting': return WindowId.SETTING; case '#/popup': @@ -13,7 +13,7 @@ function getCurrentWindowId() { case '#/developer': return WindowId.DEVELOPER; default: - return WindowId.SEARCH; + return WindowId.HOME; } } diff --git a/src/common/services/search-ui-service.ts b/src/common/services/home-ui-service.ts similarity index 55% rename from src/common/services/search-ui-service.ts rename to src/common/services/home-ui-service.ts index 537c580..de4285b 100644 --- a/src/common/services/search-ui-service.ts +++ b/src/common/services/home-ui-service.ts @@ -1,13 +1,13 @@ -export const SearchUiServiceToken = Symbol.for('search-ui-service'); +export const SearchUiServiceToken = Symbol.for('search-ui-service') -export interface SearchUiService { - toggleSearchWindow(): Promise +export interface HomeUiService { + toggle() - showSearchWindow() + show() - hideSearchWindow() + hide() - topSearchWindow() + top() search(option: { text: string }) diff --git a/src/common/services/login-ui-service.ts b/src/common/services/login-ui-service.ts index bbbcf8a..630520c 100644 --- a/src/common/services/login-ui-service.ts +++ b/src/common/services/login-ui-service.ts @@ -1,5 +1,5 @@ -export const LoginUiServiceToken = Symbol.for('login-ui-service'); +export const LoginUiServiceToken = Symbol.for('login-ui-service') export interface LoginUiService { - open(): Promise + show() } diff --git a/src/common/services/setting-ui-service.ts b/src/common/services/setting-ui-service.ts index 2633918..2012f05 100644 --- a/src/common/services/setting-ui-service.ts +++ b/src/common/services/setting-ui-service.ts @@ -1,5 +1,5 @@ export const SettingUiServiceToken = Symbol.for('setting-ui-service'); export interface SettingUiService { - open(): Promise + show() } diff --git a/src/common/window-command.ts b/src/common/window-command.ts new file mode 100644 index 0000000..d5356e9 --- /dev/null +++ b/src/common/window-command.ts @@ -0,0 +1,57 @@ +import { EnumFactory, EnumClass, EnumValue } from 'effective-enum'; +import { WindowId } from './window-id'; + +@EnumClass +class WindowCommand extends EnumFactory() { + @EnumValue + static readonly show = new WindowCommand('show'); + @EnumValue + static readonly hide = new WindowCommand('hide'); + @EnumValue + static readonly toggle = new WindowCommand('toggle'); + // @EnumValue + // static readonly restore = new WindowCommands('restore'); + // @EnumValue + // static readonly minimize = new WindowCommands('minimize'); + // @EnumValue + // static readonly focus = new WindowCommands('focus'); + @EnumValue + static readonly close = new WindowCommand('close'); + @EnumValue + static readonly top = new WindowCommand('top'); + + constructor( + public override name: string, + ) { + super(); + } + + /** + * WARN: + * todo: + * to work with ipc-decorator + * we simply transform the name into upper case. + * this may introduce problems in future + * e.g. consider a command consists of multiple words? + * + * e.g. + * - HOME/COMMAND/SHOW + * - HOME/COMMAND/HIDE + * - HOME/COMMAND/CLOSE + * + * @param windowId + */ + getIpcChannelName( + windowId: WindowId + ): string { + return `${windowId}/COMMAND/${this.name.toUpperCase()}`; + } + + override toString(): string { + return this.name; + } +} + +export { + WindowCommand, +} diff --git a/src/common/window-event.ts b/src/common/window-event.ts new file mode 100644 index 0000000..af9d5d3 --- /dev/null +++ b/src/common/window-event.ts @@ -0,0 +1,48 @@ +import { EnumFactory, EnumClass, EnumValue } from 'effective-enum'; +import { WindowId } from './window-id'; + +@EnumClass +class WindowEvent extends EnumFactory() { + @EnumValue + static readonly show = new WindowEvent('show'); + @EnumValue + static readonly hide = new WindowEvent('hide'); + @EnumValue + static readonly restore = new WindowEvent('restore'); + @EnumValue + static readonly minimize = new WindowEvent('minimize'); + @EnumValue + static readonly focus = new WindowEvent('focus'); + @EnumValue + static readonly blur = new WindowEvent('blur'); + @EnumValue + static readonly closed = new WindowEvent('closed'); + + constructor( + public override name: string, + ) { + super(); + } + + /** + * e.g. + * - HOME/EVENT/show + * - HOME/EVENT/hide + * - HOME/EVENT/closed + * + * @param windowId + */ + getIpcChannelName( + windowId: WindowId + ): string { + return `${windowId}/EVENT/${this}`; + } + + override toString(): string { + return this.name; + } +} + +export { + WindowEvent, +} diff --git a/src/common/window-events.ts b/src/common/window-events.ts deleted file mode 100644 index 8b43210..0000000 --- a/src/common/window-events.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { EnumFactory, EnumClass, EnumValue } from 'effective-enum'; - -@EnumClass -class WindowEvents extends EnumFactory() { - @EnumValue - static readonly show = new WindowEvents('show'); - @EnumValue - static readonly hide = new WindowEvents('hide'); - @EnumValue - static readonly restore = new WindowEvents('restore'); - @EnumValue - static readonly minimize = new WindowEvents('minimize'); - @EnumValue - static readonly focus = new WindowEvents('focus'); - @EnumValue - static readonly blur = new WindowEvents('blur'); - @EnumValue - static readonly closed = new WindowEvents('closed'); - - constructor( - public name: string, - ) { - super(); - } - - toString(): string { - return this.name; - } -} - -export { - WindowEvents, -} diff --git a/src/common/window-id.ts b/src/common/window-id.ts index cbedb3b..eab8048 100644 --- a/src/common/window-id.ts +++ b/src/common/window-id.ts @@ -1,31 +1,21 @@ -import { EnumFactory, EnumClass, EnumValue } from 'effective-enum'; -import { WindowEvents } from './window-events'; +import { EnumClass, EnumFactory, EnumValue } from 'effective-enum' @EnumClass class WindowId extends EnumFactory() { @EnumValue - static readonly SEARCH = new WindowId('SEARCH'); + static readonly HOME = new WindowId('HOME') @EnumValue - static readonly SETTING = new WindowId('SETTING'); + static readonly SETTING = new WindowId('SETTING') @EnumValue - static readonly POPUP = new WindowId('POPUP'); + static readonly POPUP = new WindowId('POPUP') @EnumValue - static readonly LOGIN = new WindowId('LOGIN'); + static readonly LOGIN = new WindowId('LOGIN') @EnumValue - static readonly DEVELOPER = new WindowId('DEVELOPER'); + static readonly DEVELOPER = new WindowId('DEVELOPER') - constructor( - public label: string, - ) { - super(); + constructor(public label: string) { + super() } - - getEventChannelName( - event: WindowEvents, - ): string { - return `${this}_${event}`; - } - } // function parseWindowEventChannelName( @@ -35,6 +25,4 @@ class WindowId extends EnumFactory() { // // channelName.sub // } -export { - WindowId, -}; +export { WindowId } diff --git a/src/electron-main/ipc/ipc-main-home.ts b/src/electron-main/ipc/ipc-main-home.ts new file mode 100644 index 0000000..d51cbe9 --- /dev/null +++ b/src/electron-main/ipc/ipc-main-home.ts @@ -0,0 +1,22 @@ +// search channel +import { ipcMain } from 'electron-better-ipc' +import { homeWindowManager } from '../window/home-window' +import { SearchChannel } from '../../electron-shared/ipc/ipc-channel-search' + +ipcMain.answerRenderer(SearchChannel.TOGGLE_PIN_SEARCH_WINDOW, () => { + const window = homeWindowManager.getOrCreate() + const top = window.isAlwaysOnTop() + const target = !top + window.setAlwaysOnTop(target) + return target +}) + +ipcMain.answerRenderer(SearchChannel.SEARCH, async (data: any) => { + const window = homeWindowManager.getOrCreate() + window.show() + if (data.text) { + await ipcMain.callRenderer(window, SearchChannel.SEARCH, { + text: data.text, + }) + } +}) diff --git a/src/electron-main/ipc/ipc-main-login.ts b/src/electron-main/ipc/ipc-main-login.ts deleted file mode 100644 index 239c7c8..0000000 --- a/src/electron-main/ipc/ipc-main-login.ts +++ /dev/null @@ -1,10 +0,0 @@ -// login channel -import { ipcMain } from 'electron-better-ipc'; -import { getOrCreateLoginWindow } from '../window/login-window'; -import { LoginChannel } from '../../electron-shared/ipc/ipc-channel-login'; - -ipcMain.answerRenderer(LoginChannel.SHOW_LOGIN_WINDOW, () => { - getOrCreateLoginWindow(); - return true; -}); - diff --git a/src/electron-main/ipc/ipc-main-search.ts b/src/electron-main/ipc/ipc-main-search.ts deleted file mode 100644 index 5557318..0000000 --- a/src/electron-main/ipc/ipc-main-search.ts +++ /dev/null @@ -1,42 +0,0 @@ -// search channel -import { ipcMain } from 'electron-better-ipc'; -import { - getOrCreateSearchWindow, - hideSearchWindow, - showSearchWindow, - toggleSearchWindow, topSearchWindow -} from '../window/search-window'; -import { SearchChannel } from '../../electron-shared/ipc/ipc-channel-search'; - -ipcMain.answerRenderer(SearchChannel.TOGGLE_PIN_SEARCH_WINDOW, () => { - const window = getOrCreateSearchWindow(); - const top = window.isAlwaysOnTop(); - const target = !top; - window.setAlwaysOnTop(target); - return target; -}); - -ipcMain.answerRenderer(SearchChannel.TOGGLE_SEARCH_WINDOW, async () => { - return toggleSearchWindow(); -}); - -ipcMain.answerRenderer(SearchChannel.SHOW_SEARCH_WINDOW, async () => { - return showSearchWindow(); -}); - -ipcMain.answerRenderer(SearchChannel.HIDE_SEARCH_WINDOW, async () => { - return hideSearchWindow(); -}); - -ipcMain.answerRenderer(SearchChannel.TOP_SEARCH_WINDOW, async () => { - return topSearchWindow(); -}); - -ipcMain.answerRenderer(SearchChannel.SEARCH, async (data: any) => { - const window = getOrCreateSearchWindow(); - await showSearchWindow(); - if (data.text) { - await ipcMain.callRenderer(window, SearchChannel.SEARCH, { text: data.text }); - } -}); - diff --git a/src/electron-main/ipc/ipc-main-setting.ts b/src/electron-main/ipc/ipc-main-setting.ts index d59fb88..be0e2ff 100644 --- a/src/electron-main/ipc/ipc-main-setting.ts +++ b/src/electron-main/ipc/ipc-main-setting.ts @@ -1,19 +1,20 @@ // setting channel -import { ipcMain } from 'electron-better-ipc'; -import { getOrCreateSettingWindow } from '../window/setting-window'; -import { UserProfile } from '../../electron-shared/user-profile/user-profile'; -import logger from '../../electron-shared/logger'; -import { handleSettingChange } from '../setting'; -import { SettingChannel } from '../../electron-shared/ipc/ipc-channel-setting'; +import { ipcMain } from 'electron-better-ipc' +import { settingWindowManager } from '../window/setting-window' +import { UserProfile } from '../../electron-shared/user-profile/user-profile' +import logger from '../../electron-shared/logger' +import { handleSettingChange } from '../setting' +import { SettingChannel } from '../../electron-shared/ipc/ipc-channel-setting' ipcMain.answerRenderer(SettingChannel.OPEN_SETTING_WINDOW, () => { - getOrCreateSettingWindow(); - return true; -}); - -ipcMain.answerRenderer(SettingChannel.SETTING_CHANGE, async data => { - const { newValue, oldValue } = data as { newValue: UserProfile, oldValue: UserProfile }; - logger.log('setting change', newValue); - return handleSettingChange(newValue, oldValue); -}); + settingWindowManager.getOrCreate() +}) +ipcMain.answerRenderer(SettingChannel.SETTING_CHANGE, async (data) => { + const { newValue, oldValue } = data as { + newValue: UserProfile + oldValue: UserProfile + } + logger.log('setting change', newValue) + return handleSettingChange(newValue, oldValue) +}) diff --git a/src/electron-main/ipc/ipc-main.ts b/src/electron-main/ipc/ipc-main.ts index 5da7347..0dfa05e 100644 --- a/src/electron-main/ipc/ipc-main.ts +++ b/src/electron-main/ipc/ipc-main.ts @@ -1,7 +1,6 @@ import './ipc-main-ecdict'; -import './ipc-main-login'; import './ipc-main-popup'; -import './ipc-main-search'; +import './ipc-main-home'; import './ipc-main-setting'; import './ipc-main-local-db'; import { ipcMain } from 'electron-better-ipc'; diff --git a/src/electron-main/main.ts b/src/electron-main/main.ts index b9d6585..90c3aab 100644 --- a/src/electron-main/main.ts +++ b/src/electron-main/main.ts @@ -1,23 +1,23 @@ // Modules to control application life and create native browser window -import './ipc/ipc-main'; -import { app, globalShortcut, Menu } from 'electron'; -import logger from '../electron-shared/logger'; -import { getOrCreateSearchWindow, showSearchWindow } from './window/search-window'; -import globalState from './global-state'; -import contextMenu from 'electron-context-menu'; -import { initSetting } from './setting'; -import { getOrCreateAppMenu } from './menu'; -import { initialAutoUpdater } from './auto-update'; +import './ipc/ipc-main' +import { app, globalShortcut, Menu } from 'electron' +import logger from '../electron-shared/logger' +import { homeWindowManager } from './window/home-window' +import globalState from './global-state' +import contextMenu from 'electron-context-menu' +import { initSetting } from './setting' +import { getOrCreateAppMenu } from './menu' +import { initialAutoUpdater } from './auto-update' import * as remoteMain from '@electron/remote/main' -import { Env } from '../electron-shared/env'; +import { Env } from '../electron-shared/env' // see https://github.com/electron/remote -remoteMain.initialize(); +remoteMain.initialize() // verify env -logger.debug('REACT_APP_ENV_LOAD_FLAG', Env.REACT_APP_ENV_LOAD_FLAG); -if(!Env.REACT_APP_ENV_LOAD_FLAG) { - throw new Error('failed to load env'); +logger.debug('REACT_APP_ENV_LOAD_FLAG', Env.REACT_APP_ENV_LOAD_FLAG) +if (!Env.REACT_APP_ENV_LOAD_FLAG) { + throw new Error('failed to load env') } contextMenu({ @@ -25,31 +25,29 @@ contextMenu({ labels: { inspect: 'inspect', }, -}); +}) // Make this app a single instance app. if (!app.requestSingleInstanceLock()) { - app.quit(); + app.quit() } app.on('second-instance', () => { - logger.log('second-instance'); - showSearchWindow(); -}); + logger.log('second-instance') + homeWindowManager.show() +}) // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', () => { + homeWindowManager.getOrCreate() - getOrCreateSearchWindow(); + initSetting() - initSetting(); + Menu.setApplicationMenu(getOrCreateAppMenu()) - Menu.setApplicationMenu(getOrCreateAppMenu()); - - initialAutoUpdater(); - -}); + initialAutoUpdater() +}) // Quit when all windows are closed. app.on('window-all-closed', () => { @@ -60,20 +58,22 @@ app.on('window-all-closed', () => { // } // we do not quit the app here - logger.log('window-all-closed'); -}); + logger.log('window-all-closed') +}) // On OS X it"s common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. -app.on('activate', getOrCreateSearchWindow); +app.on('activate', () => { + homeWindowManager.getOrCreate() +}) app.on('will-quit', () => { - globalShortcut.unregisterAll(); -}); + globalShortcut.unregisterAll() +}) app.on('before-quit', () => { - globalState.isQuiting = true; -}); + globalState.isQuiting = true +}) // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. @@ -82,5 +82,4 @@ app.on('before-quit', () => { // => [.DisplayCompositor]GL ERROR :GL_INVALID_OPERATION : glBufferData: <- error from previous GL command // https://github.com/electron/electron/issues/7834 // https://github.com/electron/electron/issues/12820 -app.disableHardwareAcceleration(); - +app.disableHardwareAcceleration() diff --git a/src/electron-main/menu.ts b/src/electron-main/menu.ts index aa1ceb3..253020f 100644 --- a/src/electron-main/menu.ts +++ b/src/electron-main/menu.ts @@ -11,12 +11,11 @@ import { APP_CONSTANTS } from '../common/app-constants'; import gitInfo from './utils/git-info'; import packageJson from '../../package.json'; import openAboutWindow, { PackageJson } from 'about-window'; -import { getOrCreateSearchWindow } from './window/search-window'; -import { getOrCreateSettingWindow } from './window/setting-window'; -import { getOrCreateLoginWindow } from './window/login-window'; -import { getOrCreateDeveloperWindow } from './window/debug-window'; +import { debugWindowManager } from './window/debug-window'; import { Runtime } from '../electron-shared/runtime'; import logger from '../electron-shared/logger'; +import { homeWindowManager } from './window/home-window'; +import { settingWindowManager } from './window/setting-window'; export { getOrCreateAppMenu, @@ -35,7 +34,7 @@ function createMenu(): Menu { submenu: [ { label: 'Developer Utilities', - click: () => getOrCreateDeveloperWindow(), + click: () => debugWindowManager.getOrCreate(), }, ] }; @@ -68,7 +67,7 @@ function createMenu(): Menu { about_page_dir: getPublicPath('about'), use_version_info, win_options: { - parent: getOrCreateSearchWindow(), + parent: homeWindowManager.getOrCreate(), modal: !Runtime.isMac, maximizable: false, minimizable: false, @@ -91,7 +90,7 @@ function createMenu(): Menu { // }, { label: 'Setting', - click: () => getOrCreateSettingWindow(), + click: () => settingWindowManager.getOrCreate(), }, { role: 'quit', diff --git a/src/electron-main/setting.ts b/src/electron-main/setting.ts index bbfd877..4d036f4 100644 --- a/src/electron-main/setting.ts +++ b/src/electron-main/setting.ts @@ -1,5 +1,4 @@ import { ipcMain } from 'electron-better-ipc'; -import { getOrCreateSearchWindow, toggleSearchWindow } from './window/search-window'; import { globalShortcut } from 'electron'; import { UserProfile } from '../electron-shared/user-profile/user-profile'; import logger from '../electron-shared/logger'; @@ -8,7 +7,8 @@ import { LocalDB } from './local-db/local-db'; import { ElectronStoreUserProfileService } from '../electron-shared/user-profile/electron-store-user-profile-service'; import { SettingChannel } from '../electron-shared/ipc/ipc-channel-setting'; import { GlobalShotCutChannel } from '../electron-shared/ipc/ipc-channel-global-shot-cut'; -import { getOrCreateSettingWindow } from './window/setting-window'; +import { homeWindowManager } from './window/home-window'; +import { settingWindowManager } from './window/setting-window'; export function initSetting() { ElectronStoreUserProfileService.init(); @@ -47,7 +47,7 @@ export async function handleSettingChange( ElectronStoreUserProfileService.instance().setProfile(newValue); // sync to electron store const res = await ipcMain.callRenderer( - getOrCreateSearchWindow(), + homeWindowManager.getOrCreate(), SettingChannel.SETTING_CHANGE, { newValue, oldValue, @@ -55,7 +55,7 @@ export async function handleSettingChange( // notify setting window as well await ipcMain.callRenderer( - getOrCreateSettingWindow(), + settingWindowManager.getOrCreate(), SettingChannel.SETTING_CHANGE, { newValue, oldValue, @@ -78,7 +78,7 @@ function handleAppHotKeyChange(newValue: string | null, oldValue: string | null // then let the renderer process send back to main process // because we need the information about isSettingWindowOpen... // it's over complicated... - return ipcMain.callRenderer(getOrCreateSearchWindow(), GlobalShotCutChannel.APP_HOT_KEY_PRESSED); + return ipcMain.callRenderer(homeWindowManager.getOrCreate(), GlobalShotCutChannel.APP_HOT_KEY_PRESSED); }); if (success) { logger.log(`${newValue} register success`); diff --git a/src/electron-main/tray/tray.ts b/src/electron-main/tray/tray.ts index 0e0d818..a5defbb 100644 --- a/src/electron-main/tray/tray.ts +++ b/src/electron-main/tray/tray.ts @@ -1,13 +1,11 @@ -import { app, Menu, Tray } from 'electron'; -import logger from '../../electron-shared/logger'; -import { showSearchWindow } from '../window/search-window'; -import { getIconPath } from '../../electron-shared/path-util'; -import { mainContainer } from '../../common/container/main-container'; -import { APP_CONSTANTS } from '../../common/app-constants'; +import { app, Menu, Tray } from 'electron' +import logger from '../../electron-shared/logger' +import { homeWindowManager } from '../window/home-window' +import { getIconPath } from '../../electron-shared/path-util' +import { mainContainer } from '../../common/container/main-container' +import { APP_CONSTANTS } from '../../common/app-constants' -export { - getOrCreateTray, -}; +export { getOrCreateTray } const TrayToken = Symbol.for('tray'); mainContainer.bind(TrayToken) @@ -41,7 +39,7 @@ function createMenu() { label: 'Show', click: () => { logger.log('show'); - showSearchWindow(); + homeWindowManager.show() }, }, { diff --git a/src/electron-main/utils/window-util.ts b/src/electron-main/utils/window-util.ts deleted file mode 100644 index 0127026..0000000 --- a/src/electron-main/utils/window-util.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { BrowserWindow } from 'electron'; -import { ipcMain } from 'electron-better-ipc'; -import { WindowId } from '../../common/window-id'; -import { WindowEvents } from '../../common/window-events'; - -function notifyRendererWindowEvents( - windowId: WindowId, - eventWindow: BrowserWindow, - notifyWindow: BrowserWindow = eventWindow, -) { - WindowEvents.values().forEach(event => { - const channelName = windowId.getEventChannelName(event); - eventWindow.on(event as any, e => { - ipcMain.callRenderer(notifyWindow, channelName); - }); - }); -} - -export { - notifyRendererWindowEvents, -}; - diff --git a/src/electron-main/window/abstract-window-manager.ts b/src/electron-main/window/abstract-window-manager.ts new file mode 100644 index 0000000..aa49f7b --- /dev/null +++ b/src/electron-main/window/abstract-window-manager.ts @@ -0,0 +1,136 @@ +import { BrowserWindow } from 'electron' +import { windowContainer } from './windows' +import { WindowId } from '../../common/window-id' +import logger from '../../electron-shared/logger' +import { WindowCommand } from '../../common/window-command' +import { ipcMain } from 'electron-better-ipc' +import { WindowEvent } from '../../common/window-event' + +interface WindowManager { + show() + + hide() + + toggle() + + close() + + top() +} + +export abstract class AbstractWindowManager implements WindowManager { + protected _window: BrowserWindow | null = null + + protected abstract customizedCreate(): BrowserWindow + + abstract id: WindowId + + private create() { + const window = this.customizedCreate() + this._window = window + windowContainer.add(this.id, window) + this.notifyRendererWindowEvents() + this.listenWindowCommands() + // Emitted when the window is closed. + this._window!!.on('closed', () => { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + this.onClosed() + }) + return window + } + + notifyRendererWindowEvents() { + WindowEvent.values().forEach((event) => { + const channelName = event.getIpcChannelName(this.id) + this._window!!.on(event as any, (e) => { + // by default, we only notify this window. + // consider broadcast the event instead? + ipcMain.callRenderer(this._window!!, channelName) + }) + }) + } + + listenWindowCommands() { + WindowCommand.values().forEach((command) => { + const channelName = command.getIpcChannelName(this.id) + ipcMain.answerRenderer(channelName, async () => { + logger.debug('answerRenderer: ' + channelName) + return this.handleCommand(command) + }) + }) + } + + onClosed() { + this.logger.log('closed') + this.destroy() + } + + getOrCreate() { + if (this._window) return this._window + + this._window = this.create() + windowContainer.add(this.id, this._window) + return this._window + } + + destroy() { + this._window = null + windowContainer.remove(this.id) + this.logger.log('destroy') + } + + show() { + this.getOrCreate().show() + this.logger.log('show') + + // if (window) { + // if (window.isMinimized()) window.restore(); + // window.show(); + // } else { + // logger.error('somehow window doesn\'t exist'); + // } + } + + hide() { + this.getOrCreate().hide() + this.logger.log('hide') + } + + top() { + this.getOrCreate().moveTop() + this.logger.log('top') + } + + toggle() { + this.logger.log('toggle', new Date()) + const window = this.getOrCreate() + if (window.isMinimized() || !window.isVisible()) { + this.show() + return true + } else { + this.hide() + return false + } + } + + close() { + if (!this._window) return + this._window.close() + this.logger.log('close') + } + + handleCommand(command: WindowCommand) { + const handler = this[command.name] as () => {} + if (!handler) { + this.logger.error(`No available handler for '${command}' command`) + return + } + handler.call(this) + } + + get logger() { + return logger.getLogger(this.id.name) + } +} diff --git a/src/electron-main/window/debug-window.ts b/src/electron-main/window/debug-window.ts index abaed83..d4471b1 100644 --- a/src/electron-main/window/debug-window.ts +++ b/src/electron-main/window/debug-window.ts @@ -1,38 +1,28 @@ -import { BrowserWindow } from 'electron'; -import { getOrCreateSearchWindow } from './search-window'; -import logger from '../../electron-shared/logger'; -import { getWindowHashUrl } from '../../electron-shared/path-util'; -import { windowContainer } from './windows'; -import { WindowId } from '../../common/window-id'; -import * as remoteMain from '@electron/remote/main'; -import { Runtime } from '../../electron-shared/runtime'; -import path from 'path'; +import { BrowserWindow } from 'electron' +import { homeWindowManager } from './home-window' +import logger from '../../electron-shared/logger' +import { getWindowHashUrl } from '../../electron-shared/path-util' +import { WindowId } from '../../common/window-id' +import * as remoteMain from '@electron/remote/main' +import { Runtime } from '../../electron-shared/runtime' +import path from 'path' +import { AbstractWindowManager } from './abstract-window-manager' -export { - getOrCreateDeveloperWindow, -}; +class DebugWindowManager extends AbstractWindowManager { + id: WindowId = WindowId.DEVELOPER -function getOrCreateDeveloperWindow() { - return windowContainer.find(WindowId.DEVELOPER) - ?? windowContainer.add(WindowId.DEVELOPER, createWindow()); + protected customizedCreate(): BrowserWindow { + return createWindow() + } } -function close() { - const window = windowContainer.find(WindowId.DEVELOPER); - if (window == null) return; - window.close(); -} - -function destroy() { - logger.log('destroy developer window'); - windowContainer.remove(WindowId.DEVELOPER); -} +export const debugWindowManager = new DebugWindowManager() function createWindow() { - logger.log('create developer window'); + logger.log('create developer window') // create a modal window // https://electronjs.org/docs/api/browser-window#modal-windows - const parent = getOrCreateSearchWindow(); + const parent = homeWindowManager.getOrCreate() const window = new BrowserWindow({ width: Runtime.isDev ? 1200 : 1200, height: 600, @@ -58,17 +48,17 @@ function createWindow() { // to disable the cors policy, so that we can fetch resources from different origin webSecurity: false, }, - }); - remoteMain.enable(window.webContents); - window.setMenuBarVisibility(false); + }) + remoteMain.enable(window.webContents) + window.setMenuBarVisibility(false) // Load a remote URL // https://stackoverflow.com/a/47926513 - window.loadURL(getWindowHashUrl('developer')); + window.loadURL(getWindowHashUrl('developer')) window.once('ready-to-show', () => { - window.show(); - }); + window.show() + }) // // currently in mac, modal window doesn't have close button // // hence we listen blur event, and close window // // https://github.com/electron/electron/issues/30232 @@ -77,12 +67,8 @@ function createWindow() { // close(); // }); // } - window.on('closed', async () => { - destroy(); - logger.log(`${WindowId.DEVELOPER} closed`); - }); - window.webContents.openDevTools(); + window.webContents.openDevTools() - return window; + return window } diff --git a/src/electron-main/window/search-window.ts b/src/electron-main/window/home-window.ts similarity index 51% rename from src/electron-main/window/search-window.ts rename to src/electron-main/window/home-window.ts index 1daa022..5bf49fb 100644 --- a/src/electron-main/window/search-window.ts +++ b/src/electron-main/window/home-window.ts @@ -1,74 +1,31 @@ -import { app, BrowserWindow, session } from 'electron'; -import logger from '../../electron-shared/logger'; -import { getOrCreateTray } from '../tray/tray'; -import * as path from 'path'; -import globalState from '../global-state'; -import * as os from 'os'; -import * as fs from 'fs'; -import { getIconPath, getWindowHashUrl } from '../../electron-shared/path-util'; -import { ipcMain } from 'electron-better-ipc'; -import { windowContainer } from './windows'; -import { WindowId } from '../../common/window-id'; -import * as remoteMain from '@electron/remote/main'; -import { Runtime } from '../../electron-shared/runtime'; -import { notifyRendererWindowEvents } from '../utils/window-util'; -import { AppChannel } from '../../electron-shared/ipc/ipc-channel-app'; - -export { - getOrCreateSearchWindow, - toggleSearchWindow, - showSearchWindow, - hideSearchWindow, - topSearchWindow, -}; - -function getOrCreateSearchWindow(): BrowserWindow { - return windowContainer.find(WindowId.SEARCH) - ?? windowContainer.add(WindowId.SEARCH, createWindow()); -} - -function destroy() { - windowContainer.remove(WindowId.SEARCH); -} - -function showSearchWindow() { - logger.log('show search window'); - const window = getOrCreateSearchWindow(); - window.show(); - // if (window) { - // if (window.isMinimized()) window.restore(); - // window.show(); - // } else { - // logger.error('somehow window doesn\'t exist'); - // } -} - -function hideSearchWindow() { - logger.log('hide search window'); - const window = getOrCreateSearchWindow(); - window.hide(); -} - -function topSearchWindow() { - const window = getOrCreateSearchWindow(); - window.moveTop(); -} - -function toggleSearchWindow() { - logger.log('toggleSearchWindow', new Date()); - const searchWindow = getOrCreateSearchWindow(); - if (searchWindow.isMinimized() || !searchWindow.isVisible()) { - showSearchWindow(); - return true; - } else { - hideSearchWindow(); - return false; +import { app, BrowserWindow, session } from 'electron' +import logger from '../../electron-shared/logger' +import { getOrCreateTray } from '../tray/tray' +import * as path from 'path' +import globalState from '../global-state' +import * as os from 'os' +import * as fs from 'fs' +import { getIconPath, getWindowHashUrl } from '../../electron-shared/path-util' +import { ipcMain } from 'electron-better-ipc' +import { WindowId } from '../../common/window-id' +import * as remoteMain from '@electron/remote/main' +import { Runtime } from '../../electron-shared/runtime' +import { AppChannel } from '../../electron-shared/ipc/ipc-channel-app' +import { AbstractWindowManager } from './abstract-window-manager' + +class HomeWindowManager extends AbstractWindowManager { + id: WindowId = WindowId.HOME + + customizedCreate(): BrowserWindow { + return createWindow() } } +export const homeWindowManager = new HomeWindowManager() + function createWindow() { const window = new BrowserWindow({ - width: Runtime.isDev ? 1600:1200, + width: Runtime.isDev ? 1600 : 1200, height: 900, icon: getIconPath('icon.png'), webPreferences: { @@ -90,8 +47,8 @@ function createWindow() { // https://electronjs.org/docs/api/browser-window minimizable: false, maximizable: false, - }); - remoteMain.enable(window.webContents); + }) + remoteMain.enable(window.webContents) // remove menu bar // https://stackoverflow.com/questions/39091964/remove-menubar-from-electron-app @@ -101,52 +58,44 @@ function createWindow() { // Menu.setApplicationMenu(null); // window.setAutoHideMenuBar(true); - window.loadURL(getWindowHashUrl('main/search')); + window.loadURL(getWindowHashUrl('main/search')) window.once('ready-to-show', () => { if (!process.argv.includes('--background')) { - window.show(); + window.show() } - }); + }) // let allowAppQuit = false; // todo: vite - let allowAppQuit = true; + let allowAppQuit = true - window.on('close', async e => { + window.on('close', async (e) => { // close to tray // https://stackoverflow.com/questions/37828758/electron-js-how-to-minimize-close-window-to-system-tray-and-restore-window-back if (!globalState.isQuiting) { - e.preventDefault(); - window.hide(); + e.preventDefault() + window.hide() } else { if (!allowAppQuit) { - e.preventDefault(); - logger.log('app is quiting'); - allowAppQuit = await ipcMain.callRenderer(window, AppChannel.APP_QUITING) as boolean; + e.preventDefault() + logger.log('app is quiting') + allowAppQuit = (await ipcMain.callRenderer( + window, + AppChannel.APP_QUITING, + )) as boolean if (allowAppQuit) { - logger.log('app is ready to quit'); - app.quit(); + logger.log('app is ready to quit') + app.quit() } } } - }); - - // Emitted when the window is closed. - window.on('closed', () => { - // Dereference the window object, usually you would store windows - // in an array if your app supports multi windows, this is the time - // when you should delete the corresponding element. - destroy(); - logger.log('search window closed'); - }); - - notifyRendererWindowEvents(WindowId.SEARCH, window); + }) window.webContents.on('did-finish-load', () => { - getOrCreateTray(); - logger.log('did-finish-load'); - }); + getOrCreateTray() + logger.log('did-finish-load') + }) // stop link from opening new window // https://stackoverflow.com/questions/46462248/electron-link-opens-in-new-window // window.webContents.on('new-window', (event) => { @@ -154,22 +103,23 @@ function createWindow() { // }); // https://www.electronjs.org/docs/latest/api/web-contents#contentssetwindowopenhandlerhandler window.webContents.setWindowOpenHandler(() => { - return { action: 'deny' }; - }); + return { action: 'deny' } + }) if (Runtime.isDev) { // Open the DevTools. - window.webContents.openDevTools(); + window.webContents.openDevTools() // add devtools - let extensionRelativeFolder: string; + let extensionRelativeFolder: string if (Runtime.isMac) { - extensionRelativeFolder = 'Library/Application Support/Google/Chrome/Default/Extensions'; + extensionRelativeFolder = + 'Library/Application Support/Google/Chrome/Default/Extensions' } else { - extensionRelativeFolder = '.config/google-chrome/Default/Extensions'; + extensionRelativeFolder = '.config/google-chrome/Default/Extensions' } - const extensionDir = path.join(os.homedir(), extensionRelativeFolder); + const extensionDir = path.join(os.homedir(), extensionRelativeFolder) // const reactDir = path.join(extensionDir, 'fmkadmapgofadopljbjfkapdkoienihi'); - const reduxDir = path.join(extensionDir, 'lmhkpmbekcpmknklioeibfkpmmfibljd'); + const reduxDir = path.join(extensionDir, 'lmhkpmbekcpmknklioeibfkpmmfibljd') // somehow react devtools doesn't work // Message: Uncaught TypeError: Cannot read properties of undefined (reading 'ExecutionWorld') // https://github.com/electron/electron/issues/36545 @@ -182,14 +132,14 @@ function createWindow() { // } // } if (fs.existsSync(reduxDir)) { - const redux = fs.readdirSync(reduxDir); + const redux = fs.readdirSync(reduxDir) if (redux.length) { - const reduxExt = path.join(reduxDir, redux[redux.length - 1]); - console.log(`load redux dev tools from: ${reduxExt}`); - session.defaultSession.loadExtension(reduxExt); + const reduxExt = path.join(reduxDir, redux[redux.length - 1]) + console.log(`load redux dev tools from: ${reduxExt}`) + session.defaultSession.loadExtension(reduxExt) } } } - return window; + return window } diff --git a/src/electron-main/window/login-window.ts b/src/electron-main/window/login-window.ts index 0a8ccdc..e5dd67c 100644 --- a/src/electron-main/window/login-window.ts +++ b/src/electron-main/window/login-window.ts @@ -1,42 +1,36 @@ -import { BrowserWindow } from 'electron'; -import { getOrCreateSearchWindow } from './search-window'; -import logger from '../../electron-shared/logger'; -import { ipcMain } from 'electron-better-ipc'; -import { getWindowHashUrl } from '../../electron-shared/path-util'; -import { windowContainer } from './windows'; -import { WindowId } from '../../common/window-id'; -import { extractCode, getLoginOption, getLoginType, githubOption } from '../../common/social-login'; -import * as remoteMain from '@electron/remote/main'; -import { Runtime } from '../../electron-shared/runtime'; -import { notifyRendererWindowEvents } from '../utils/window-util'; -import { LoginChannel } from '../../electron-shared/ipc/ipc-channel-login'; -import path from 'path'; - -export { - getOrCreateLoginWindow, -}; - -function getOrCreateLoginWindow() { - return windowContainer.find(WindowId.LOGIN) - ?? windowContainer.add(WindowId.LOGIN, createWindow()); -} - -function close() { - const window = windowContainer.find(WindowId.LOGIN); - if (window == null) return; - window.close(); +import { BrowserWindow } from 'electron' +import { homeWindowManager } from './home-window' +import logger from '../../electron-shared/logger' +import { ipcMain } from 'electron-better-ipc' +import { getWindowHashUrl } from '../../electron-shared/path-util' +import { WindowId } from '../../common/window-id' +import { + extractCode, + getLoginOption, + getLoginType, + githubOption, +} from '../../common/social-login' +import * as remoteMain from '@electron/remote/main' +import { Runtime } from '../../electron-shared/runtime' +import { LoginChannel } from '../../electron-shared/ipc/ipc-channel-login' +import path from 'path' +import { AbstractWindowManager } from './abstract-window-manager' + +class LoginWindowManager extends AbstractWindowManager { + id: WindowId = WindowId.LOGIN + + protected customizedCreate(): BrowserWindow { + return createWindow() + } } -function destroy() { - logger.log('destroy login window'); - windowContainer.remove(WindowId.LOGIN); -} +export const loginWindowManager = new LoginWindowManager() function createWindow() { - logger.log('create login window'); + logger.log('create login window') // create a modal window // https://electronjs.org/docs/api/browser-window#modal-windows - const parent = getOrCreateSearchWindow(); + const parent = homeWindowManager.getOrCreate() const window = new BrowserWindow({ width: Runtime.isDev ? 400 : 400, height: 200, @@ -62,14 +56,14 @@ function createWindow() { // to disable the cors policy, so that we can fetch resources from different origin webSecurity: false, }, - }); - remoteMain.enable(window.webContents); - window.setMenuBarVisibility(false); + }) + remoteMain.enable(window.webContents) + window.setMenuBarVisibility(false) // Load a remote URL // https://stackoverflow.com/a/47926513 const loginWindowUrl = getWindowHashUrl('login') - window.loadURL(loginWindowUrl); + window.loadURL(loginWindowUrl) if (!(githubOption.params.redirect_uri && githubOption.params.client_id)) { throw new Error('"client_id" and "redirect_uri" are required') } @@ -78,100 +72,98 @@ function createWindow() { // see https://auth0.com/blog/securing-electron-applications-with-openid-connect-and-oauth-2/ const filter = { urls: [`${githubOption.params.redirect_uri}*`], - }; - logger.debug('[onBeforeRequest] register filter:', filter); - window.webContents.session.webRequest.onBeforeRequest(filter, async ({ url }) => { - logger.debug('[onBeforeRequest] url:', url); - const code = extractCode(url); - logger.debug('[onBeforeRequest] extract code:', code); - if (code) { - // Close the browser if code found or error - window.close(); - - const loginType = getLoginType(url); - logger.debug('[will-redirect] loginType:', loginType); - if (!loginType) return; - const loginOption = getLoginOption(loginType); - // logger.debug('[will-redirect] loginOption:', loginOption); - - ipcMain.callRenderer(parent, LoginChannel.LOGIN_CODE_RECEIVED, { - code, - loginType, - loginOption, - }); - } - }); + } + logger.debug('[onBeforeRequest] register filter:', filter) + window.webContents.session.webRequest.onBeforeRequest( + filter, + async ({ url }) => { + logger.debug('[onBeforeRequest] url:', url) + const code = extractCode(url) + logger.debug('[onBeforeRequest] extract code:', code) + if (code) { + // Close the browser if code found or error + window.close() + + const loginType = getLoginType(url) + logger.debug('[will-redirect] loginType:', loginType) + if (!loginType) return + const loginOption = getLoginOption(loginType) + // logger.debug('[will-redirect] loginOption:', loginOption); + + ipcMain.callRenderer(parent, LoginChannel.LOGIN_CODE_RECEIVED, { + code, + loginType, + loginOption, + }) + } + }, + ) // this only works when we have corresponding cookie // we keep this block cuz it produces better user experience(it won't create then close a blank window) // we handle success login event here window.webContents.on('will-redirect', (event, url, ...args) => { - const oldUrl = window.webContents.getURL(); + const oldUrl = window.webContents.getURL() - logger.debug('[will-redirect] old url:', oldUrl); - logger.debug('[will-redirect] new url:', url); + logger.debug('[will-redirect] old url:', oldUrl) + logger.debug('[will-redirect] new url:', url) - const code = extractCode(url); - logger.debug('[will-redirect] extract code:', code); + const code = extractCode(url) + logger.debug('[will-redirect] extract code:', code) if (code) { // Close the browser if code found or error - event.preventDefault(); // prevent redirect - window.close(); + event.preventDefault() // prevent redirect + window.close() - const loginType = getLoginType(url); - logger.debug('[will-redirect] loginType:', loginType); - if (!loginType) return; - const loginOption = getLoginOption(loginType); + const loginType = getLoginType(url) + logger.debug('[will-redirect] loginType:', loginType) + if (!loginType) return + const loginOption = getLoginOption(loginType) // logger.debug('[will-redirect] loginOption:', loginOption); ipcMain.callRenderer(parent, LoginChannel.LOGIN_CODE_RECEIVED, { code, loginType, loginOption, - }); + }) } - }); + }) // we handle redirect to oauth2 vendor authorization page event here window.webContents.on('did-redirect-navigation', (event, url) => { - logger.debug('[did-redirect-navigation]', url); - const loginType = getLoginType(url); - logger.debug('[did-redirect-navigation] login type:', loginType); - if (!loginType) return; - const loginOption = getLoginOption(loginType); + logger.debug('[did-redirect-navigation]', url) + const loginType = getLoginType(url) + logger.debug('[did-redirect-navigation] login type:', loginType) + if (!loginType) return + const loginOption = getLoginOption(loginType) - const windowSize = loginOption.windowSize; + const windowSize = loginOption.windowSize if (windowSize) { - window.setContentSize(windowSize.width, windowSize.height); + window.setContentSize(windowSize.width, windowSize.height) } - }); + }) window.once('ready-to-show', () => { - window.show(); - }); - window.on('show', e => { + window.show() + }) + window.on('show', (e) => { // to mimic the 1st time login behavior // session.defaultSession.clearStorageData({ // storages: ['cookies'], // }); - }); + }) // currently in mac, modal window doesn't have close button // hence we listen blur event, and close window // https://github.com/electron/electron/issues/30232 if (Runtime.isMac) { window.on('blur', async () => { - close(); - }); + close() + }) } - window.on('closed', async () => { - destroy(); - }); - // note that we notify parent window here! - notifyRendererWindowEvents(WindowId.LOGIN, window, parent) // if (Runtime.isDev) { // window.webContents.openDevTools(); // } - return window; + return window } diff --git a/src/electron-main/window/setting-window.ts b/src/electron-main/window/setting-window.ts index 4781543..42e04ce 100644 --- a/src/electron-main/window/setting-window.ts +++ b/src/electron-main/window/setting-window.ts @@ -1,38 +1,26 @@ -import { BrowserWindow } from 'electron'; -import { getOrCreateSearchWindow } from './search-window'; -import logger from '../../electron-shared/logger'; -import { getWindowHashUrl } from '../../electron-shared/path-util'; -import { windowContainer } from './windows'; -import { WindowId } from '../../common/window-id'; -import * as remoteMain from '@electron/remote/main'; -import { Runtime } from '../../electron-shared/runtime'; -import { notifyRendererWindowEvents } from '../utils/window-util'; -import path from 'path'; +import { BrowserWindow } from 'electron' +import { homeWindowManager } from './home-window' +import { getWindowHashUrl } from '../../electron-shared/path-util' +import { WindowId } from '../../common/window-id' +import * as remoteMain from '@electron/remote/main' +import { Runtime } from '../../electron-shared/runtime' +import path from 'path' +import { AbstractWindowManager } from './abstract-window-manager' -export { - getOrCreateSettingWindow, -}; +class SettingWindowManager extends AbstractWindowManager { + id: WindowId = WindowId.SETTING -function getOrCreateSettingWindow() { - return windowContainer.find(WindowId.SETTING) - ?? windowContainer.add(WindowId.SETTING, createWindow()); + protected customizedCreate(): BrowserWindow { + return createWindow() + } } -function close() { - const window = windowContainer.find(WindowId.SETTING); - if (window == null) return; - window.close(); -} - -function destroy() { - logger.log('destroy setting window'); - windowContainer.remove(WindowId.SETTING); -} +export const settingWindowManager = new SettingWindowManager() function createWindow() { // create a modal window // https://electronjs.org/docs/api/browser-window#modal-windows - const parent = getOrCreateSearchWindow(); + const parent = homeWindowManager.getOrCreate() const window = new BrowserWindow({ width: Runtime.isDev ? 1200 : 400, height: Runtime.isDev ? 600 : 200, @@ -59,17 +47,17 @@ function createWindow() { // to disable the cors policy, so that we can fetch resources from different origin webSecurity: false, }, - }); - remoteMain.enable(window.webContents); - window.setMenuBarVisibility(false); + }) + remoteMain.enable(window.webContents) + window.setMenuBarVisibility(false) // Load a remote URL // https://stackoverflow.com/a/47926513 - window.loadURL(getWindowHashUrl('setting')); + window.loadURL(getWindowHashUrl('setting')) window.once('ready-to-show', () => { - window.show(); - }); + window.show() + }) // // currently in mac, modal window doesn't have close button // // hence we listen blur event, and close window // // https://github.com/electron/electron/issues/30232 @@ -78,16 +66,10 @@ function createWindow() { // close(); // }); // } - window.on('closed', async () => { - destroy(); - }); - - // note that we notify parent window here! - notifyRendererWindowEvents(WindowId.SETTING, window, parent) if (Runtime.isDev) { - window.webContents.openDevTools(); + window.webContents.openDevTools() } - return window; + return window } diff --git a/src/electron-renderer/services/home-ui-service-impl.ts b/src/electron-renderer/services/home-ui-service-impl.ts new file mode 100644 index 0000000..d466c63 --- /dev/null +++ b/src/electron-renderer/services/home-ui-service-impl.ts @@ -0,0 +1,41 @@ +import { injectable } from 'inversify' +import { HomeUiService } from '../../common/services/home-ui-service' +import { ipcRenderer } from 'electron-better-ipc' +import { SearchChannel } from '../../electron-shared/ipc/ipc-channel-search' +import { ipcCallMain } from '../utils/ipc-decorator' +import { WindowId } from '../../common/window-id' +import logger from '../../electron-shared/logger'; + +@injectable() +export class ElectronHomeUiService implements HomeUiService { + @ipcCallMain(WindowId.HOME.name + '/COMMAND') + async toggle() { + logger.error('delegation failed') + } + + @ipcCallMain(WindowId.HOME.name + '/COMMAND') + async show() { + logger.error('delegation failed') + } + + @ipcCallMain(WindowId.HOME.name + '/COMMAND') + async hide() { + logger.error('delegation failed') + } + + @ipcCallMain(WindowId.HOME.name + '/COMMAND') + async top() { + logger.error('delegation failed') + } + + async search(option: { text: string }) { + return ipcRenderer.callMain(SearchChannel.SEARCH, option) + } + + async togglePin(): Promise { + const res = await ipcRenderer.callMain( + SearchChannel.TOGGLE_PIN_SEARCH_WINDOW, + ) + return res as boolean + } +} diff --git a/src/electron-renderer/services/index.ts b/src/electron-renderer/services/index.ts index c5e481f..2f77add 100644 --- a/src/electron-renderer/services/index.ts +++ b/src/electron-renderer/services/index.ts @@ -3,8 +3,8 @@ import { rendererContainer } from '../../common/container/renderer-container'; import { SearchService, CorsSearchServiceToken, EcDictSearchServiceToken } from '../../common/services/search-service'; import { SettingUiService, SettingUiServiceToken } from '../../common/services/setting-ui-service'; import { ElectronSettingUiService } from './setting-ui-service-impl'; -import { SearchUiService, SearchUiServiceToken } from '../../common/services/search-ui-service'; -import { ElectronSearchUiService } from './search-ui-service-impl'; +import { HomeUiService, SearchUiServiceToken } from '../../common/services/home-ui-service'; +import { ElectronHomeUiService } from './home-ui-service-impl'; import { PopupUiService, PopupUiServiceToken } from '../../common/services/popup-ui-service'; import { ElectronPopupUiService } from './popup-ui-service-impl'; import { WindowService, WindowServiceToken } from '../../common/services/window-service'; @@ -43,7 +43,7 @@ function registerAllService() { rendererContainer.bind(CorsSearchServiceToken).to(CorsSearchService); rendererContainer.bind(EcDictSearchServiceToken).to(EcDictSearchService); - rendererContainer.bind(SearchUiServiceToken).to(ElectronSearchUiService); + rendererContainer.bind(SearchUiServiceToken).to(ElectronHomeUiService); rendererContainer.bind(SettingUiServiceToken).to(ElectronSettingUiService); rendererContainer.bind(SettingServiceToken).to(ElectronSettingService); rendererContainer.bind(PopupUiServiceToken).to(ElectronPopupUiService); diff --git a/src/electron-renderer/services/login-ui-service-impl.ts b/src/electron-renderer/services/login-ui-service-impl.ts index 5c5d3e2..2280273 100644 --- a/src/electron-renderer/services/login-ui-service-impl.ts +++ b/src/electron-renderer/services/login-ui-service-impl.ts @@ -1,13 +1,10 @@ -import { LoginUiService } from '../../common/services/login-ui-service'; -import { ipcRenderer } from 'electron-better-ipc'; -import { injectable } from 'inversify'; -import { LoginChannel } from '../../electron-shared/ipc/ipc-channel-login'; - +import { LoginUiService } from '../../common/services/login-ui-service' +import { injectable } from 'inversify' +import { ipcCallMain } from '../utils/ipc-decorator' +import { WindowId } from '../../common/window-id' @injectable() export class ElectronLoginUiService implements LoginUiService { - async open(): Promise { - const res = await ipcRenderer.callMain(LoginChannel.SHOW_LOGIN_WINDOW); - return res as boolean; - } + @ipcCallMain(WindowId.LOGIN.name + '/COMMAND') + async show() {} } diff --git a/src/electron-renderer/services/search-ui-service-impl.ts b/src/electron-renderer/services/search-ui-service-impl.ts deleted file mode 100644 index d0916b4..0000000 --- a/src/electron-renderer/services/search-ui-service-impl.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { injectable } from 'inversify'; -import { SearchUiService } from '../../common/services/search-ui-service'; -import { ipcRenderer } from 'electron-better-ipc'; -import { SearchChannel } from '../../electron-shared/ipc/ipc-channel-search'; - -@injectable() -export class ElectronSearchUiService implements SearchUiService { - async toggleSearchWindow(): Promise { - await ipcRenderer.callMain(SearchChannel.TOGGLE_SEARCH_WINDOW); - } - - async showSearchWindow() { - return ipcRenderer.callMain(SearchChannel.SHOW_SEARCH_WINDOW); - } - - async hideSearchWindow() { - return ipcRenderer.callMain(SearchChannel.HIDE_SEARCH_WINDOW); - } - - async topSearchWindow() { - return ipcRenderer.callMain(SearchChannel.TOP_SEARCH_WINDOW); - } - - async search(option: { text: string }) { - return ipcRenderer.callMain(SearchChannel.SEARCH, option); - } - - async togglePin(): Promise { - const res = await ipcRenderer.callMain(SearchChannel.TOGGLE_PIN_SEARCH_WINDOW); - return res as boolean; - } - -} diff --git a/src/electron-renderer/services/setting-service-impl.ts b/src/electron-renderer/services/setting-service-impl.ts index daf0755..68f9c0f 100644 --- a/src/electron-renderer/services/setting-service-impl.ts +++ b/src/electron-renderer/services/setting-service-impl.ts @@ -48,7 +48,7 @@ export class ElectronSettingService implements SettingService { const res = ElectronStoreUserProfileService .instance() .getProfile(); - if (getCurrentWindowId()===WindowId.SEARCH) { + if (getCurrentWindowId()===WindowId.HOME) { await this.handleSettingChange(res, null); } return res as UserProfile; diff --git a/src/electron-renderer/services/setting-ui-service-impl.ts b/src/electron-renderer/services/setting-ui-service-impl.ts index 7439ad8..11e2c06 100644 --- a/src/electron-renderer/services/setting-ui-service-impl.ts +++ b/src/electron-renderer/services/setting-ui-service-impl.ts @@ -1,13 +1,10 @@ -import { SettingUiService } from '../../common/services/setting-ui-service'; -import { ipcRenderer } from 'electron-better-ipc'; -import { injectable } from 'inversify'; -import { SettingChannel } from '../../electron-shared/ipc/ipc-channel-setting'; - +import { SettingUiService } from '../../common/services/setting-ui-service' +import { injectable } from 'inversify' +import { ipcCallMain } from '../utils/ipc-decorator' +import { WindowId } from '../../common/window-id' @injectable() export class ElectronSettingUiService implements SettingUiService { - async open(): Promise { - const res = await ipcRenderer.callMain(SettingChannel.OPEN_SETTING_WINDOW); - return res as boolean; - } + @ipcCallMain(WindowId.SETTING.name + '/COMMAND') + async show() {} } diff --git a/src/electron-renderer/utils/ipc-decorator.ts b/src/electron-renderer/utils/ipc-decorator.ts index b31339d..a6c97da 100644 --- a/src/electron-renderer/utils/ipc-decorator.ts +++ b/src/electron-renderer/utils/ipc-decorator.ts @@ -77,6 +77,15 @@ export function ipcCallMain( }; } +/** + * e.g. + * CHANNEL_PREFIX/CHANNEL + * + * @param propertyKey only take effect when `channel` is missing + * @param channelPrefix + * @param channel if missing, we try guess it from propertyKey + * @param delimiter + */ function getActualChannel( propertyKey: string | symbol, channelPrefix?: String, diff --git a/src/electron-shared/ipc/ipc-channel-search.ts b/src/electron-shared/ipc/ipc-channel-search.ts index 1aa2de4..9768f4b 100644 --- a/src/electron-shared/ipc/ipc-channel-search.ts +++ b/src/electron-shared/ipc/ipc-channel-search.ts @@ -1,10 +1,6 @@ enum SearchChannel { TOGGLE_PIN_SEARCH_WINDOW = 'TOGGLE_PIN_SEARCH_WINDOW', - TOGGLE_SEARCH_WINDOW = 'TOGGLE_SEARCH_WINDOW', - SHOW_SEARCH_WINDOW = 'SHOW_SEARCH_WINDOW', - HIDE_SEARCH_WINDOW = 'HIDE_SEARCH_WINDOW', - TOP_SEARCH_WINDOW = 'TOP_SEARCH_WINDOW', SEARCH = 'SEARCH', } -export { SearchChannel }; +export { SearchChannel } diff --git a/tsconfig.json b/tsconfig.json index 5b325bc..80677c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "allowSyntheticDefaultImports": true, "strict": true, "noImplicitAny": false, + "noImplicitOverride": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node",