refactor: WorkspaceService

This commit is contained in:
tiddlygit-test 2021-01-01 00:29:09 +08:00
parent f17fefab20
commit 88481725b6
5 changed files with 329 additions and 259 deletions

View file

@ -3,7 +3,9 @@
"fullscreenable",
"maximizable",
"minimizable",
"subwiki"
"subwiki",
"subwiki's",
"tiddlywiki's"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.localesPaths": "localization/locales",

View file

@ -1,254 +0,0 @@
import { app } from 'electron';
import path from 'path';
import fsExtra from 'fs-extra';
import settings from 'electron-settings';
import { v1 as uuidv1 } from 'uuid';
import Jimp from 'jimp';
import isUrl from 'is-url';
import download from 'download';
import tmp from 'tmp';
import sendToAllWindows from './send-to-all-windows';
import wikiStartup from './wiki/wiki-startup';
import { stopWatchWiki } from './wiki/watch-wiki';
import { stopWiki } from './wiki/wiki-worker-mamager';
import { updateSubWikiPluginContent } from './wiki/update-plugin-content';
const v = '14';
let workspaces: any;
const countWorkspaces = () => Object.keys(workspaces).length;
const getWorkspaces = () => {
if (workspaces) return workspaces;
const storedWorkspaces = settings.getSync(`workspaces.${v}`) || {};
// keep workspace objects in memory
workspaces = storedWorkspaces;
return workspaces;
};
const getWorkspacesAsList = () => {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
const workspaceLst = Object.values(getWorkspaces()).sort((a, b) => a.order - b.order);
return workspaceLst;
};
const getWorkspace = (id: any) => workspaces[id];
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
const getWorkspaceByName = (name: any) => getWorkspacesAsList().find((workspace) => workspace.name === name);
const getPreviousWorkspace = (id: any) => {
const workspaceLst = getWorkspacesAsList();
let currentWorkspaceI = 0;
for (const [index, element] of workspaceLst.entries()) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
if (element.id === id) {
currentWorkspaceI = index;
break;
}
}
if (currentWorkspaceI === 0) {
return workspaceLst[workspaceLst.length - 1];
}
return workspaceLst[currentWorkspaceI - 1];
};
const getNextWorkspace = (id: any) => {
const workspaceLst = getWorkspacesAsList();
let currentWorkspaceI = 0;
for (const [index, element] of workspaceLst.entries()) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
if (element.id === id) {
currentWorkspaceI = index;
break;
}
}
if (currentWorkspaceI === workspaceLst.length - 1) {
return workspaceLst[0];
}
return workspaceLst[currentWorkspaceI + 1];
};
const getActiveWorkspace = () => {
if (!workspaces) return null;
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
return Object.values(workspaces).find((workspace) => workspace.active);
};
const setActiveWorkspace = (id: any) => {
// deactive the current one
let currentActiveWorkspace = getActiveWorkspace();
if (currentActiveWorkspace) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
if (currentActiveWorkspace.id === id) return;
// @ts-expect-error ts-migrate(2698) FIXME: Spread types may only be created from object types... Remove this comment to see the full error message
currentActiveWorkspace = { ...currentActiveWorkspace };
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
currentActiveWorkspace.active = false;
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
workspaces[currentActiveWorkspace.id] = currentActiveWorkspace;
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
sendToAllWindows('set-workspace', currentActiveWorkspace.id, currentActiveWorkspace);
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
settings.setSync(`workspaces.${v}.${currentActiveWorkspace.id}`, currentActiveWorkspace);
}
// active new one
const newActiveWorkspace = { ...workspaces[id] };
newActiveWorkspace.active = true;
newActiveWorkspace.hibernated = false;
workspaces[id] = newActiveWorkspace;
sendToAllWindows('set-workspace', id, newActiveWorkspace);
settings.setSync(`workspaces.${v}.${id}`, newActiveWorkspace);
};
const setWorkspace = (id: any, options: any) => {
const workspace = { ...workspaces[id], ...options };
// set fileSystemPaths on sub-wiki setting update
if (workspaces[id].isSubWiki && options.tagName && workspaces[id].tagName !== options.tagName) {
const subWikiFolderName = path.basename(workspaces[id].name);
updateSubWikiPluginContent(
workspaces[id].mainWikiToLink,
{ tagName: options.tagName, subWikiFolderName },
{ tagName: workspaces[id].tagName, subWikiFolderName },
);
wikiStartup(workspace);
}
workspaces[id] = workspace;
sendToAllWindows('set-workspace', id, workspace);
settings.setSync(`workspaces.${v}.${id}`, workspace);
};
const setWorkspaces = (newWorkspaces: any) => {
workspaces = newWorkspaces;
sendToAllWindows('set-workspaces', newWorkspaces);
settings.setSync(`workspaces.${v}`, newWorkspaces);
};
const setWorkspacePicture = (id: any, sourcePicturePath: any) => {
const workspace = getWorkspace(id);
const pictureId = uuidv1();
if (workspace.picturePath === sourcePicturePath) {
return;
}
const destinationPicturePath = path.join(app.getPath('userData'), 'pictures', `${pictureId}.png`);
Promise.resolve()
.then(() => {
if (isUrl(sourcePicturePath)) {
const temporaryObject = tmp.dirSync();
const temporaryPath = temporaryObject.name;
return download(sourcePicturePath, temporaryPath, {
filename: 'e.png',
}).then(() => path.join(temporaryPath, 'e.png'));
}
return sourcePicturePath;
})
.then((picturePath) => Jimp.read(picturePath))
.then(
(img) =>
new Promise((resolve) => {
img.clone().resize(128, 128).quality(100).write(destinationPicturePath, resolve);
}),
)
.then(() => {
const currentPicturePath = getWorkspace(id).picturePath;
setWorkspace(id, {
pictureId,
picturePath: destinationPicturePath,
});
if (currentPicturePath) {
return fsExtra.remove(currentPicturePath);
}
return null;
})
.catch(console.log); // eslint-disable-line no-console
};
const removeWorkspacePicture = (id: any) => {
const workspace = getWorkspace(id);
if (workspace.picturePath) {
return fsExtra.remove(workspace.picturePath).then(() => {
setWorkspace(id, {
pictureId: null,
picturePath: null,
});
});
}
return Promise.resolve();
};
const removeWorkspace = (id: any) => {
const { name } = workspaces[id];
stopWiki(name);
stopWatchWiki(name);
delete workspaces[id];
sendToAllWindows('set-workspace', id, null);
settings.unsetSync(`workspaces.${v}.${id}`);
};
const createWorkspace = (name: any, isSubWiki: any, mainWikiToLink: any, port: any, homeUrl: any, gitUrl: any, transparentBackground: any, tagName: any) => {
const newId = uuidv1();
// find largest order
const workspaceLst = getWorkspacesAsList();
let max = 0;
for (const element of workspaceLst) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
if (element.order > max) {
// @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
max = element.order;
}
}
const newWorkspace = {
active: false,
hibernated: false,
isSubWiki,
mainWikiToLink,
port,
homeUrl,
gitUrl,
id: newId,
name,
order: max + 1,
transparentBackground,
tagName,
};
workspaces[newId] = newWorkspace;
sendToAllWindows('set-workspace', newId, newWorkspace);
settings.setSync(`workspaces.${v}.${newId}`, newWorkspace);
return newWorkspace;
};
export {
countWorkspaces,
createWorkspace,
getActiveWorkspace,
getNextWorkspace,
getPreviousWorkspace,
getWorkspace,
getWorkspaceByName,
getWorkspaces,
getWorkspacesAsList,
removeWorkspace,
setActiveWorkspace,
setWorkspace,
setWorkspaces,
setWorkspacePicture,
removeWorkspacePicture,
};

View file

@ -190,6 +190,13 @@ export class Preference {
// }
}
/**
* Batch update all preferences
*/
private async setPreferences(newPreferences: IPreferences): Promise<void> {
await settings.set(`preferences.${this.version}`, newPreferences);
}
/**
* get preferences, may return cached version
*/
@ -205,10 +212,6 @@ export class Preference {
return this.cachedPreferences[key];
}
private async setPreferences(newPreferences: IPreferences): Promise<void> {
await settings.set(`preferences.${this.version}`, newPreferences);
}
public async reset(): Promise<void> {
await settings.unset();
const preferences = this.getPreferences();

View file

@ -1,3 +1,53 @@
export interface IService {
init?: (...parameters: never[]) => Promise<void>;
}
export interface IWorkspace {
/**
* Is this workspace selected by user, and showing corresponding webview?
*/
active: boolean;
/**
* Is this workspace hibernated
*/
hibernated: boolean;
/**
* Is this workspace a subwiki that link to a main wiki, and doesn't have its own webview?
*/
isSubWiki: boolean;
/**
* Only useful when isSubWiki === true , this is the wiki repo that this subwiki's folder soft links to
*/
mainWikiToLink: string;
/**
* Localhost tiddlywiki server port
*/
port: number;
/**
* Localhost server url to load in the electron webview
*/
homeUrl: string;
/**
* The online repo to back data up to
*/
gitUrl: string;
id: string;
/**
* Display name for this wiki workspace
*/
name: string;
/**
* You can drag workspaces to reorder them
*/
order: number;
transparentBackground: boolean;
/**
* Tag name in tiddlywiki's filesystemPath, tiddler with this tag will be save into this subwiki
*/
tagName: string;
subWikiFolderName: string;
/**
* workspace icon's path in file system
*/
picturePath: string | null;
}

269
src/services/workspaces.ts Normal file
View file

@ -0,0 +1,269 @@
/* eslint-disable unicorn/no-null */
import { injectable } from 'inversify';
import getDecorators from 'inversify-inject-decorators';
import { app, App, remote, ipcMain, dialog } from 'electron';
import settings from 'electron-settings';
import { pickBy, mapValues } from 'lodash';
import { v4 as uuid } from 'uuid';
import path from 'path';
import fsExtra from 'fs-extra';
import Jimp from 'jimp';
import isUrl from 'is-url';
import download from 'download';
import tmp from 'tmp';
import serviceIdentifiers from '@services/serviceIdentifier';
import { container } from '@/services/container';
import { Window } from '@/services/windows';
import { IWorkspace } from '@/services/types';
const { lazyInject } = getDecorators(container);
@injectable()
export class Workspace {
/**
* version of current setting schema
*/
version = '14';
/**
* Record from workspace id to workspace settings
*/
workspaces: Record<string, IWorkspace> = {};
@lazyInject(serviceIdentifiers.Window) private readonly windowService!: Window;
constructor() {
this.init();
}
init(): void {
this.workspaces = this.getInitWorkspacesForCache();
}
/**
* load workspaces in sync, and ensure it is an Object
*/
getInitWorkspacesForCache = (): Record<string, IWorkspace> => {
const workspacesFromDisk = settings.getSync(`workspaces.${this.version}`) ?? {};
return typeof workspacesFromDisk === 'object' && !Array.isArray(workspacesFromDisk)
? mapValues((pickBy(workspacesFromDisk, (value) => value !== null) as unknown) as Record<string, IWorkspace>, (workspace) =>
this.sanitizeWorkspace(workspace),
)
: {};
};
public getWorkspaces(): Record<string, IWorkspace> {
return this.workspaces;
}
public get countWorkspaces(): number {
return Object.keys(this.workspaces).length;
}
getWorkspacesAsList(): IWorkspace[] {
return Object.values(this.workspaces).sort((a, b) => a.order - b.order);
}
get(id: string): IWorkspace | undefined {
return this.workspaces[id];
}
async set(id: string, workspace: IWorkspace): Promise<void> {
this.workspaces[id] = workspace;
await settings.set(`workspaces.${this.version}.${id}`, { ...workspace });
await this.reactWhenWorkspaceChanged(workspace);
}
async update(id: string, workspaceSetting: Partial<IWorkspace>): Promise<void> {
const workspace = this.get(id);
if (workspace === undefined) {
return;
}
await this.set(id, { ...workspace, ...workspaceSetting });
}
async setWorkspaces(newWorkspaces: Record<string, IWorkspace>): Promise<void> {
for (const id in newWorkspaces) {
await this.set(id, newWorkspaces[id]);
}
}
/**
* Pure function that make sure workspace setting is consistent
* @param workspaceToSanitize User input workspace or loaded workspace, that may contains bad values
*/
private sanitizeWorkspace(workspaceToSanitize: IWorkspace): IWorkspace {
return workspaceToSanitize;
}
/**
* Do some side effect when config change, update other services or filesystem
* @param workspace new workspace settings
*/
private async reactWhenWorkspaceChanged(workspace: IWorkspace): Promise<void> {
// TODO: call wiki service, update fileSystemPaths on sub-wiki setting update
// const { id, tagName } = workspaceToSanitize;
// if (this.workspaces[id].isSubWiki && typeof tagName === 'string' && tagName.length > 0 && this.workspaces[id].tagName !== tagName) {
// const subWikiFolderName = path.basename(workspaces[id].name);
// updateSubWikiPluginContent(
// workspaces[id].mainWikiToLink,
// { tagName: options.tagName, subWikiFolderName },
// { tagName: workspaces[id].tagName, subWikiFolderName },
// );
// wikiStartup(workspace);
// }
}
getByName(name: string): IWorkspace | undefined {
return this.getWorkspacesAsList().find((workspace) => workspace.name === name);
}
getPreviousWorkspace = (id: string): IWorkspace | undefined => {
const workspaceList = this.getWorkspacesAsList();
let currentWorkspaceIndex = 0;
for (const [index, workspace] of workspaceList.entries()) {
if (workspace.id === id) {
currentWorkspaceIndex = index;
break;
}
}
if (currentWorkspaceIndex === 0) {
return workspaceList[workspaceList.length - 1];
}
return workspaceList[currentWorkspaceIndex - 1];
};
getNextWorkspace = (id: string): IWorkspace | undefined => {
const workspaceList = this.getWorkspacesAsList();
let currentWorkspaceIndex = 0;
for (const [index, workspace] of workspaceList.entries()) {
if (workspace.id === id) {
currentWorkspaceIndex = index;
break;
}
}
if (currentWorkspaceIndex === workspaceList.length - 1) {
return workspaceList[0];
}
return workspaceList[currentWorkspaceIndex + 1];
};
getActiveWorkspace = (): IWorkspace | undefined => {
return Object.values(this.getWorkspaces()).find((workspace) => workspace.active);
};
async setActiveWorkspace(id: string): Promise<void> {
const currentActiveWorkspace = this.getActiveWorkspace();
if (currentActiveWorkspace !== undefined) {
if (currentActiveWorkspace.id === id) {
return;
}
// de-active the current one
await this.set(currentActiveWorkspace.id, { ...currentActiveWorkspace, active: false });
}
// active new one
await this.set(id, { ...this.workspaces[id], active: true, hibernated: false });
}
/**
*
* @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> {
const workspace = this.get(id);
if (workspace === undefined) {
throw new Error(`Try to setWorkspacePicture() but this workspace is not existed ${id}`);
}
const pictureID = uuid();
if (workspace.picturePath === sourcePicturePath) {
return;
}
const destinationPicturePath = path.join(app.getPath('userData'), 'pictures', `${pictureID}.png`);
// store new picture to fs
let picturePath;
if (isUrl(sourcePicturePath)) {
const temporaryObject = tmp.dirSync();
const temporaryPath = temporaryObject.name;
picturePath = await download(sourcePicturePath, temporaryPath, {
filename: 'e.png',
}).then(() => path.join(temporaryPath, 'e.png'));
} else {
picturePath = sourcePicturePath;
}
const newImage = await Jimp.read(picturePath);
await new Promise((resolve) => {
newImage.clone().resize(128, 128).quality(100).write(destinationPicturePath, resolve);
});
const currentPicturePath = this.get(id)?.picturePath;
await this.update(id, {
picturePath: destinationPicturePath,
});
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (currentPicturePath) {
try {
return await fsExtra.remove(currentPicturePath);
} catch (error) {
console.error(error);
}
}
}
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}`);
}
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (workspace.picturePath) {
await fsExtra.remove(workspace.picturePath);
await this.set(id, {
...workspace,
picturePath: null,
});
}
}
async remove(id: string): Promise<void> {
if (id in this.workspaces) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete this.workspaces[id];
await settings.unset(`workspaces.${this.version}.${id}`);
} else {
throw new Error(`Try to remote workspace, but id ${id} is not existed`);
}
// TODO: call wiki service
// const { name } = workspaces[id];
// stopWiki(name);
// stopWatchWiki(name);
}
async create(newWorkspaceConfig: Omit<IWorkspace, 'active' | 'hibernated' | 'id' | 'order'>): Promise<IWorkspace> {
const newID = uuid();
// find largest order
const workspaceLst = this.getWorkspacesAsList();
let max = 0;
for (const element of workspaceLst) {
if (element.order > max) {
max = element.order;
}
}
const newWorkspace: IWorkspace = {
...newWorkspaceConfig,
active: false,
hibernated: false,
id: newID,
order: max + 1,
};
await this.set(newID, newWorkspace);
return newWorkspace;
}
}