fix: circular dependency

This commit is contained in:
tiddlygit-test 2021-01-19 20:46:40 +08:00
parent 9d32ffd57e
commit 9385b84da8
27 changed files with 495 additions and 320 deletions

View file

@ -42,6 +42,7 @@ module.exports = {
},
},
],
'@typescript-eslint/method-signature-style': 'off',
'@typescript-eslint/member-delimiter-style': [
'warn',
{

15
package-lock.json generated
View file

@ -2436,6 +2436,15 @@
"@types/responselike": "*"
}
},
"@types/circular-dependency-plugin": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@types/circular-dependency-plugin/-/circular-dependency-plugin-5.0.1.tgz",
"integrity": "sha512-FdtMz/DdrqeSTTXwvb7SRUF+Lmh8a8snyGDb7+1+SxxgjHUScyZQDq/1RcL2JCmPAhbNODfqxgRNkB1HWLcZnQ==",
"dev": true,
"requires": {
"@types/webpack": "*"
}
},
"@types/classnames": {
"version": "2.2.11",
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.11.tgz",
@ -5047,6 +5056,12 @@
"safe-buffer": "^5.0.1"
}
},
"circular-dependency-plugin": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz",
"integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==",
"dev": true
},
"class-utils": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",

View file

@ -115,6 +115,7 @@
"@material-ui/lab": "4.0.0-alpha.57",
"@material-ui/pickers": "^4.0.0-alpha.12",
"@types/bluebird": "^3.5.33",
"@types/circular-dependency-plugin": "^5.0.1",
"@types/classnames": "2.2.11",
"@types/copy-webpack-plugin": "^6.4.0",
"@types/csp-html-webpack-plugin": "3.0.0",
@ -142,6 +143,7 @@
"@typescript-eslint/eslint-plugin": "4.11.1",
"@typescript-eslint/parser": "4.11.1",
"ace-builds": "1.4.12",
"circular-dependency-plugin": "^5.2.2",
"classnames": "2.2.6",
"copy-webpack-plugin": "^6.4.1",
"csp-html-webpack-plugin": "5.0.1",

View file

@ -15,45 +15,45 @@ import loadListeners from '@services/listeners';
import MAILTO_URLS from '@services/constants/mailto-urls';
import serviceIdentifier from '@services/serviceIdentifier';
import { Authentication } from '@services/auth';
import { Git } from '@services/git';
import { MenuService } from '@services/menu';
import { IAuthenticationService, Authentication } from '@services/auth';
import { IGitService, Git } from '@services/git';
import { IMenuService, MenuService } from '@services/menu';
import { Notification } from '@services/notifications';
import { Preference } from '@services/preferences';
import { IPreferenceService, Preference } from '@services/preferences';
import { SystemPreference } from '@services/systemPreferences';
import { Updater } from '@services/updater';
import { View } from '@services/view';
import { Wiki } from '@services/wiki';
import { IViewService, View } from '@services/view';
import { IWikiService, Wiki } from '@services/wiki';
import { WikiGitWorkspace } from '@services/wikiGitWorkspace';
import { Window } from '@services/windows';
import { IWindowService, Window } from '@services/windows';
import { WindowNames } from '@services/windows/WindowProperties';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { IWorkspaceService, Workspace } from '@services/workspaces';
import { IWorkspaceViewService, WorkspaceView } from '@services/workspacesView';
const gotTheLock = app.requestSingleInstanceLock();
container.bind<Authentication>(serviceIdentifier.Authentication).to(Authentication);
container.bind<Git>(serviceIdentifier.Git).to(Git);
container.bind<MenuService>(serviceIdentifier.View).to(MenuService);
container.bind<Notification>(serviceIdentifier.Notification).to(Notification);
container.bind<Preference>(serviceIdentifier.Preference).to(Preference);
container.bind<SystemPreference>(serviceIdentifier.SystemPreference).to(SystemPreference);
container.bind<Updater>(serviceIdentifier.Updater).to(Updater);
container.bind<View>(serviceIdentifier.View).to(View);
container.bind<Wiki>(serviceIdentifier.Wiki).to(Wiki);
container.bind<WikiGitWorkspace>(serviceIdentifier.WikiGitWorkspace).to(WikiGitWorkspace);
container.bind<Window>(serviceIdentifier.Window).to(Window);
container.bind<Workspace>(serviceIdentifier.Workspace).to(Workspace);
container.bind<WorkspaceView>(serviceIdentifier.WorkspaceView).to(WorkspaceView);
const authService = container.resolve(Authentication);
const gitService = container.resolve(Git);
const menuService = container.resolve(MenuService);
const preferenceService = container.resolve(Preference);
const viewService = container.resolve(View);
const wikiService = container.resolve(Wiki);
const windowService = container.resolve(Window);
const workspaceService = container.resolve(Workspace);
const workspaceViewService = container.resolve(WorkspaceView);
container.bind<Authentication>(serviceIdentifier.Authentication).to(Authentication).inSingletonScope();
container.bind<Git>(serviceIdentifier.Git).to(Git).inSingletonScope();
container.bind<MenuService>(serviceIdentifier.MenuService).to(MenuService).inSingletonScope();
container.bind<Notification>(serviceIdentifier.Notification).to(Notification).inSingletonScope();
container.bind<Preference>(serviceIdentifier.Preference).to(Preference).inSingletonScope();
container.bind<SystemPreference>(serviceIdentifier.SystemPreference).to(SystemPreference).inSingletonScope();
container.bind<Updater>(serviceIdentifier.Updater).to(Updater).inSingletonScope();
container.bind<View>(serviceIdentifier.View).to(View).inSingletonScope();
container.bind<Wiki>(serviceIdentifier.Wiki).to(Wiki).inSingletonScope();
container.bind<WikiGitWorkspace>(serviceIdentifier.WikiGitWorkspace).to(WikiGitWorkspace).inSingletonScope();
container.bind<Window>(serviceIdentifier.Window).to(Window).inSingletonScope();
container.bind<Workspace>(serviceIdentifier.Workspace).to(Workspace).inSingletonScope();
container.bind<WorkspaceView>(serviceIdentifier.WorkspaceView).to(WorkspaceView).inSingletonScope();
const authService = container.get<IAuthenticationService>(serviceIdentifier.Authentication);
const gitService = container.get<IGitService>(serviceIdentifier.Git);
const menuService = container.get<IMenuService>(serviceIdentifier.MenuService);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const viewService = container.get<IViewService>(serviceIdentifier.View);
const wikiService = container.get<IWikiService>(serviceIdentifier.Wiki);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.

View file

@ -12,8 +12,13 @@ export type IUserInfos = typeof defaultUserInfos;
/**
* Handle login to Github GitLab Coding.net
*/
export interface IAuthenticationService {
getUserInfos: () => IUserInfos;
get<K extends keyof IUserInfos>(key: K): IUserInfos[K] | undefined;
reset(): Promise<void>;
}
@injectable()
export class Authentication {
export class Authentication implements IAuthenticationService {
cachedUserInfo: IUserInfos;
readonly version = '2021.1';

View file

@ -6,9 +6,9 @@ import isDev from 'electron-is-dev';
import * as gitSync from './sync';
import * as github from './github';
import serviceIdentifiers from '@services/serviceIdentifier';
import { View } from '@services/view';
import { Preference } from '@services/preferences';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IViewService } from '@services/view';
import type { IPreferenceService } from '@services/preferences';
import { logger } from '@services/libs/log';
import i18n from '@services/libs/i18n';
import { IUserInfo } from '@services/types';
@ -17,13 +17,20 @@ import { IUserInfo } from '@services/types';
* System Preferences are not stored in storage but stored in macOS Preferences.
* It can be retrieved and changed using Electron APIs
*/
export interface IGitService {
debounceCommitAndSync: (wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo) => Promise<void> | undefined;
updateGitInfoTiddler(githubRepoName: string): Promise<void>;
initWikiGit(wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo, isMainWiki: boolean): Promise<void>;
commitAndSync(wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo): Promise<void>;
clone(githubRepoUrl: string, repoFolderPath: string, userInfo: IUserInfo): Promise<void>;
}
@injectable()
export class Git {
export class Git implements IGitService {
disableSyncOnDevelopment = true;
constructor(
@inject(serviceIdentifiers.View) private readonly viewService: View,
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifier.View) private readonly viewService: IViewService,
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
) {
const syncDebounceInterval = this.preferenceService.get('syncDebounceInterval');
this.debounceCommitAndSync = debounce(this.commitAndSync.bind(this), syncDebounceInterval);
@ -32,7 +39,7 @@ export class Git {
public debounceCommitAndSync: (wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo) => Promise<void> | undefined;
init(): void {
private init(): void {
ipcMain.handle('get-workspaces-remote', async (_event, wikiFolderPath) => {
return await github.getRemoteUrl(wikiFolderPath);
});

View file

@ -1,7 +1,8 @@
import { container } from '@services/container';
import { Window } from '@services/windows';
import { Preference } from '@services/preferences';
import type { IWindowService } from '@services/windows';
import type { IPreferenceService } from '@services/preferences';
import { WindowNames } from '@services/windows/WindowProperties';
import serviceIdentifier from '@services/serviceIdentifier';
export default function getViewBounds(
contentSize: [number, number],
@ -9,9 +10,9 @@ export default function getViewBounds(
height?: number,
width?: number,
): { x: number; y: number; height: number; width: number } {
const mainWindow = container.resolve(Window).get(WindowNames.main);
const mainWindow = container.get<IWindowService>(serviceIdentifier.Window).get(WindowNames.main);
const isFullScreen = mainWindow?.isFullScreen();
const preferencesService = container.resolve(Preference);
const preferencesService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const showSidebar = preferencesService.get('sidebar');
const showTitleBar = process.platform === 'darwin' && preferencesService.get('titleBar') && isFullScreen !== true;
const showNavigationBar =

View file

@ -1,12 +1,13 @@
import { ipcMain } from 'electron';
import { container } from '@services/container';
import { Window } from '@services/windows';
import type { IWindowService } from '@services/windows';
import { WindowNames } from '@services/windows/WindowProperties';
import { mainBindings } from './i18next-electron-fs-backend';
import serviceIdentifier from '@services/serviceIdentifier';
export default function bindI18nListener(): void {
const windows = container.resolve(Window);
const windows = container.get<IWindowService>(serviceIdentifier.Window);
const mainWindow = windows.get(WindowNames.main);
if (mainWindow === undefined) {
throw new Error('Window is undefined in bindI18nListener()');

View file

@ -3,10 +3,10 @@ import fs from 'fs-extra';
import path from 'path';
import { IpcRenderer, IpcMain, BrowserWindow, IpcMainInvokeEvent, IpcRendererEvent, MenuItemConstructorOptions } from 'electron';
import { Window } from '@services/windows';
import { Preference } from '@services/preferences';
import { View } from '@services/view';
import { MenuService } from '@services/menu';
import type { IWindowService } from '@services/windows';
import type { IPreferenceService } from '@services/preferences';
import type { IViewService } from '@services/view';
import serviceIdentifier from '@services/serviceIdentifier';
import { container } from '@services/container';
import { LOCALIZATION_FOLDER } from '@services/constants/paths';
import { I18NChannels } from '@/constants/channels';
@ -63,7 +63,7 @@ export const preloadBindings = function (
export const mainBindings = function (ipcMain: IpcMain, browserWindow: BrowserWindow): void {
ipcMain.handle(I18NChannels.readFileRequest, (_event: IpcMainInvokeEvent, readFileArgs: IReadFileRequest) => {
const localeFilePath = path.join(LOCALIZATION_FOLDER, readFileArgs.filename);
const windowService = container.resolve(Window);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
fs.readFile(localeFilePath, 'utf8', (error, data) => {
windowService.sendToAllWindows(I18NChannels.readFileResponse, {
key: readFileArgs.key,
@ -76,7 +76,7 @@ export const mainBindings = function (ipcMain: IpcMain, browserWindow: BrowserWi
ipcMain.handle(I18NChannels.writeFileRequest, (_event: IpcMainInvokeEvent, writeFileArgs: IWriteFileRequest) => {
const localeFilePath = path.join(LOCALIZATION_FOLDER, writeFileArgs.filename);
const localeFileFolderPath = path.dirname(localeFilePath);
const windowService = container.resolve(Window);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
fs.ensureDir(localeFileFolderPath, (directoryCreationError?: Error) => {
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (directoryCreationError) {
@ -110,10 +110,10 @@ const whiteListedLanguages = Object.keys(whitelistMap);
* Register languages into language menu, call this function after container init
*/
export function buildLanguageMenu(): void {
const preferenceService = container.resolve(Preference);
const windowService = container.resolve(Window);
const viewService = container.resolve(View);
const menuService = container.resolve(MenuService);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const viewService = container.get<IViewService>(serviceIdentifier.View);
const menuService = container.get<IMenuServiceService>(serviceIdentifier.MenuService);
const subMenu: MenuItemConstructorOptions[] = [];
for (const language of whiteListedLanguages) {
subMenu.push({

View file

@ -1,10 +1,11 @@
import i18n from 'i18next';
import { container } from '@services/container';
import { Preference } from '@services/preferences';
import type { IPreferenceService } from '@services/preferences';
import serviceIdentifier from '@services/serviceIdentifier';
export default async function useDefaultLanguage(i18next: typeof i18n): Promise<void> {
const preferences = container.resolve(Preference);
const preferences = container.get<IPreferenceService>(serviceIdentifier.Preference);
const language = preferences.get('language');
if (typeof language === 'string') {
await i18next.changeLanguage(language);

View file

@ -2,18 +2,19 @@
import Transport from 'winston-transport';
import { container } from '@services/container';
import { View } from '@services/view';
import { Window } from '@services/windows';
import type { IViewService } from '@services/view';
import type { IWindowService } from '@services/windows';
import serviceIdentifier from '@services/serviceIdentifier';
import { WindowNames } from '@services/windows/WindowProperties';
const handlers = {
createWikiProgress: (message: string) => {
const windowService = container.resolve(Window);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const createWorkspaceWindow = windowService.get(WindowNames.addWorkspace);
createWorkspaceWindow?.webContents?.send('create-wiki-progress', message);
},
wikiSyncProgress: (message: string) => {
const viewService = container.resolve(View);
const viewService = container.get<IViewService>(serviceIdentifier.View);
const browserView = viewService.getActiveBrowserView();
browserView?.webContents?.send('wiki-sync-progress', message);
},

View file

@ -1,9 +1,6 @@
import { Menu, MenuItemConstructorOptions, shell } from 'electron';
import { debounce, take, drop } from 'lodash';
import { injectable, inject } from 'inversify';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { View } from '@services/view';
import { injectable } from 'inversify';
interface DeferredMenuItemConstructorOptions extends Omit<MenuItemConstructorOptions, 'label' | 'enabled' | 'submenu'> {
label?: (() => string) | string;
@ -13,8 +10,15 @@ interface DeferredMenuItemConstructorOptions extends Omit<MenuItemConstructorOpt
| Array<MenuItemConstructorOptions | DeferredMenuItemConstructorOptions>;
}
/**
* Handle creation of app menu, other services can register their menu tab and menu items here.
*/
export interface IMenuService {
buildMenu(): void;
insertMenu(menuID: string, menuItems: DeferredMenuItemConstructorOptions[], afterSubMenu?: string | null, withSeparator = false): void;
}
@injectable()
export class MenuService {
export class MenuService implements IMenuService {
private readonly menuTemplate: DeferredMenuItemConstructorOptions[];
/**
@ -42,10 +46,7 @@ export class MenuService {
}));
}
constructor(
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.View) private readonly viewService: View,
) {
constructor() {
// debounce so build menu won't be call very frequently on app launch, where every services are registering menu items
this.buildMenu = debounce(this.buildMenu.bind(this), 50);
// add some default app menus
@ -124,7 +125,7 @@ export class MenuService {
* @param afterSubMenu The `id` or `role` of a submenu you want your submenu insert after. `null` means inserted as first submenu item; `undefined` means inserted as last submenu item;
* @param withSeparator Need to insert a separator first, before insert menu items
*/
insertMenu(menuID: string, menuItems: DeferredMenuItemConstructorOptions[], afterSubMenu?: string | null, withSeparator = false): void {
public insertMenu(menuID: string, menuItems: DeferredMenuItemConstructorOptions[], afterSubMenu?: string | null, withSeparator = false): void {
let foundMenuName = false;
// try insert menu into an existed menu's submenu
for (const menu of this.menuTemplate) {
@ -162,9 +163,9 @@ export class MenuService {
const afterSubMenuIndex = menu.submenu.findIndex((item) => item.id === afterSubMenu || item.role === afterSubMenu);
if (afterSubMenuIndex === -1) {
throw new Error(
`You try to insert menu with afterSubMenu ${afterSubMenu}, but we can not found it in menu ${
`You try to insert menu with afterSubMenu "${afterSubMenu}" in menu "${menuID}", but we can not found it in menu "${
menu.id ?? menu.role ?? JSON.stringify(menu)
}, please specific a menuitem with correct id attribute`,
}", please specific a menuitem with correct id attribute`,
);
}
menu.submenu = [...take(menu.submenu, afterSubMenuIndex + 1), ...menuItems, ...drop(menu.submenu, afterSubMenuIndex - 1)];

View file

@ -1,7 +1,7 @@
import { injectable, inject } from 'inversify';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { View } from '@services/view';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IPreferenceService } from '@services/preferences';
import type { IViewService } from '@services/view';
export interface IPauseNotificationsInfo {
reason: string;
@ -9,11 +9,18 @@ export interface IPauseNotificationsInfo {
schedule: { from: Date; to: Date };
}
/**
* Preference and method about notification, to set and pause notification.
*/
export interface INotificationService {
updatePauseNotificationsInfo(): void;
getPauseNotificationsInfo: () => IPauseNotificationsInfo | undefined;
}
@injectable()
export class Notification {
export class Notification implements INotificationService {
constructor(
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.View) private readonly viewService: View,
@inject(serviceIdentifier.Preference) private readonly preferenceService: IPreferenceService,
@inject(serviceIdentifier.View) private readonly viewService: IViewService,
) {}
private pauseNotificationsInfo?: IPauseNotificationsInfo;

View file

@ -5,10 +5,10 @@ import path from 'path';
import semver from 'semver';
import settings from 'electron-settings';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Window } from '@services/windows';
import { WorkspaceView } from '@services/workspacesView';
import { Notification } from '@services/notifications';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IWindowService } from '@services/windows';
import type { IWorkspaceViewService } from '@services/workspacesView';
import type { INotificationService } from '@services/notifications';
import { WindowNames } from '@services/windows/WindowProperties';
import { PreferenceChannel } from '@/constants/channels';
import { container } from '@services/container';
@ -82,11 +82,20 @@ const defaultPreferences = {
};
export type IPreferences = typeof defaultPreferences;
/**
* Getter and setter for app business logic preferences.
*/
export interface IPreferenceService {
set<K extends keyof IPreferences>(key: K, value: IPreferences[K]): Promise<void>;
getPreferences: () => IPreferences;
get<K extends keyof IPreferences>(key: K): IPreferences[K];
reset(): Promise<void>;
}
@injectable()
export class Preference {
@lazyInject(serviceIdentifiers.Window) private readonly windowService!: Window;
@lazyInject(serviceIdentifiers.Notification) private readonly notificationService!: Notification;
@lazyInject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService!: WorkspaceView;
@lazyInject(serviceIdentifier.Window) private readonly windowService!: IWindowService;
@lazyInject(serviceIdentifier.Notification) private readonly notificationService!: INotificationService;
@lazyInject(serviceIdentifier.WorkspaceView) private readonly workspaceViewService!: IWorkspaceViewService;
cachedPreferences: IPreferences;
readonly version = '2018.2';
@ -149,7 +158,7 @@ export class Preference {
/**
* load preferences in sync, and ensure it is an Object
*/
getInitPreferencesForCache = (): IPreferences => {
private readonly getInitPreferencesForCache = (): IPreferences => {
let preferencesFromDisk = settings.getSync(`preferences.${this.version}`) ?? {};
preferencesFromDisk = typeof preferencesFromDisk === 'object' && !Array.isArray(preferencesFromDisk) ? preferencesFromDisk : {};
return { ...defaultPreferences, ...this.sanitizePreference(preferencesFromDisk) };

View file

@ -6,22 +6,23 @@ import { contextBridge } from 'electron';
import { container } from '@services/container';
import { getModifiedFileList } from '@services/git/inspect';
import { Git } from '@services/git';
import { Workspace } from '@services/workspaces';
import { Authentication } from '@services/auth';
import type { IGitService } from '@services/git';
import type { IWorkspaceService } from '@services/workspaces';
import type { IAuthenticationService } from '@services/auth';
import serviceIdentifier from '@services/serviceIdentifier';
contextBridge.exposeInMainWorld('git', {
getModifiedFileList,
commitAndSync: (wikiPath: string, githubRepoUrl: string) => {
const gitService = container.resolve(Git);
const authService = container.resolve(Authentication);
const gitService = container.get<IGitService>(serviceIdentifier.Git);
const authService = container.get<IAuthenticationService>(serviceIdentifier.Authentication);
const userInfo = authService.get('authing');
if (userInfo !== undefined) {
return gitService.commitAndSync(wikiPath, githubRepoUrl, userInfo);
}
},
getWorkspacesAsList: () => {
const workspaceService = container.resolve(Workspace);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
return workspaceService.getWorkspacesAsList();
},
});

View file

@ -9,13 +9,18 @@ interface IUsedElectionSettings {
* System Preferences are not stored in storage but stored in macOS Preferences.
* It can be retrieved and changed using Electron APIs
*/
export interface ISystemPreferenceService {
get<K extends keyof IUsedElectionSettings>(key: K): IUsedElectionSettings[K];
getSystemPreferences(): IUsedElectionSettings;
setSystemPreference<K extends keyof IUsedElectionSettings>(key: K, value: IUsedElectionSettings[K]): void;
}
@injectable()
export class SystemPreference {
export class SystemPreference implements ISystemPreferenceService {
constructor() {
this.init();
}
init(): void {
private init(): void {
ipcMain.handle('get-system-preference', (event, key: keyof IUsedElectionSettings) => {
return this.get(key);
});

View file

@ -4,23 +4,24 @@ import { injectable, inject } from 'inversify';
import { autoUpdater, UpdateInfo } from 'electron-updater';
import type { ProgressInfo } from 'builder-util-runtime';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Window } from '@services/windows';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IWindowService } from '@services/windows';
import { WindowNames } from '@services/windows/WindowProperties';
// FIXME: use electron-forge 's auto update solution maybe see https://headspring.com/2020/09/24/building-signing-and-publishing-electron-forge-applications-for-windows/
// TODO: use electron-forge 's auto update solution maybe see https://headspring.com/2020/09/24/building-signing-and-publishing-electron-forge-applications-for-windows/
export interface IUpdaterService {}
@injectable()
export class Updater {
constructor(@inject(serviceIdentifiers.Window) private readonly windowService: Window) {}
export class Updater implements IUpdaterService {
constructor(@inject(serviceIdentifier.Window) private readonly windowService: IWindowService) {}
init(): void {
private init(): void {
(global as any).updateSilent = true;
global.updaterObj = {};
this.configAutoUpdater();
this.initIPC();
}
initIPC(): void {
private initIPC(): void {
ipcMain.handle('request-check-for-updates', async (_event, isSilent) => {
// https://github.com/electron-userland/electron-builder/issues/4028
if (!autoUpdater.isUpdaterActive()) {
@ -69,7 +70,7 @@ export class Updater {
});
}
configAutoUpdater(): void {
private configAutoUpdater(): void {
autoUpdater.on('checking-for-update', () => {
global.updaterObj = {
status: 'checking-for-update',

View file

@ -1,34 +1,57 @@
import { BrowserView, BrowserWindow, WebContents, app, session, dialog, ipcMain } from 'electron';
import { injectable, inject } from 'inversify';
import { injectable } from 'inversify';
import getDecorators from 'inversify-inject-decorators';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IPreferenceService } from '@services/preferences';
import type { IWorkspaceService } from '@services/workspaces';
import type { IWorkspaceViewService } from '@services/workspacesView';
import type { IWikiService } from '@services/wiki';
import type { IAuthenticationService } from '@services/auth';
import type { IWindowService } from '@services/windows';
import type { IMenuService } from '@services/menu';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { Wiki } from '@services/wiki';
import { Authentication } from '@services/auth';
import { Window } from '@services/windows';
import { MenuService } from '@services/menu';
import { WindowNames, IBrowserViewMetaData } from '@services/windows/WindowProperties';
import i18n from '../libs/i18n';
import i18n from '@services/libs/i18n';
import getViewBounds from '@services/libs/get-view-bounds';
import { extractDomain } from '@services/libs/url';
import { IWorkspace } from '@services/types';
import setupViewEventHandlers from './setupViewEventHandlers';
import getFromRenderer from '@services/libs/getFromRenderer';
import { MetaDataChannel } from '@/constants/channels';
import { container } from '@services/container';
const { lazyInject } = getDecorators(container);
/**
* BrowserView related things, the BrowserView is the webview like frame that renders our wiki website.
*/
export interface IViewService {
addView: (browserWindow: BrowserWindow, workspace: IWorkspace) => Promise<void>;
getView: (id: string) => BrowserView;
forEachView: (functionToRun: (view: BrowserView, id: string) => void) => void;
setActiveView: (browserWindow: BrowserWindow, id: string) => Promise<void>;
removeView: (id: string) => void;
setViewsAudioPref: (_shouldMuteAudio?: boolean) => void;
setViewsNotificationsPref: (_shouldPauseNotifications?: boolean) => void;
hibernateView: (id: string) => void;
reloadViewsDarkReader: () => void;
reloadViewsWebContentsIfDidFailLoad: () => void;
reloadViewsWebContents: () => void;
getActiveBrowserView: () => BrowserView | undefined;
realignActiveView: (browserWindow: BrowserWindow, activeId: string) => void;
}
@injectable()
export class View {
constructor(
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.Wiki) private readonly wikiService: Wiki,
@inject(serviceIdentifiers.Window) private readonly windowService: Window,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.Authentication) private readonly authService: Authentication,
@inject(serviceIdentifiers.MenuService) private readonly menuService: MenuService,
@inject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService: WorkspaceView,
) {
export class View implements IViewService {
@lazyInject(serviceIdentifier.Preference) private readonly preferenceService!: IPreferenceService;
@lazyInject(serviceIdentifier.Wiki) private readonly wikiService!: IWikiService;
@lazyInject(serviceIdentifier.Window) private readonly windowService!: IWindowService;
@lazyInject(serviceIdentifier.Workspace) private readonly workspaceService!: IWorkspaceService;
@lazyInject(serviceIdentifier.Authentication) private readonly authService!: IAuthenticationService;
@lazyInject(serviceIdentifier.MenuService) private readonly menuService!: IMenuService;
@lazyInject(serviceIdentifier.WorkspaceView) private readonly workspaceViewService!: IWorkspaceViewService;
constructor() {
this.initIPCHandlers();
this.registerMenu();
}
@ -404,10 +427,8 @@ export class View {
Object.keys(this.views).forEach((id) => {
const view = this.getView(id);
const workspace = this.workspaceService.get(id);
if (view !== undefined) {
if (workspace !== undefined) {
view.webContents.audioMuted = workspace.disableAudio || this.shouldMuteAudio;
}
if (view !== undefined && workspace !== undefined) {
view.webContents.audioMuted = workspace.disableAudio || this.shouldMuteAudio;
}
});
};

View file

@ -7,10 +7,11 @@ import getViewBounds from '@services/libs/get-view-bounds';
import { extractDomain, isInternalUrl } from '@services/libs/url';
import { buildResourcePath } from '@services/constants/paths';
import { Preference } from '@services/preferences';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { Window } from '@services/windows';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IPreferenceService } from '@services/preferences';
import type { IWorkspaceService } from '@services/workspaces';
import type { IWorkspaceViewService } from '@services/workspacesView';
import type { IWindowService } from '@services/windows';
import { WindowNames, IBrowserViewMetaData } from '@services/windows/WindowProperties';
import { container } from '@services/container';
@ -32,10 +33,10 @@ export default function setupViewEventHandlers(
{ workspace, shouldPauseNotifications, sharedWebPreferences }: IViewContext,
{ adjustUserAgentByUrl }: IViewModifier,
): void {
const workspaceService = container.resolve(Workspace);
const workspaceViewService = container.resolve(WorkspaceView);
const windowService = container.resolve(Window);
const preferenceService = container.resolve(Preference);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
const windowService = container.get<IWindowService>(serviceIdentifier.Window);
const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);
view.webContents.once('did-stop-loading', () => {
view.webContents.send('should-pause-notifications-changed', workspace.disableNotifications || shouldPauseNotifications);
@ -297,7 +298,7 @@ function handleNewWindow(
_postBody: Electron.PostBody,
newWindowContext: INewWindowContext,
): void {
const workspaceService = container.resolve(Workspace);
const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
const { view, workspace, sharedWebPreferences, adjustUserAgentByUrl } = newWindowContext;
const appUrl = workspaceService.get(workspace.id)?.homeUrl;

View file

@ -8,34 +8,63 @@ import isDev from 'electron-is-dev';
import { dialog, ipcMain } from 'electron';
import chokidar from 'chokidar';
import { trim, compact, debounce } from 'lodash';
import getDecorators from 'inversify-inject-decorators';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Authentication } from '@services/auth';
import { Window } from '@services/windows';
import { View } from '@services/view';
import { Workspace } from '@services/workspaces';
import { Git } from '@services/git';
import { WorkspaceView } from '@services/workspacesView';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IAuthenticationService } from '@services/auth';
import type { IWindowService } from '@services/windows';
import type { IViewService } from '@services/view';
import type { IWorkspaceService } from '@services/workspaces';
import type { IGitService } from '@services/git';
import type { IWorkspaceViewService } from '@services/workspacesView';
import { IWorkspace, IUserInfo } from '@services/types';
import { WindowNames } from '@services/windows/WindowProperties';
import { logger, wikiOutputToFile, refreshOutputFile } from '@services/libs/log';
import i18n from '@services/libs/i18n';
import { TIDDLYWIKI_TEMPLATE_FOLDER_PATH, TIDDLERS_PATH } from '@services/constants/paths';
import { updateSubWikiPluginContent, getSubWikiPluginContent } from './update-plugin-content';
import { container } from '@services/container';
const { lazyInject } = getDecorators(container);
/**
* Handle wiki worker startup and restart
*/
export interface IWikiService {
updateSubWikiPluginContent(mainWikiPath: string, newConfig?: IWorkspace, oldConfig?: IWorkspace): void;
startWiki(homePath: string, tiddlyWikiPort: number, userName: string): Promise<void>;
stopWiki(homePath: string): Promise<void>;
stopAllWiki(): Promise<void>;
linkWiki(mainWikiPath: string, folderName: string, subWikiPath: string): Promise<void>;
createWiki(newFolderPath: string, folderName: string): Promise<void>;
createSubWiki(newFolderPath: string, folderName: string, mainWikiPath: string, tagName?: string, onlyLink?: boolean): Promise<void>;
removeWiki(wikiPath: string, mainWikiToUnLink?: string, onlyRemoveLink?: boolean): Promise<void>;
ensureWikiExist(wikiPath: string, shouldBeMainWiki: boolean): Promise<void>;
cloneWiki(parentFolderLocation: string, wikiFolderName: string, githubWikiUrl: string, userInfo: IUserInfo): Promise<void>;
cloneSubWiki(
parentFolderLocation: string,
wikiFolderName: string,
mainWikiPath: string,
githubWikiUrl: string,
userInfo: IUserInfo,
tagName?: string,
): Promise<void>;
wikiStartup(workspace: IWorkspace): Promise<void>;
startNodeJSWiki(homePath: string, port: number, userName: string, workspaceID: string): Promise<void>;
watchWiki(wikiRepoPath: string, githubRepoUrl: string, userInfo: IUserInfo, wikiFolderPath?: string): Promise<void>;
stopWatchWiki(wikiRepoPath: string): Promise<void>;
stopWatchAllWiki(): Promise<void>;
}
@injectable()
export class Wiki {
constructor(
@inject(serviceIdentifiers.Authentication) private readonly authService: Authentication,
@inject(serviceIdentifiers.Window) private readonly windowService: Window,
@inject(serviceIdentifiers.Git) private readonly gitService: Git,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.View) private readonly viewService: View,
@inject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService: WorkspaceView,
) {
@lazyInject(serviceIdentifier.Authentication) private readonly authService!: IAuthenticationService;
@lazyInject(serviceIdentifier.Window) private readonly windowService!: IWindowService;
@lazyInject(serviceIdentifier.Git) private readonly gitService!: IGitService;
@lazyInject(serviceIdentifier.Workspace) private readonly workspaceService!: IWorkspaceService;
@lazyInject(serviceIdentifier.View) private readonly viewService!: IViewService;
@lazyInject(serviceIdentifier.WorkspaceView) private readonly workspaceViewService!: IWorkspaceViewService;
constructor() {
this.init();
}
@ -321,7 +350,7 @@ export class Wiki {
}
}
public async cloneWiki(parentFolderLocation: string, wikiFolderName: string, githubWikiUrl: any, userInfo: IUserInfo): Promise<void> {
public async cloneWiki(parentFolderLocation: string, wikiFolderName: string, githubWikiUrl: string, userInfo: IUserInfo): Promise<void> {
this.logProgress(i18n.t('AddWorkspace.StartCloningWiki'));
const newWikiPath = path.join(parentFolderLocation, wikiFolderName);
if (!(await fs.pathExists(parentFolderLocation))) {
@ -538,7 +567,7 @@ export class Wiki {
logger.info('All wiki watcher is stopped', { function: 'stopWatchAllWiki' });
}
updateSubWikiPluginContent(mainWikiPath: string, newConfig?: IWorkspace, oldConfig?: IWorkspace): void {
public updateSubWikiPluginContent(mainWikiPath: string, newConfig?: IWorkspace, oldConfig?: IWorkspace): void {
return updateSubWikiPluginContent(mainWikiPath, newConfig, oldConfig);
}
}

View file

@ -2,91 +2,89 @@ import path from 'path';
import { ipcMain, dialog } from 'electron';
import { injectable, inject } from 'inversify';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Wiki } from '@services/wiki';
import { Git } from '@services/git';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { Window } from '@services/windows';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IWikiService } from '@services/wiki';
import type { IGitService } from '@services/git';
import type { IWorkspaceService } from '@services/workspaces';
import type { IWorkspaceViewService } from '@services/workspacesView';
import type { IWindowService } from '@services/windows';
import { WindowNames } from '@services/windows/WindowProperties';
import { Authentication } from '@services/auth';
import type { IAuthenticationService } from '@services/auth';
import { logger } from '@services/libs/log';
import i18n from '@services/libs/i18n';
import { IUserInfo } from './types';
/**
* Deal with operations that needs to create a wiki and a git repo at once in a workspace
*/
export interface IWikiGitWorkspaceService {
initWikiGit: (wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo, isMainWiki: boolean) => Promise<string>;
removeWorkspace: (id: string) => Promise<void>;
}
@injectable()
export class WikiGitWorkspace {
constructor(
@inject(serviceIdentifiers.Wiki) private readonly wikiService: Wiki,
@inject(serviceIdentifiers.Git) private readonly gitService: Git,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.Window) private readonly windowService: Window,
@inject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService: WorkspaceView,
@inject(serviceIdentifiers.Authentication) private readonly authService: Authentication,
) {
this.init();
}
@inject(serviceIdentifier.Wiki) private readonly wikiService: IWikiService,
@inject(serviceIdentifier.Git) private readonly gitService: IGitService,
@inject(serviceIdentifier.Workspace) private readonly workspaceService: IWorkspaceService,
@inject(serviceIdentifier.Window) private readonly windowService: IWindowService,
@inject(serviceIdentifier.WorkspaceView) private readonly workspaceViewService: IWorkspaceViewService,
@inject(serviceIdentifier.Authentication) private readonly authService: IAuthenticationService,
) {}
private init(): void {
ipcMain.handle('request-init-wiki-git', async (_event, wikiFolderPath, githubRepoUrl, userInfo, isMainWiki) => {
public initWikiGit = async (wikiFolderPath: string, githubRepoUrl: string, userInfo: IUserInfo, isMainWiki: boolean): Promise<string> => {
try {
await this.gitService.initWikiGit(wikiFolderPath, githubRepoUrl, userInfo, isMainWiki);
return '';
} catch (error) {
console.info(error);
await this.wikiService.removeWiki(wikiFolderPath);
return String(error);
}
};
public removeWorkspace = async (id: string): Promise<void> => {
const mainWindow = this.windowService.get(WindowNames.main);
if (mainWindow !== undefined) {
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: [i18n.t('WorkspaceSelector.RemoveWorkspace'), i18n.t('WorkspaceSelector.RemoveWorkspaceAndDelete'), i18n.t('Cancel')],
message: i18n.t('WorkspaceSelector.AreYouSure'),
cancelId: 2,
});
try {
await this.gitService.initWikiGit(wikiFolderPath, githubRepoUrl, userInfo, isMainWiki);
return '';
} catch (error) {
console.info(error);
await this.wikiService.removeWiki(wikiFolderPath);
return String(error);
}
});
ipcMain.handle(
'request-remove-workspace',
async (_event, id: string): Promise<void> => {
const mainWindow = this.windowService.get(WindowNames.main);
if (mainWindow !== undefined) {
const { response } = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: [i18n.t('WorkspaceSelector.RemoveWorkspace'), i18n.t('WorkspaceSelector.RemoveWorkspaceAndDelete'), i18n.t('Cancel')],
message: i18n.t('WorkspaceSelector.AreYouSure'),
cancelId: 2,
});
try {
if (response === 0 || response === 1) {
const workspace = this.workspaceService.get(id);
if (workspace === undefined) {
throw new Error(`Need to get workspace with id ${id} but failed`);
}
await this.wikiService.stopWatchWiki(workspace.name).catch((error) => logger.error((error as Error).message, error));
await this.wikiService.stopWiki(workspace.name).catch((error: any) => logger.error((error as Error).message, error));
await this.wikiService.removeWiki(workspace.name, workspace.isSubWiki ? workspace.mainWikiToLink : undefined, response === 0);
await this.workspaceViewService.removeWorkspaceView(id);
// TODO: createMenu();
// createMenu();
// restart the main wiki to load content from private wiki
const mainWikiPath = workspace.mainWikiToLink;
const mainWorkspace = this.workspaceService.getByName(mainWikiPath);
if (mainWorkspace === undefined) {
throw new Error(`Need to get mainWorkspace with name ${mainWikiPath} but failed`);
}
const userName = this.authService.get('userName') ?? '';
await this.wikiService.stopWiki(mainWikiPath);
await this.wikiService.startWiki(mainWikiPath, mainWorkspace.port, userName);
// remove folderName from fileSystemPaths
if (workspace.isSubWiki) {
this.wikiService.updateSubWikiPluginContent(mainWikiPath, undefined, {
...workspace,
subWikiFolderName: path.basename(workspace.name),
});
}
}
} catch (error) {
logger.error((error as Error).message, error);
if (response === 0 || response === 1) {
const workspace = this.workspaceService.get(id);
if (workspace === undefined) {
throw new Error(`Need to get workspace with id ${id} but failed`);
}
await this.wikiService.stopWatchWiki(workspace.name).catch((error) => logger.error((error as Error).message, error));
await this.wikiService.stopWiki(workspace.name).catch((error: any) => logger.error((error as Error).message, error));
await this.wikiService.removeWiki(workspace.name, workspace.isSubWiki ? workspace.mainWikiToLink : undefined, response === 0);
await this.workspaceViewService.removeWorkspaceView(id);
// TODO: createMenu();
// createMenu();
// restart the main wiki to load content from private wiki
const mainWikiPath = workspace.mainWikiToLink;
const mainWorkspace = this.workspaceService.getByName(mainWikiPath);
if (mainWorkspace === undefined) {
throw new Error(`Need to get mainWorkspace with name ${mainWikiPath} but failed`);
}
const userName = this.authService.get('userName') ?? '';
await this.wikiService.stopWiki(mainWikiPath);
await this.wikiService.startWiki(mainWikiPath, mainWorkspace.port, userName);
// remove folderName from fileSystemPaths
if (workspace.isSubWiki) {
this.wikiService.updateSubWikiPluginContent(mainWikiPath, undefined, {
...workspace,
subWikiFolderName: path.basename(workspace.name),
});
}
}
},
);
}
} catch (error) {
logger.error((error as Error).message, error);
}
}
};
}

View file

@ -2,14 +2,17 @@
import { BrowserWindow, ipcMain, dialog, app, App, remote, clipboard, BrowserWindowConstructorOptions } from 'electron';
import isDevelopment from 'electron-is-dev';
import { injectable, inject } from 'inversify';
import getDecorators from 'inversify-inject-decorators';
import windowStateKeeper, { State as windowStateKeeperState } from 'electron-window-state';
import { IBrowserViewMetaData, WindowNames, windowDimension, WindowMeta, CodeInjectionType } from '@services/windows/WindowProperties';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { MenuService } from '@services/menu';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IPreferenceService } from '@services/preferences';
import type { IWorkspaceService } from '@services/workspaces';
import type { IWorkspaceViewService } from '@services/workspacesView';
import type { IMenuService } from '@services/menu';
import { container } from '@services/container';
import { Channels, WindowChannel, MetaDataChannel } from '@/constants/channels';
import i18n from '@services/libs/i18n';
@ -17,17 +20,35 @@ import getViewBounds from '@services/libs/get-view-bounds';
import getFromRenderer from '@services/libs/getFromRenderer';
import handleAttachToMenuBar from './handleAttachToMenuBar';
const { lazyInject } = getDecorators(container);
/**
* Create and manage window open and destroy, you can get all opened electron window instance here
*/
export interface IWindowService {
get(windowName: WindowNames): BrowserWindow | undefined;
open<N extends WindowNames>(windowName: N, meta?: WindowMeta[N], recreate?: boolean | ((windowMeta: WindowMeta[N]) => boolean)): Promise<void>;
setWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): void;
updateWindowMeta<N extends WindowNames>(windowName: N, meta?: WindowMeta[N]): void;
getWindowMeta<N extends WindowNames>(windowName: N): WindowMeta[N];
sendToAllWindows: (channel: Channels, ...arguments_: unknown[]) => void;
goHome(windowName: WindowNames): Promise<void>;
goBack(windowName: WindowNames): void;
goForward(windowName: WindowNames): void;
reload(windowName: WindowNames): void;
showMessageBox(message: Electron.MessageBoxOptions['message'], type?: Electron.MessageBoxOptions['type']): void;
}
@injectable()
export class Window {
private windows = {} as Record<WindowNames, BrowserWindow | undefined>;
private windowMeta = {} as WindowMeta;
constructor(
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService: WorkspaceView,
@inject(serviceIdentifiers.MenuService) private readonly menuService: MenuService,
) {
@lazyInject(serviceIdentifier.Preference) private readonly preferenceService!: IPreferenceService;
@lazyInject(serviceIdentifier.Workspace) private readonly workspaceService!: IWorkspaceService;
@lazyInject(serviceIdentifier.WorkspaceView) private readonly workspaceViewService!: IWorkspaceViewService;
@lazyInject(serviceIdentifier.MenuService) private readonly menuService!: IMenuService;
constructor() {
this.initIPCHandlers();
this.registerMenu();
}
@ -391,45 +412,41 @@ export class Window {
'close',
);
this.menuService.insertMenu(
'Edit',
[
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.webContents.focus();
mainWindow.webContents.send('open-find-in-page');
const contentSize = mainWindow.getContentSize();
const view = mainWindow.getBrowserView();
view?.setBounds(getViewBounds(contentSize as [number, number], true));
}
},
enabled: () => this.workspaceService.countWorkspaces() > 0,
this.menuService.insertMenu('Edit', [
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.webContents.focus();
mainWindow.webContents.send('open-find-in-page');
const contentSize = mainWindow.getContentSize();
const view = mainWindow.getBrowserView();
view?.setBounds(getViewBounds(contentSize as [number, number], true));
}
},
{
label: 'Find Next',
accelerator: 'CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', true);
},
enabled: () => this.workspaceService.countWorkspaces() > 0,
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{
label: 'Find Next',
accelerator: 'CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', true);
},
{
label: 'Find Previous',
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', false);
},
enabled: () => this.workspaceService.countWorkspaces() > 0,
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{
label: 'Find Previous',
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', false);
},
],
'close',
);
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
]);
this.menuService.insertMenu('History', [
{

View file

@ -12,20 +12,45 @@ import isUrl from 'is-url';
import download from 'download';
import tmp from 'tmp';
import serviceIdentifiers from '@services/serviceIdentifier';
import serviceIdentifier from '@services/serviceIdentifier';
import { container } from '@services/container';
import { Wiki } from '@services/wiki';
import { View } from '@services/view';
import { WorkspaceView } from '@services/workspacesView';
import { Window } from '@services/windows';
import type { IWikiService } from '@services/wiki';
import type { IViewService } from '@services/view';
import type { IWorkspaceViewService } from '@services/workspacesView';
import type { IWindowService } from '@services/windows';
import type { IMenuService } from '@services/menu';
import { WindowNames } from '@services/windows/WindowProperties';
import { MenuService } from '@services/menu';
import { IWorkspace, IWorkspaceMetaData } from '@services/types';
const { lazyInject } = getDecorators(container);
/**
* Manage workspace level preferences and workspace metadata.
*/
export interface IWorkspaceService {
getWorkspacesAsList(): IWorkspace[];
get(id: string): IWorkspace | undefined;
create(newWorkspaceConfig: Omit<IWorkspace, 'active' | 'hibernated' | 'id' | 'order'>): Promise<IWorkspace>;
getWorkspaces(): Record<string, IWorkspace>;
countWorkspaces(): number;
getMetaData: (id: string) => Partial<IWorkspaceMetaData>;
getAllMetaData: () => Record<string, Partial<IWorkspaceMetaData>>;
updateMetaData: (id: string, options: Partial<IWorkspaceMetaData>) => void;
set(id: string, workspace: IWorkspace): Promise<void>;
update(id: string, workspaceSetting: Partial<IWorkspace>): Promise<void>;
setWorkspaces(newWorkspaces: Record<string, IWorkspace>): Promise<void>;
setActiveWorkspace(id: string): Promise<void>;
setWorkspacePicture(id: string, sourcePicturePath: string): Promise<void>;
removeWorkspacePicture(id: string): Promise<void>;
remove(id: string): Promise<void>;
getByName(name: string): IWorkspace | undefined;
getPreviousWorkspace: (id: string) => IWorkspace | undefined;
getNextWorkspace: (id: string) => IWorkspace | undefined;
getActiveWorkspace: () => IWorkspace | undefined;
getFirstWorkspace: () => IWorkspace | undefined;
}
@injectable()
export class Workspace {
export class Workspace implements IWorkspaceService {
/**
* version of current setting schema
*/
@ -35,11 +60,11 @@ export class Workspace {
*/
workspaces: Record<string, IWorkspace> = {};
@lazyInject(serviceIdentifiers.Wiki) private readonly wikiService!: Wiki;
@lazyInject(serviceIdentifiers.Window) private readonly windowService!: Window;
@lazyInject(serviceIdentifiers.View) private readonly viewService!: View;
@lazyInject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService!: WorkspaceView;
@lazyInject(serviceIdentifiers.MenuService) private readonly menuService!: MenuService;
@lazyInject(serviceIdentifier.Wiki) private readonly wikiService!: IWikiService;
@lazyInject(serviceIdentifier.Window) private readonly windowService!: IWindowService;
@lazyInject(serviceIdentifier.View) private readonly viewService!: IViewService;
@lazyInject(serviceIdentifier.WorkspaceView) private readonly workspaceViewService!: IWorkspaceViewService;
@lazyInject(serviceIdentifier.MenuService) private readonly menuService!: IMenuService;
constructor() {
this.workspaces = this.getInitWorkspacesForCache();
@ -178,22 +203,22 @@ export class Workspace {
/**
* Get sorted workspace list
*/
getWorkspacesAsList(): IWorkspace[] {
public getWorkspacesAsList(): IWorkspace[] {
return Object.values(this.workspaces).sort((a, b) => a.order - b.order);
}
get(id: string): IWorkspace | undefined {
public get(id: string): IWorkspace | undefined {
return this.workspaces[id];
}
async set(id: string, workspace: IWorkspace): Promise<void> {
public async set(id: string, workspace: IWorkspace): Promise<void> {
this.workspaces[id] = this.sanitizeWorkspace(workspace);
await this.reactBeforeWorkspaceChanged(workspace);
await settings.set(`workspaces.${this.version}.${id}`, { ...workspace });
this.updateWorkspaceMenuItems();
}
async update(id: string, workspaceSetting: Partial<IWorkspace>): Promise<void> {
public async update(id: string, workspaceSetting: Partial<IWorkspace>): Promise<void> {
const workspace = this.get(id);
if (workspace === undefined) {
return;
@ -201,7 +226,7 @@ export class Workspace {
await this.set(id, { ...workspace, ...workspaceSetting });
}
async setWorkspaces(newWorkspaces: Record<string, IWorkspace>): Promise<void> {
public async setWorkspaces(newWorkspaces: Record<string, IWorkspace>): Promise<void> {
for (const id in newWorkspaces) {
await this.set(id, newWorkspaces[id]);
}
@ -232,11 +257,11 @@ export class Workspace {
}
}
getByName(name: string): IWorkspace | undefined {
public getByName(name: string): IWorkspace | undefined {
return this.getWorkspacesAsList().find((workspace) => workspace.name === name);
}
getPreviousWorkspace = (id: string): IWorkspace | undefined => {
public getPreviousWorkspace = (id: string): IWorkspace | undefined => {
const workspaceList = this.getWorkspacesAsList();
let currentWorkspaceIndex = 0;
for (const [index, workspace] of workspaceList.entries()) {
@ -251,7 +276,7 @@ export class Workspace {
return workspaceList[currentWorkspaceIndex - 1];
};
getNextWorkspace = (id: string): IWorkspace | undefined => {
public getNextWorkspace = (id: string): IWorkspace | undefined => {
const workspaceList = this.getWorkspacesAsList();
let currentWorkspaceIndex = 0;
for (const [index, workspace] of workspaceList.entries()) {
@ -266,15 +291,15 @@ export class Workspace {
return workspaceList[currentWorkspaceIndex + 1];
};
getActiveWorkspace = (): IWorkspace | undefined => {
public getActiveWorkspace = (): IWorkspace | undefined => {
return this.getWorkspacesAsList().find((workspace) => workspace.active);
};
getFirstWorkspace = (): IWorkspace | undefined => {
public getFirstWorkspace = (): IWorkspace | undefined => {
return this.getWorkspacesAsList()[0];
};
async setActiveWorkspace(id: string): Promise<void> {
public async setActiveWorkspace(id: string): Promise<void> {
const currentActiveWorkspace = this.getActiveWorkspace();
if (currentActiveWorkspace !== undefined) {
if (currentActiveWorkspace.id === id) {
@ -292,7 +317,7 @@ export class Workspace {
* @param id workspace id
* @param sourcePicturePath image path, could be an image in app's resource folder or temp folder, we will copy it into app data folder
*/
async setWorkspacePicture(id: string, sourcePicturePath: string): Promise<void> {
public async setWorkspacePicture(id: string, sourcePicturePath: string): Promise<void> {
const workspace = this.get(id);
if (workspace === undefined) {
throw new Error(`Try to setWorkspacePicture() but this workspace is not existed ${id}`);
@ -335,7 +360,7 @@ export class Workspace {
}
}
async removeWorkspacePicture(id: string): Promise<void> {
public async removeWorkspacePicture(id: string): Promise<void> {
const workspace = this.get(id);
if (workspace === undefined) {
throw new Error(`Try to removeWorkspacePicture() but this workspace is not existed ${id}`);
@ -350,7 +375,7 @@ export class Workspace {
}
}
async remove(id: string): Promise<void> {
public async remove(id: string): Promise<void> {
if (id in this.workspaces) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.workspaces[id];
@ -365,7 +390,7 @@ export class Workspace {
this.updateWorkspaceMenuItems();
}
async create(newWorkspaceConfig: Omit<IWorkspace, 'active' | 'hibernated' | 'id' | 'order'>): Promise<IWorkspace> {
public async create(newWorkspaceConfig: Omit<IWorkspace, 'active' | 'hibernated' | 'id' | 'order'>): Promise<IWorkspace> {
const newID = uuid();
// find largest order

View file

@ -1,26 +1,36 @@
import { app, ipcMain, session } from 'electron';
import { injectable, inject } from 'inversify';
import serviceIdentifiers from '@services/serviceIdentifier';
import { View } from '@services/view';
import { Workspace } from '@services/workspaces';
import { Window } from '@services/windows';
import { MenuService } from '@services/menu';
import serviceIdentifier from '@services/serviceIdentifier';
import type { IViewService } from '@services/view';
import type { IWorkspaceService } from '@services/workspaces';
import type { IWindowService } from '@services/windows';
import type { IMenuService } from '@services/menu';
import { IWorkspace } from '@services/types';
import { WindowNames } from '@services/windows/WindowProperties';
import { Preference } from '@services/preferences';
/**
* Deal with operations that needs to create a workspace and a browserView at once
*/
export interface IWorkspaceViewService {
createWorkspaceView(workspaceOptions: IWorkspace): Promise<void>;
setWorkspaceView(id: string, workspaceOptions: IWorkspace): Promise<void>;
setWorkspaceViews(workspaces: Record<string, IWorkspace>): Promise<void>;
wakeUpWorkspaceView(id: string): Promise<void>;
hibernateWorkspaceView(id: string): Promise<void>;
setActiveWorkspaceView(id: string): Promise<void>;
removeWorkspaceView(id: string): Promise<void>;
clearBrowsingData(): Promise<void>;
loadURL(url: string, id: string): Promise<void>;
realignActiveWorkspace(): void;
}
@injectable()
export class WorkspaceView {
export class WorkspaceView implements IWorkspaceViewService {
constructor(
@inject(serviceIdentifiers.View) private readonly viewService: View,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.Window) private readonly windowService: Window,
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.MenuService) private readonly menuService: MenuService,
@inject(serviceIdentifier.View) private readonly viewService: IViewService,
@inject(serviceIdentifier.Workspace) private readonly workspaceService: IWorkspaceService,
@inject(serviceIdentifier.Window) private readonly windowService: IWindowService,
@inject(serviceIdentifier.MenuService) private readonly menuService: IMenuService,
) {
this.initIPCHandlers();
this.registerMenu();

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires */
// @ts-expect-error ts-migrate(2451) FIXME: Cannot redeclare block-scoped variable 'webpackAli... Remove this comment to see the full error message
const { webpackAlias } = require('./webpack.alias');
const CopyPlugin = require('copy-webpack-plugin');
const plugins = require('./webpack.plugins');
module.exports = {
/**
@ -13,12 +13,7 @@ module.exports = {
module: {
rules: require('./webpack.rules'),
},
plugins: [
new CopyPlugin({
// to is relative to ./.webpack/main/
patterns: [{ from: 'src/services/wiki/wiki-worker.js', to: 'wiki-worker.js' }],
}),
],
plugins: plugins.main,
resolve: {
alias: webpackAlias,
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],

View file

@ -2,8 +2,29 @@
const webpack = require('webpack');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const CspHtmlWebpackPlugin = require('csp-html-webpack-plugin');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = [
exports.main = [
// new ForkTsCheckerWebpackPlugin(),
new CopyPlugin({
// to is relative to ./.webpack/main/
patterns: [{ from: 'src/services/wiki/wiki-worker.js', to: 'wiki-worker.js' }],
}),
new CircularDependencyPlugin({
// exclude detection of files based on a RegExp
exclude: /node_modules/,
// add errors to webpack instead of warnings
failOnError: true,
// allow import cycles that include an asyncronous import,
// e.g. via import(/* webpackMode: "weak" */ './file.js')
allowAsyncCycles: true,
// set the current working directory for displaying module paths
cwd: process.cwd(),
}),
];
exports.renderer = [
// new ForkTsCheckerWebpackPlugin(),
new webpack.DefinePlugin({
'process.env': '{}',

View file

@ -9,7 +9,7 @@ module.exports = {
module: {
rules,
},
plugins: plugins,
plugins: plugins.renderer,
resolve: {
alias: webpackAlias,
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'],