mirror of
https://github.com/laurent22/joplin.git
synced 2025-03-29 21:21:15 +02:00
parent
58bf93a112
commit
6458ad0540
@ -148,6 +148,9 @@ packages/app-desktop/checkForUpdates.js.map
|
|||||||
packages/app-desktop/commands/copyDevCommand.d.ts
|
packages/app-desktop/commands/copyDevCommand.d.ts
|
||||||
packages/app-desktop/commands/copyDevCommand.js
|
packages/app-desktop/commands/copyDevCommand.js
|
||||||
packages/app-desktop/commands/copyDevCommand.js.map
|
packages/app-desktop/commands/copyDevCommand.js.map
|
||||||
|
packages/app-desktop/commands/editProfileConfig.d.ts
|
||||||
|
packages/app-desktop/commands/editProfileConfig.js
|
||||||
|
packages/app-desktop/commands/editProfileConfig.js.map
|
||||||
packages/app-desktop/commands/exportFolders.d.ts
|
packages/app-desktop/commands/exportFolders.d.ts
|
||||||
packages/app-desktop/commands/exportFolders.js
|
packages/app-desktop/commands/exportFolders.js
|
||||||
packages/app-desktop/commands/exportFolders.js.map
|
packages/app-desktop/commands/exportFolders.js.map
|
||||||
@ -175,6 +178,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
|
|||||||
packages/app-desktop/commands/stopExternalEditing.d.ts
|
packages/app-desktop/commands/stopExternalEditing.d.ts
|
||||||
packages/app-desktop/commands/stopExternalEditing.js
|
packages/app-desktop/commands/stopExternalEditing.js
|
||||||
packages/app-desktop/commands/stopExternalEditing.js.map
|
packages/app-desktop/commands/stopExternalEditing.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile.js
|
||||||
|
packages/app-desktop/commands/switchProfile.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile1.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile1.js
|
||||||
|
packages/app-desktop/commands/switchProfile1.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile2.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile2.js
|
||||||
|
packages/app-desktop/commands/switchProfile2.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile3.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile3.js
|
||||||
|
packages/app-desktop/commands/switchProfile3.js.map
|
||||||
packages/app-desktop/commands/toggleExternalEditing.d.ts
|
packages/app-desktop/commands/toggleExternalEditing.d.ts
|
||||||
packages/app-desktop/commands/toggleExternalEditing.js
|
packages/app-desktop/commands/toggleExternalEditing.js
|
||||||
packages/app-desktop/commands/toggleExternalEditing.js.map
|
packages/app-desktop/commands/toggleExternalEditing.js.map
|
||||||
@ -259,6 +274,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
|
|||||||
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
|
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/MainScreen.js
|
packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||||
packages/app-desktop/gui/MainScreen/MainScreen.js.map
|
packages/app-desktop/gui/MainScreen/MainScreen.js.map
|
||||||
|
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
|
||||||
|
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||||
|
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
|
||||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
|
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
|
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
|
||||||
@ -1246,6 +1264,9 @@ packages/lib/services/DecryptionWorker.js.map
|
|||||||
packages/lib/services/ExternalEditWatcher.d.ts
|
packages/lib/services/ExternalEditWatcher.d.ts
|
||||||
packages/lib/services/ExternalEditWatcher.js
|
packages/lib/services/ExternalEditWatcher.js
|
||||||
packages/lib/services/ExternalEditWatcher.js.map
|
packages/lib/services/ExternalEditWatcher.js.map
|
||||||
|
packages/lib/services/ExternalEditWatcher/utils.d.ts
|
||||||
|
packages/lib/services/ExternalEditWatcher/utils.js
|
||||||
|
packages/lib/services/ExternalEditWatcher/utils.js.map
|
||||||
packages/lib/services/ItemChangeUtils.d.ts
|
packages/lib/services/ItemChangeUtils.d.ts
|
||||||
packages/lib/services/ItemChangeUtils.js
|
packages/lib/services/ItemChangeUtils.js
|
||||||
packages/lib/services/ItemChangeUtils.js.map
|
packages/lib/services/ItemChangeUtils.js.map
|
||||||
@ -1570,6 +1591,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
|
|||||||
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
|
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
|
||||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js
|
packages/lib/services/plugins/utils/validatePluginVersion.test.js
|
||||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
|
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
|
||||||
|
packages/lib/services/profileConfig/index.d.ts
|
||||||
|
packages/lib/services/profileConfig/index.js
|
||||||
|
packages/lib/services/profileConfig/index.js.map
|
||||||
|
packages/lib/services/profileConfig/index.test.d.ts
|
||||||
|
packages/lib/services/profileConfig/index.test.js
|
||||||
|
packages/lib/services/profileConfig/index.test.js.map
|
||||||
|
packages/lib/services/profileConfig/initProfile.d.ts
|
||||||
|
packages/lib/services/profileConfig/initProfile.js
|
||||||
|
packages/lib/services/profileConfig/initProfile.js.map
|
||||||
|
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
|
||||||
|
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
|
||||||
|
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
|
||||||
|
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
|
||||||
|
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
|
||||||
|
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
|
||||||
|
packages/lib/services/profileConfig/types.d.ts
|
||||||
|
packages/lib/services/profileConfig/types.js
|
||||||
|
packages/lib/services/profileConfig/types.js.map
|
||||||
packages/lib/services/rest/Api.d.ts
|
packages/lib/services/rest/Api.d.ts
|
||||||
packages/lib/services/rest/Api.js
|
packages/lib/services/rest/Api.js
|
||||||
packages/lib/services/rest/Api.js.map
|
packages/lib/services/rest/Api.js.map
|
||||||
|
39
.gitignore
vendored
39
.gitignore
vendored
@ -138,6 +138,9 @@ packages/app-desktop/checkForUpdates.js.map
|
|||||||
packages/app-desktop/commands/copyDevCommand.d.ts
|
packages/app-desktop/commands/copyDevCommand.d.ts
|
||||||
packages/app-desktop/commands/copyDevCommand.js
|
packages/app-desktop/commands/copyDevCommand.js
|
||||||
packages/app-desktop/commands/copyDevCommand.js.map
|
packages/app-desktop/commands/copyDevCommand.js.map
|
||||||
|
packages/app-desktop/commands/editProfileConfig.d.ts
|
||||||
|
packages/app-desktop/commands/editProfileConfig.js
|
||||||
|
packages/app-desktop/commands/editProfileConfig.js.map
|
||||||
packages/app-desktop/commands/exportFolders.d.ts
|
packages/app-desktop/commands/exportFolders.d.ts
|
||||||
packages/app-desktop/commands/exportFolders.js
|
packages/app-desktop/commands/exportFolders.js
|
||||||
packages/app-desktop/commands/exportFolders.js.map
|
packages/app-desktop/commands/exportFolders.js.map
|
||||||
@ -165,6 +168,18 @@ packages/app-desktop/commands/startExternalEditing.js.map
|
|||||||
packages/app-desktop/commands/stopExternalEditing.d.ts
|
packages/app-desktop/commands/stopExternalEditing.d.ts
|
||||||
packages/app-desktop/commands/stopExternalEditing.js
|
packages/app-desktop/commands/stopExternalEditing.js
|
||||||
packages/app-desktop/commands/stopExternalEditing.js.map
|
packages/app-desktop/commands/stopExternalEditing.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile.js
|
||||||
|
packages/app-desktop/commands/switchProfile.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile1.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile1.js
|
||||||
|
packages/app-desktop/commands/switchProfile1.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile2.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile2.js
|
||||||
|
packages/app-desktop/commands/switchProfile2.js.map
|
||||||
|
packages/app-desktop/commands/switchProfile3.d.ts
|
||||||
|
packages/app-desktop/commands/switchProfile3.js
|
||||||
|
packages/app-desktop/commands/switchProfile3.js.map
|
||||||
packages/app-desktop/commands/toggleExternalEditing.d.ts
|
packages/app-desktop/commands/toggleExternalEditing.d.ts
|
||||||
packages/app-desktop/commands/toggleExternalEditing.js
|
packages/app-desktop/commands/toggleExternalEditing.js
|
||||||
packages/app-desktop/commands/toggleExternalEditing.js.map
|
packages/app-desktop/commands/toggleExternalEditing.js.map
|
||||||
@ -249,6 +264,9 @@ packages/app-desktop/gui/KeymapConfig/utils/useKeymap.js.map
|
|||||||
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
|
packages/app-desktop/gui/MainScreen/MainScreen.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/MainScreen.js
|
packages/app-desktop/gui/MainScreen/MainScreen.js
|
||||||
packages/app-desktop/gui/MainScreen/MainScreen.js.map
|
packages/app-desktop/gui/MainScreen/MainScreen.js.map
|
||||||
|
packages/app-desktop/gui/MainScreen/commands/addProfile.d.ts
|
||||||
|
packages/app-desktop/gui/MainScreen/commands/addProfile.js
|
||||||
|
packages/app-desktop/gui/MainScreen/commands/addProfile.js.map
|
||||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
|
packages/app-desktop/gui/MainScreen/commands/commandPalette.d.ts
|
||||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
packages/app-desktop/gui/MainScreen/commands/commandPalette.js
|
||||||
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
|
packages/app-desktop/gui/MainScreen/commands/commandPalette.js.map
|
||||||
@ -1236,6 +1254,9 @@ packages/lib/services/DecryptionWorker.js.map
|
|||||||
packages/lib/services/ExternalEditWatcher.d.ts
|
packages/lib/services/ExternalEditWatcher.d.ts
|
||||||
packages/lib/services/ExternalEditWatcher.js
|
packages/lib/services/ExternalEditWatcher.js
|
||||||
packages/lib/services/ExternalEditWatcher.js.map
|
packages/lib/services/ExternalEditWatcher.js.map
|
||||||
|
packages/lib/services/ExternalEditWatcher/utils.d.ts
|
||||||
|
packages/lib/services/ExternalEditWatcher/utils.js
|
||||||
|
packages/lib/services/ExternalEditWatcher/utils.js.map
|
||||||
packages/lib/services/ItemChangeUtils.d.ts
|
packages/lib/services/ItemChangeUtils.d.ts
|
||||||
packages/lib/services/ItemChangeUtils.js
|
packages/lib/services/ItemChangeUtils.js
|
||||||
packages/lib/services/ItemChangeUtils.js.map
|
packages/lib/services/ItemChangeUtils.js.map
|
||||||
@ -1560,6 +1581,24 @@ packages/lib/services/plugins/utils/validatePluginVersion.js.map
|
|||||||
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
|
packages/lib/services/plugins/utils/validatePluginVersion.test.d.ts
|
||||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js
|
packages/lib/services/plugins/utils/validatePluginVersion.test.js
|
||||||
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
|
packages/lib/services/plugins/utils/validatePluginVersion.test.js.map
|
||||||
|
packages/lib/services/profileConfig/index.d.ts
|
||||||
|
packages/lib/services/profileConfig/index.js
|
||||||
|
packages/lib/services/profileConfig/index.js.map
|
||||||
|
packages/lib/services/profileConfig/index.test.d.ts
|
||||||
|
packages/lib/services/profileConfig/index.test.js
|
||||||
|
packages/lib/services/profileConfig/index.test.js.map
|
||||||
|
packages/lib/services/profileConfig/initProfile.d.ts
|
||||||
|
packages/lib/services/profileConfig/initProfile.js
|
||||||
|
packages/lib/services/profileConfig/initProfile.js.map
|
||||||
|
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.d.ts
|
||||||
|
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js
|
||||||
|
packages/lib/services/profileConfig/mergeGlobalAndLocalSettings.js.map
|
||||||
|
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.d.ts
|
||||||
|
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js
|
||||||
|
packages/lib/services/profileConfig/splitGlobalAndLocalSettings.js.map
|
||||||
|
packages/lib/services/profileConfig/types.d.ts
|
||||||
|
packages/lib/services/profileConfig/types.js
|
||||||
|
packages/lib/services/profileConfig/types.js.map
|
||||||
packages/lib/services/rest/Api.d.ts
|
packages/lib/services/rest/Api.d.ts
|
||||||
packages/lib/services/rest/Api.js
|
packages/lib/services/rest/Api.js
|
||||||
packages/lib/services/rest/Api.js.map
|
packages/lib/services/rest/Api.js.map
|
||||||
|
19
packages/app-desktop/commands/editProfileConfig.ts
Normal file
19
packages/app-desktop/commands/editProfileConfig.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import Setting from '../../lib/models/Setting';
|
||||||
|
import { openFileWithExternalEditor } from '../../lib/services/ExternalEditWatcher/utils';
|
||||||
|
import bridge from '../services/bridge';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'editProfileConfig',
|
||||||
|
label: () => _('Edit profile configuration...'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: CommandContext) => {
|
||||||
|
await openFileWithExternalEditor(`${Setting.value('rootProfileDir')}/profiles.json`, bridge());
|
||||||
|
},
|
||||||
|
enabledCondition: 'hasMultiProfiles',
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||||
import * as copyDevCommand from './copyDevCommand';
|
import * as copyDevCommand from './copyDevCommand';
|
||||||
|
import * as editProfileConfig from './editProfileConfig';
|
||||||
import * as exportFolders from './exportFolders';
|
import * as exportFolders from './exportFolders';
|
||||||
import * as exportNotes from './exportNotes';
|
import * as exportNotes from './exportNotes';
|
||||||
import * as focusElement from './focusElement';
|
import * as focusElement from './focusElement';
|
||||||
@ -8,11 +9,16 @@ import * as replaceMisspelling from './replaceMisspelling';
|
|||||||
import * as restoreNoteRevision from './restoreNoteRevision';
|
import * as restoreNoteRevision from './restoreNoteRevision';
|
||||||
import * as startExternalEditing from './startExternalEditing';
|
import * as startExternalEditing from './startExternalEditing';
|
||||||
import * as stopExternalEditing from './stopExternalEditing';
|
import * as stopExternalEditing from './stopExternalEditing';
|
||||||
|
import * as switchProfile from './switchProfile';
|
||||||
|
import * as switchProfile1 from './switchProfile1';
|
||||||
|
import * as switchProfile2 from './switchProfile2';
|
||||||
|
import * as switchProfile3 from './switchProfile3';
|
||||||
import * as toggleExternalEditing from './toggleExternalEditing';
|
import * as toggleExternalEditing from './toggleExternalEditing';
|
||||||
import * as toggleSafeMode from './toggleSafeMode';
|
import * as toggleSafeMode from './toggleSafeMode';
|
||||||
|
|
||||||
const index:any[] = [
|
const index:any[] = [
|
||||||
copyDevCommand,
|
copyDevCommand,
|
||||||
|
editProfileConfig,
|
||||||
exportFolders,
|
exportFolders,
|
||||||
exportNotes,
|
exportNotes,
|
||||||
focusElement,
|
focusElement,
|
||||||
@ -21,6 +27,10 @@ const index:any[] = [
|
|||||||
restoreNoteRevision,
|
restoreNoteRevision,
|
||||||
startExternalEditing,
|
startExternalEditing,
|
||||||
stopExternalEditing,
|
stopExternalEditing,
|
||||||
|
switchProfile,
|
||||||
|
switchProfile1,
|
||||||
|
switchProfile2,
|
||||||
|
switchProfile3,
|
||||||
toggleExternalEditing,
|
toggleExternalEditing,
|
||||||
toggleSafeMode,
|
toggleSafeMode,
|
||||||
];
|
];
|
||||||
|
26
packages/app-desktop/commands/switchProfile.ts
Normal file
26
packages/app-desktop/commands/switchProfile.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import { saveProfileConfig } from '@joplin/lib/services/profileConfig';
|
||||||
|
import { ProfileConfig } from '@joplin/lib/services/profileConfig/types';
|
||||||
|
import bridge from '../services/bridge';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'switchProfile',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (context: CommandContext, profileIndex: number) => {
|
||||||
|
const currentConfig = context.state.profileConfig;
|
||||||
|
if (currentConfig.currentProfile === profileIndex) return;
|
||||||
|
|
||||||
|
const newConfig: ProfileConfig = {
|
||||||
|
...currentConfig,
|
||||||
|
currentProfile: profileIndex,
|
||||||
|
};
|
||||||
|
|
||||||
|
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
|
||||||
|
bridge().restart();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
15
packages/app-desktop/commands/switchProfile1.ts
Normal file
15
packages/app-desktop/commands/switchProfile1.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'switchProfile1',
|
||||||
|
label: () => _('Switch to profile %d', 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: CommandContext) => {
|
||||||
|
await CommandService.instance().execute('switchProfile', 0);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
15
packages/app-desktop/commands/switchProfile2.ts
Normal file
15
packages/app-desktop/commands/switchProfile2.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'switchProfile2',
|
||||||
|
label: () => _('Switch to profile %d', 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: CommandContext) => {
|
||||||
|
await CommandService.instance().execute('switchProfile', 1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
15
packages/app-desktop/commands/switchProfile3.ts
Normal file
15
packages/app-desktop/commands/switchProfile3.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import CommandService, { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'switchProfile3',
|
||||||
|
label: () => _('Switch to profile %d', 3),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (_context: CommandContext) => {
|
||||||
|
await CommandService.instance().execute('switchProfile', 2);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
34
packages/app-desktop/gui/MainScreen/commands/addProfile.ts
Normal file
34
packages/app-desktop/gui/MainScreen/commands/addProfile.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import { createNewProfile, saveProfileConfig } from '@joplin/lib/services/profileConfig';
|
||||||
|
import Setting from '@joplin/lib/models/Setting';
|
||||||
|
import bridge from '../../../services/bridge';
|
||||||
|
|
||||||
|
export const declaration: CommandDeclaration = {
|
||||||
|
name: 'addProfile',
|
||||||
|
label: () => _('Create new profile...'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runtime = (comp: any): CommandRuntime => {
|
||||||
|
return {
|
||||||
|
execute: async (context: CommandContext) => {
|
||||||
|
comp.setState({
|
||||||
|
promptOptions: {
|
||||||
|
label: _('Profile name:'),
|
||||||
|
buttons: ['create', 'cancel'],
|
||||||
|
value: '',
|
||||||
|
onClose: async (answer: string) => {
|
||||||
|
if (answer) {
|
||||||
|
const newConfig = await createNewProfile(context.state.profileConfig, answer);
|
||||||
|
newConfig.currentProfile = newConfig.profiles.length - 1;
|
||||||
|
await saveProfileConfig(`${Setting.value('rootProfileDir')}/profiles.json`, newConfig);
|
||||||
|
bridge().restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
comp.setState({ promptOptions: null });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
// AUTO-GENERATED using `gulp buildCommandIndex`
|
// AUTO-GENERATED using `gulp buildCommandIndex`
|
||||||
|
import * as addProfile from './addProfile';
|
||||||
import * as commandPalette from './commandPalette';
|
import * as commandPalette from './commandPalette';
|
||||||
import * as editAlarm from './editAlarm';
|
import * as editAlarm from './editAlarm';
|
||||||
import * as exportPdf from './exportPdf';
|
import * as exportPdf from './exportPdf';
|
||||||
@ -38,6 +39,7 @@ import * as toggleSideBar from './toggleSideBar';
|
|||||||
import * as toggleVisiblePanes from './toggleVisiblePanes';
|
import * as toggleVisiblePanes from './toggleVisiblePanes';
|
||||||
|
|
||||||
const index:any[] = [
|
const index:any[] = [
|
||||||
|
addProfile,
|
||||||
commandPalette,
|
commandPalette,
|
||||||
editAlarm,
|
editAlarm,
|
||||||
exportPdf,
|
exportPdf,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
||||||
import { AppState } from '../app.reducer';
|
import { AppState } from '../app.reducer';
|
||||||
import InteropService from '@joplin/lib/services/interop/InteropService';
|
import InteropService from '@joplin/lib/services/interop/InteropService';
|
||||||
import { stateUtils } from '@joplin/lib/reducer';
|
import { stateUtils } from '@joplin/lib/reducer';
|
||||||
@ -18,9 +18,9 @@ import menuCommandNames from './menuCommandNames';
|
|||||||
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
|
import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext';
|
||||||
import bridge from '../services/bridge';
|
import bridge from '../services/bridge';
|
||||||
import checkForUpdates from '../checkForUpdates';
|
import checkForUpdates from '../checkForUpdates';
|
||||||
|
|
||||||
const { connect } = require('react-redux');
|
const { connect } = require('react-redux');
|
||||||
import { reg } from '@joplin/lib/registry';
|
import { reg } from '@joplin/lib/registry';
|
||||||
|
import { ProfileConfig } from '../../lib/services/profileConfig/types';
|
||||||
const packageInfo = require('../packageInfo.js');
|
const packageInfo = require('../packageInfo.js');
|
||||||
const { clipboard } = require('electron');
|
const { clipboard } = require('electron');
|
||||||
const Menu = bridge().Menu;
|
const Menu = bridge().Menu;
|
||||||
@ -39,7 +39,7 @@ function pluginMenuItemsCommandNames(menuItems: MenuItem[]): string[] {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pluginCommandNames(plugins: PluginStates): string[] {
|
function getPluginCommandNames(plugins: PluginStates): string[] {
|
||||||
let output: string[] = [];
|
let output: string[] = [];
|
||||||
|
|
||||||
for (const view of pluginUtils.viewsByType(plugins, 'menu')) {
|
for (const view of pluginUtils.viewsByType(plugins, 'menu')) {
|
||||||
@ -70,6 +70,42 @@ function createPluginMenuTree(label: string, menuItems: MenuItem[], onMenuItemCl
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useSwitchProfileMenuItems = (profileConfig: ProfileConfig, menuItemDic: any) => {
|
||||||
|
return useMemo(() => {
|
||||||
|
const switchProfileMenuItems: any[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < profileConfig.profiles.length; i++) {
|
||||||
|
const profile = profileConfig.profiles[i];
|
||||||
|
|
||||||
|
let menuItem: any = {};
|
||||||
|
const profileNum = i + 1;
|
||||||
|
|
||||||
|
if (menuItemDic[`switchProfile${profileNum}`]) {
|
||||||
|
menuItem = { ...menuItemDic[`switchProfile${profileNum}`] };
|
||||||
|
} else {
|
||||||
|
menuItem = {
|
||||||
|
label: profile.name,
|
||||||
|
click: () => {
|
||||||
|
void CommandService.instance().execute('switchProfile', i);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItem.label = profile.name;
|
||||||
|
menuItem.type = 'checkbox';
|
||||||
|
menuItem.checked = profileConfig.currentProfile === i;
|
||||||
|
|
||||||
|
switchProfileMenuItems.push(menuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
switchProfileMenuItems.push({ type: 'separator' });
|
||||||
|
switchProfileMenuItems.push(menuItemDic.addProfile);
|
||||||
|
switchProfileMenuItems.push(menuItemDic.editProfileConfig);
|
||||||
|
|
||||||
|
return switchProfileMenuItems;
|
||||||
|
}, [profileConfig, menuItemDic]);
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
menuItemProps: any;
|
menuItemProps: any;
|
||||||
@ -90,6 +126,7 @@ interface Props {
|
|||||||
plugins: PluginStates;
|
plugins: PluginStates;
|
||||||
customCss: string;
|
customCss: string;
|
||||||
locale: string;
|
locale: string;
|
||||||
|
profileConfig: ProfileConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commandNames: string[] = menuCommandNames();
|
const commandNames: string[] = menuCommandNames();
|
||||||
@ -241,6 +278,18 @@ function useMenu(props: Props) {
|
|||||||
const onImportModuleClickRef = useRef(null);
|
const onImportModuleClickRef = useRef(null);
|
||||||
onImportModuleClickRef.current = onImportModuleClick;
|
onImportModuleClickRef.current = onImportModuleClick;
|
||||||
|
|
||||||
|
const pluginCommandNames = useMemo(() => props.pluginMenuItems.map((view: any) => view.commandName), [props.pluginMenuItems]);
|
||||||
|
|
||||||
|
const menuItemDic = useMemo(() => {
|
||||||
|
return menuUtils.commandsToMenuItems(
|
||||||
|
commandNames.concat(pluginCommandNames),
|
||||||
|
(commandName: string) => onMenuItemClickRef.current(commandName),
|
||||||
|
props.locale
|
||||||
|
);
|
||||||
|
}, [commandNames, pluginCommandNames, props.locale]);
|
||||||
|
|
||||||
|
const switchProfileMenuItems: any[] = useSwitchProfileMenuItems(props.profileConfig, menuItemDic);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeoutId: any = null;
|
let timeoutId: any = null;
|
||||||
|
|
||||||
@ -249,13 +298,6 @@ function useMenu(props: Props) {
|
|||||||
|
|
||||||
const keymapService = KeymapService.instance();
|
const keymapService = KeymapService.instance();
|
||||||
|
|
||||||
const pluginCommandNames = props.pluginMenuItems.map((view: any) => view.commandName);
|
|
||||||
const menuItemDic = menuUtils.commandsToMenuItems(
|
|
||||||
commandNames.concat(pluginCommandNames),
|
|
||||||
(commandName: string) => onMenuItemClickRef.current(commandName),
|
|
||||||
props.locale
|
|
||||||
);
|
|
||||||
|
|
||||||
const quitMenuItem = {
|
const quitMenuItem = {
|
||||||
label: _('Quit'),
|
label: _('Quit'),
|
||||||
accelerator: keymapService.getAccelerator('quit'),
|
accelerator: keymapService.getAccelerator('quit'),
|
||||||
@ -385,6 +427,10 @@ function useMenu(props: Props) {
|
|||||||
const newFolderItem = menuItemDic.newFolder;
|
const newFolderItem = menuItemDic.newFolder;
|
||||||
const newSubFolderItem = menuItemDic.newSubFolder;
|
const newSubFolderItem = menuItemDic.newSubFolder;
|
||||||
const printItem = menuItemDic.print;
|
const printItem = menuItemDic.print;
|
||||||
|
const switchProfileItem = {
|
||||||
|
label: _('Switch profile'),
|
||||||
|
submenu: switchProfileMenuItems,
|
||||||
|
};
|
||||||
|
|
||||||
let toolsItems: any[] = [];
|
let toolsItems: any[] = [];
|
||||||
|
|
||||||
@ -499,6 +545,8 @@ function useMenu(props: Props) {
|
|||||||
platforms: ['darwin'],
|
platforms: ['darwin'],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shim.isMac() ? noItem : switchProfileItem,
|
||||||
|
|
||||||
shim.isMac() ? {
|
shim.isMac() ? {
|
||||||
label: _('Hide %s', 'Joplin'),
|
label: _('Hide %s', 'Joplin'),
|
||||||
platforms: ['darwin'],
|
platforms: ['darwin'],
|
||||||
@ -545,6 +593,7 @@ function useMenu(props: Props) {
|
|||||||
type: 'separator',
|
type: 'separator',
|
||||||
},
|
},
|
||||||
printItem,
|
printItem,
|
||||||
|
switchProfileItem,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -848,7 +897,21 @@ function useMenu(props: Props) {
|
|||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
timeoutId = null;
|
timeoutId = null;
|
||||||
};
|
};
|
||||||
}, [props.routeName, props.pluginMenuItems, props.pluginMenus, keymapLastChangeTime, modulesLastChangeTime, props['spellChecker.language'], props['spellChecker.enabled'], props.plugins, props.customCss, props.locale]);
|
}, [
|
||||||
|
props.routeName,
|
||||||
|
props.pluginMenuItems,
|
||||||
|
props.pluginMenus,
|
||||||
|
keymapLastChangeTime,
|
||||||
|
modulesLastChangeTime,
|
||||||
|
props['spellChecker.language'],
|
||||||
|
props['spellChecker.enabled'],
|
||||||
|
props.plugins,
|
||||||
|
props.customCss,
|
||||||
|
props.locale,
|
||||||
|
props.profileConfig,
|
||||||
|
switchProfileMenuItems,
|
||||||
|
menuItemDic,
|
||||||
|
]);
|
||||||
|
|
||||||
useMenuStates(menu, props);
|
useMenuStates(menu, props);
|
||||||
|
|
||||||
@ -889,7 +952,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
const whenClauseContext = stateToWhenClauseContext(state);
|
const whenClauseContext = stateToWhenClauseContext(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(pluginCommandNames(state.pluginService.plugins)), whenClauseContext),
|
menuItemProps: menuUtils.commandsToMenuItemProps(commandNames.concat(getPluginCommandNames(state.pluginService.plugins)), whenClauseContext),
|
||||||
locale: state.settings.locale,
|
locale: state.settings.locale,
|
||||||
routeName: state.route.routeName,
|
routeName: state.route.routeName,
|
||||||
selectedFolderId: state.selectedFolderId,
|
selectedFolderId: state.selectedFolderId,
|
||||||
@ -907,6 +970,7 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
['spellChecker.enabled']: state.settings['spellChecker.enabled'],
|
||||||
plugins: state.pluginService.plugins,
|
plugins: state.pluginService.plugins,
|
||||||
customCss: state.customCss,
|
customCss: state.customCss,
|
||||||
|
profileConfig: state.profileConfig,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -231,6 +231,13 @@ class PromptDialog extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buttonComps = [];
|
const buttonComps = [];
|
||||||
|
if (buttonTypes.indexOf('create') >= 0) {
|
||||||
|
buttonComps.push(
|
||||||
|
<button key="create" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'create')}>
|
||||||
|
{_('Create')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
if (buttonTypes.indexOf('ok') >= 0) {
|
if (buttonTypes.indexOf('ok') >= 0) {
|
||||||
buttonComps.push(
|
buttonComps.push(
|
||||||
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>
|
<button key="ok" disabled={!this.state.answer} style={styles.button} onClick={() => onClose(true, 'ok')}>
|
||||||
|
@ -60,5 +60,10 @@ export default function() {
|
|||||||
'gotoAnything',
|
'gotoAnything',
|
||||||
'commandPalette',
|
'commandPalette',
|
||||||
'openMasterPasswordDialog',
|
'openMasterPasswordDialog',
|
||||||
|
'addProfile',
|
||||||
|
'editProfileConfig',
|
||||||
|
'switchProfile1',
|
||||||
|
'switchProfile2',
|
||||||
|
'switchProfile3',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,8 @@ import SyncTargetNone from './SyncTargetNone';
|
|||||||
import { setRSA } from './services/e2ee/ppk';
|
import { setRSA } from './services/e2ee/ppk';
|
||||||
import RSA from './services/e2ee/RSA.node';
|
import RSA from './services/e2ee/RSA.node';
|
||||||
import Resource from './models/Resource';
|
import Resource from './models/Resource';
|
||||||
|
import { ProfileConfig } from './services/profileConfig/types';
|
||||||
|
import initProfile from './services/profileConfig/initProfile';
|
||||||
|
|
||||||
const appLogger: LoggerWrapper = Logger.create('App');
|
const appLogger: LoggerWrapper = Logger.create('App');
|
||||||
|
|
||||||
@ -70,6 +72,7 @@ export default class BaseApplication {
|
|||||||
private eventEmitter_: any;
|
private eventEmitter_: any;
|
||||||
private scheduleAutoAddResourcesIID_: any = null;
|
private scheduleAutoAddResourcesIID_: any = null;
|
||||||
private database_: any = null;
|
private database_: any = null;
|
||||||
|
private profileConfig_: ProfileConfig = null;
|
||||||
|
|
||||||
protected showStackTraces_: boolean = false;
|
protected showStackTraces_: boolean = false;
|
||||||
protected showPromptString_: boolean = false;
|
protected showPromptString_: boolean = false;
|
||||||
@ -646,6 +649,12 @@ export default class BaseApplication {
|
|||||||
public initRedux() {
|
public initRedux() {
|
||||||
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
|
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn() as any));
|
||||||
setStore(this.store_);
|
setStore(this.store_);
|
||||||
|
|
||||||
|
this.store_.dispatch({
|
||||||
|
type: 'PROFILE_CONFIG_SET',
|
||||||
|
value: this.profileConfig_,
|
||||||
|
});
|
||||||
|
|
||||||
BaseModel.dispatch = this.store().dispatch;
|
BaseModel.dispatch = this.store().dispatch;
|
||||||
FoldersScreenUtils.dispatch = this.store().dispatch;
|
FoldersScreenUtils.dispatch = this.store().dispatch;
|
||||||
// reg.dispatch = this.store().dispatch;
|
// reg.dispatch = this.store().dispatch;
|
||||||
@ -714,14 +723,16 @@ export default class BaseApplication {
|
|||||||
// https://immerjs.github.io/immer/docs/freezing
|
// https://immerjs.github.io/immer/docs/freezing
|
||||||
setAutoFreeze(initArgs.env === 'dev');
|
setAutoFreeze(initArgs.env === 'dev');
|
||||||
|
|
||||||
const profileDir = this.determineProfileDir(initArgs);
|
const rootProfileDir = this.determineProfileDir(initArgs);
|
||||||
|
const { profileDir, profileConfig, isSubProfile } = await initProfile(rootProfileDir);
|
||||||
|
this.profileConfig_ = profileConfig;
|
||||||
|
|
||||||
const resourceDirName = 'resources';
|
const resourceDirName = 'resources';
|
||||||
const resourceDir = `${profileDir}/${resourceDirName}`;
|
const resourceDir = `${profileDir}/${resourceDirName}`;
|
||||||
const tempDir = `${profileDir}/tmp`;
|
const tempDir = `${profileDir}/tmp`;
|
||||||
const cacheDir = `${profileDir}/cache`;
|
const cacheDir = `${profileDir}/cache`;
|
||||||
|
|
||||||
Setting.setConstant('env', initArgs.env);
|
Setting.setConstant('env', initArgs.env);
|
||||||
Setting.setConstant('profileDir', profileDir);
|
|
||||||
Setting.setConstant('resourceDirName', resourceDirName);
|
Setting.setConstant('resourceDirName', resourceDirName);
|
||||||
Setting.setConstant('resourceDir', resourceDir);
|
Setting.setConstant('resourceDir', resourceDir);
|
||||||
Setting.setConstant('tempDir', tempDir);
|
Setting.setConstant('tempDir', tempDir);
|
||||||
@ -778,6 +789,7 @@ export default class BaseApplication {
|
|||||||
|
|
||||||
|
|
||||||
appLogger.info(`Profile directory: ${profileDir}`);
|
appLogger.info(`Profile directory: ${profileDir}`);
|
||||||
|
appLogger.info(`Root profile directory: ${rootProfileDir}`);
|
||||||
|
|
||||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||||
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||||
@ -838,6 +850,7 @@ export default class BaseApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
|
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
|
||||||
|
if (isSubProfile) Setting.setValue('welcome.enabled', false);
|
||||||
|
|
||||||
if (!Setting.value('api.token')) {
|
if (!Setting.value('api.token')) {
|
||||||
void EncryptionService.instance()
|
void EncryptionService.instance()
|
||||||
|
@ -1,12 +1,28 @@
|
|||||||
import Setting, { SettingSectionSource } from '../models/Setting';
|
import Setting, { SettingSectionSource, SettingStorage } from '../models/Setting';
|
||||||
import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, msleep } from '../testing/test-utils';
|
import { setupDatabaseAndSynchronizer, switchClient, expectThrow, expectNotThrow, msleep } from '../testing/test-utils';
|
||||||
import * as fs from 'fs-extra';
|
import { readFile, stat, mkdirp, writeFile, pathExists, readdir } from 'fs-extra';
|
||||||
import Logger from '../Logger';
|
import Logger from '../Logger';
|
||||||
|
import { defaultProfileConfig } from '../services/profileConfig/types';
|
||||||
|
import { createNewProfile, saveProfileConfig } from '../services/profileConfig';
|
||||||
|
import initProfile from '../services/profileConfig/initProfile';
|
||||||
|
|
||||||
async function loadSettingsFromFile(): Promise<any> {
|
async function loadSettingsFromFile(): Promise<any> {
|
||||||
return JSON.parse(await fs.readFile(Setting.settingFilePath, 'utf8'));
|
return JSON.parse(await readFile(Setting.settingFilePath, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const switchToSubProfileSettings = async () => {
|
||||||
|
await Setting.reset();
|
||||||
|
const rootProfileDir = Setting.value('profileDir');
|
||||||
|
const profileConfigPath = `${rootProfileDir}/profiles.json`;
|
||||||
|
let profileConfig = defaultProfileConfig();
|
||||||
|
profileConfig = createNewProfile(profileConfig, 'Sub-profile');
|
||||||
|
profileConfig.currentProfile = 1;
|
||||||
|
await saveProfileConfig(profileConfigPath, profileConfig);
|
||||||
|
const { profileDir } = await initProfile(rootProfileDir);
|
||||||
|
await mkdirp(profileDir);
|
||||||
|
await Setting.load();
|
||||||
|
};
|
||||||
|
|
||||||
describe('models/Setting', function() {
|
describe('models/Setting', function() {
|
||||||
|
|
||||||
beforeEach(async (done) => {
|
beforeEach(async (done) => {
|
||||||
@ -180,19 +196,19 @@ describe('models/Setting', function() {
|
|||||||
{
|
{
|
||||||
// Double-check that timestamp is indeed changed when the content is
|
// Double-check that timestamp is indeed changed when the content is
|
||||||
// changed.
|
// changed.
|
||||||
const beforeStat = await fs.stat(Setting.settingFilePath);
|
const beforeStat = await stat(Setting.settingFilePath);
|
||||||
await msleep(1001);
|
await msleep(1001);
|
||||||
Setting.setValue('sync.mobileWifiOnly', false);
|
Setting.setValue('sync.mobileWifiOnly', false);
|
||||||
await Setting.saveAll();
|
await Setting.saveAll();
|
||||||
const afterStat = await fs.stat(Setting.settingFilePath);
|
const afterStat = await stat(Setting.settingFilePath);
|
||||||
expect(afterStat.mtime.getTime()).toBeGreaterThan(beforeStat.mtime.getTime());
|
expect(afterStat.mtime.getTime()).toBeGreaterThan(beforeStat.mtime.getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const beforeStat = await fs.stat(Setting.settingFilePath);
|
const beforeStat = await stat(Setting.settingFilePath);
|
||||||
await msleep(1001);
|
await msleep(1001);
|
||||||
Setting.setValue('sync.mobileWifiOnly', false);
|
Setting.setValue('sync.mobileWifiOnly', false);
|
||||||
const afterStat = await fs.stat(Setting.settingFilePath);
|
const afterStat = await stat(Setting.settingFilePath);
|
||||||
await Setting.saveAll();
|
await Setting.saveAll();
|
||||||
expect(afterStat.mtime.getTime()).toBe(beforeStat.mtime.getTime());
|
expect(afterStat.mtime.getTime()).toBe(beforeStat.mtime.getTime());
|
||||||
}
|
}
|
||||||
@ -200,7 +216,7 @@ describe('models/Setting', function() {
|
|||||||
|
|
||||||
it('should handle invalid JSON', (async () => {
|
it('should handle invalid JSON', (async () => {
|
||||||
const badContent = '{ oopsIforgotTheQuotes: true}';
|
const badContent = '{ oopsIforgotTheQuotes: true}';
|
||||||
await fs.writeFile(Setting.settingFilePath, badContent, 'utf8');
|
await writeFile(Setting.settingFilePath, badContent, 'utf8');
|
||||||
await Setting.reset();
|
await Setting.reset();
|
||||||
|
|
||||||
Logger.globalLogger.enabled = false;
|
Logger.globalLogger.enabled = false;
|
||||||
@ -208,12 +224,12 @@ describe('models/Setting', function() {
|
|||||||
Logger.globalLogger.enabled = true;
|
Logger.globalLogger.enabled = true;
|
||||||
|
|
||||||
// Invalid JSON file has been moved to .bak file
|
// Invalid JSON file has been moved to .bak file
|
||||||
expect(await fs.pathExists(Setting.settingFilePath)).toBe(false);
|
expect(await pathExists(Setting.settingFilePath)).toBe(false);
|
||||||
|
|
||||||
const files = await fs.readdir(Setting.value('profileDir'));
|
const files = await readdir(Setting.value('profileDir'));
|
||||||
expect(files.length).toBe(1);
|
expect(files.length).toBe(1);
|
||||||
expect(files[0].endsWith('.bak')).toBe(true);
|
expect(files[0].endsWith('.bak')).toBe(true);
|
||||||
expect(await fs.readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
|
expect(await readFile(`${Setting.value('profileDir')}/${files[0]}`, 'utf8')).toBe(badContent);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should allow applying default migrations', (async () => {
|
it('should allow applying default migrations', (async () => {
|
||||||
@ -256,4 +272,67 @@ describe('models/Setting', function() {
|
|||||||
expect(Setting.value('style.editor.contentMaxWidth')).toBe(600); // Changed
|
expect(Setting.value('style.editor.contentMaxWidth')).toBe(600); // Changed
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should load sub-profile settings', async () => {
|
||||||
|
await Setting.reset();
|
||||||
|
|
||||||
|
Setting.setValue('locale', 'fr_FR'); // Global setting
|
||||||
|
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
|
||||||
|
Setting.setValue('sync.target', 9); // Local setting
|
||||||
|
await Setting.saveAll();
|
||||||
|
|
||||||
|
await switchToSubProfileSettings();
|
||||||
|
|
||||||
|
expect(Setting.value('locale')).toBe('fr_FR'); // Should come from the root profile
|
||||||
|
expect(Setting.value('theme')).toBe(Setting.THEME_DARK); // Should come from the root profile
|
||||||
|
expect(Setting.value('sync.target')).toBe(0); // Should come from the local profile
|
||||||
|
|
||||||
|
// Also check that the special loadOne() function works as expected
|
||||||
|
|
||||||
|
expect((await Setting.loadOne('locale')).value).toBe('fr_FR');
|
||||||
|
expect((await Setting.loadOne('theme')).value).toBe(Setting.THEME_DARK);
|
||||||
|
expect((await Setting.loadOne('sync.target')).value).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save sub-profile settings', async () => {
|
||||||
|
await Setting.reset();
|
||||||
|
Setting.setValue('locale', 'fr_FR'); // Global setting
|
||||||
|
Setting.setValue('theme', Setting.THEME_DARK); // Global setting
|
||||||
|
await Setting.saveAll();
|
||||||
|
|
||||||
|
await switchToSubProfileSettings();
|
||||||
|
|
||||||
|
Setting.setValue('locale', 'en_GB'); // Should be saved to global
|
||||||
|
Setting.setValue('sync.target', 8); // Should be saved to local
|
||||||
|
|
||||||
|
await Setting.saveAll();
|
||||||
|
await Setting.reset();
|
||||||
|
await Setting.load();
|
||||||
|
|
||||||
|
expect(Setting.value('locale')).toBe('en_GB');
|
||||||
|
expect(Setting.value('theme')).toBe(Setting.THEME_DARK);
|
||||||
|
expect(Setting.value('sync.target')).toBe(8);
|
||||||
|
|
||||||
|
// Double-check that actual file content is correct
|
||||||
|
|
||||||
|
const globalSettings = JSON.parse(await readFile(`${Setting.value('rootProfileDir')}/settings-1.json`, 'utf8'));
|
||||||
|
const localSettings = JSON.parse(await readFile(`${Setting.value('profileDir')}/settings-1.json`, 'utf8'));
|
||||||
|
|
||||||
|
expect(globalSettings).toEqual({
|
||||||
|
'$schema': 'https://joplinapp.org/schema/settings.json',
|
||||||
|
locale: 'en_GB',
|
||||||
|
theme: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(localSettings).toEqual({
|
||||||
|
'$schema': 'https://joplinapp.org/schema/settings.json',
|
||||||
|
'sync.target': 8,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('all global settings should be saved to file', async () => {
|
||||||
|
for (const [k, v] of Object.entries(Setting.metadata())) {
|
||||||
|
if (v.isGlobal && v.storage !== SettingStorage.File) throw new Error(`Setting "${k}" is global but storage is not "file"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -7,6 +7,8 @@ import SyncTargetRegistry from '../SyncTargetRegistry';
|
|||||||
import time from '../time';
|
import time from '../time';
|
||||||
import FileHandler, { SettingValues } from './settings/FileHandler';
|
import FileHandler, { SettingValues } from './settings/FileHandler';
|
||||||
import Logger from '../Logger';
|
import Logger from '../Logger';
|
||||||
|
import mergeGlobalAndLocalSettings from '../services/profileConfig/mergeGlobalAndLocalSettings';
|
||||||
|
import splitGlobalAndLocalSettings from '../services/profileConfig/splitGlobalAndLocalSettings';
|
||||||
const { sprintf } = require('sprintf-js');
|
const { sprintf } = require('sprintf-js');
|
||||||
const ObjectUtils = require('../ObjectUtils');
|
const ObjectUtils = require('../ObjectUtils');
|
||||||
const { toTitleCase } = require('../string-utils.js');
|
const { toTitleCase } = require('../string-utils.js');
|
||||||
@ -59,6 +61,18 @@ export interface SettingItem {
|
|||||||
autoSave?: boolean;
|
autoSave?: boolean;
|
||||||
storage?: SettingStorage;
|
storage?: SettingStorage;
|
||||||
hideLabel?: boolean;
|
hideLabel?: boolean;
|
||||||
|
|
||||||
|
// In a multi-profile context, all settings are by default local - they take
|
||||||
|
// their value from the current profile. This flag can be set to specify
|
||||||
|
// that the setting is global and that its value should come from the root
|
||||||
|
// profile. This flag only applies to sub-profiles.
|
||||||
|
//
|
||||||
|
// At the moment, all global settings must be saved to file (have the
|
||||||
|
// storage attribute set to "file") because it's simpler to load the root
|
||||||
|
// profile settings.json than load the whole SQLite database. This
|
||||||
|
// restriction is not an issue normally since all settings that are
|
||||||
|
// considered global are also the user-facing ones.
|
||||||
|
isGlobal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingItems {
|
interface SettingItems {
|
||||||
@ -112,6 +126,7 @@ export interface Constants {
|
|||||||
resourceDirName: string;
|
resourceDirName: string;
|
||||||
resourceDir: string;
|
resourceDir: string;
|
||||||
profileDir: string;
|
profileDir: string;
|
||||||
|
rootProfileDir: string;
|
||||||
tempDir: string;
|
tempDir: string;
|
||||||
pluginDataDir: string;
|
pluginDataDir: string;
|
||||||
cacheDir: string;
|
cacheDir: string;
|
||||||
@ -119,6 +134,7 @@ export interface Constants {
|
|||||||
flagOpenDevTools: boolean;
|
flagOpenDevTools: boolean;
|
||||||
syncVersion: number;
|
syncVersion: number;
|
||||||
startupDevPlugins: string[];
|
startupDevPlugins: string[];
|
||||||
|
isSubProfile: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingSections {
|
interface SettingSections {
|
||||||
@ -243,6 +259,7 @@ class Setting extends BaseModel {
|
|||||||
resourceDirName: '',
|
resourceDirName: '',
|
||||||
resourceDir: '',
|
resourceDir: '',
|
||||||
profileDir: '',
|
profileDir: '',
|
||||||
|
rootProfileDir: '',
|
||||||
tempDir: '',
|
tempDir: '',
|
||||||
pluginDataDir: '',
|
pluginDataDir: '',
|
||||||
cacheDir: '',
|
cacheDir: '',
|
||||||
@ -250,6 +267,7 @@ class Setting extends BaseModel {
|
|||||||
flagOpenDevTools: false,
|
flagOpenDevTools: false,
|
||||||
syncVersion: 3,
|
syncVersion: 3,
|
||||||
startupDevPlugins: [],
|
startupDevPlugins: [],
|
||||||
|
isSubProfile: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static autoSaveEnabled = true;
|
public static autoSaveEnabled = true;
|
||||||
@ -264,6 +282,7 @@ class Setting extends BaseModel {
|
|||||||
private static customSections_: SettingSections = {};
|
private static customSections_: SettingSections = {};
|
||||||
private static changedKeys_: string[] = [];
|
private static changedKeys_: string[] = [];
|
||||||
private static fileHandler_: FileHandler = null;
|
private static fileHandler_: FileHandler = null;
|
||||||
|
private static rootFileHandler_: FileHandler = null;
|
||||||
private static settingFilename_: string = 'settings.json';
|
private static settingFilename_: string = 'settings.json';
|
||||||
|
|
||||||
static tableName() {
|
static tableName() {
|
||||||
@ -291,6 +310,10 @@ class Setting extends BaseModel {
|
|||||||
return `${this.value('profileDir')}/${this.settingFilename_}`;
|
return `${this.value('profileDir')}/${this.settingFilename_}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static get rootSettingFilePath(): string {
|
||||||
|
return `${this.value('rootProfileDir')}/${this.settingFilename_}`;
|
||||||
|
}
|
||||||
|
|
||||||
public static get settingFilename(): string {
|
public static get settingFilename(): string {
|
||||||
return this.settingFilename_;
|
return this.settingFilename_;
|
||||||
}
|
}
|
||||||
@ -306,6 +329,13 @@ class Setting extends BaseModel {
|
|||||||
return this.fileHandler_;
|
return this.fileHandler_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static get rootFileHandler(): FileHandler {
|
||||||
|
if (!this.rootFileHandler_) {
|
||||||
|
this.rootFileHandler_ = new FileHandler(this.rootSettingFilePath);
|
||||||
|
}
|
||||||
|
return this.rootFileHandler_;
|
||||||
|
}
|
||||||
|
|
||||||
static keychainService() {
|
static keychainService() {
|
||||||
if (!this.keychainService_) throw new Error('keychainService has not been set!!');
|
if (!this.keychainService_) throw new Error('keychainService has not been set!!');
|
||||||
return this.keychainService_;
|
return this.keychainService_;
|
||||||
@ -359,6 +389,7 @@ class Setting extends BaseModel {
|
|||||||
public: false,
|
public: false,
|
||||||
appTypes: [AppType.Desktop],
|
appTypes: [AppType.Desktop],
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'sync.openSyncWizard': {
|
'sync.openSyncWizard': {
|
||||||
@ -669,6 +700,7 @@ class Setting extends BaseModel {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'sync.3.auth': { value: '', type: SettingItemType.String, public: false },
|
'sync.3.auth': { value: '', type: SettingItemType.String, public: false },
|
||||||
@ -687,7 +719,7 @@ class Setting extends BaseModel {
|
|||||||
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
|
'sync.9.context': { value: '', type: SettingItemType.String, public: false },
|
||||||
'sync.10.context': { value: '', type: SettingItemType.String, public: false },
|
'sync.10.context': { value: '', type: SettingItemType.String, public: false },
|
||||||
|
|
||||||
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
'sync.maxConcurrentConnections': { value: 5, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||||
|
|
||||||
// The active folder ID is guaranteed to be valid as long as there's at least one
|
// The active folder ID is guaranteed to be valid as long as there's at least one
|
||||||
// existing folder, so it is a good default in contexts where there's no currently
|
// existing folder, so it is a good default in contexts where there's no currently
|
||||||
@ -695,7 +727,7 @@ class Setting extends BaseModel {
|
|||||||
// to the last folder that was selected.
|
// to the last folder that was selected.
|
||||||
activeFolderId: { value: '', type: SettingItemType.String, public: false },
|
activeFolderId: { value: '', type: SettingItemType.String, public: false },
|
||||||
|
|
||||||
richTextBannerDismissed: { value: false, type: SettingItemType.Bool, public: false },
|
richTextBannerDismissed: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },
|
||||||
|
|
||||||
firstStart: { value: true, type: SettingItemType.Bool, public: false },
|
firstStart: { value: true, type: SettingItemType.Bool, public: false },
|
||||||
locale: {
|
locale: {
|
||||||
@ -708,6 +740,7 @@ class Setting extends BaseModel {
|
|||||||
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
|
return ObjectUtils.sortByValue(supportedLocalesToLanguages({ includeStats: true }));
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
dateFormat: {
|
dateFormat: {
|
||||||
value: Setting.DATE_FORMAT_1,
|
value: Setting.DATE_FORMAT_1,
|
||||||
@ -730,6 +763,7 @@ class Setting extends BaseModel {
|
|||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
timeFormat: {
|
timeFormat: {
|
||||||
value: Setting.TIME_FORMAT_1,
|
value: Setting.TIME_FORMAT_1,
|
||||||
@ -746,6 +780,7 @@ class Setting extends BaseModel {
|
|||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
@ -761,6 +796,7 @@ class Setting extends BaseModel {
|
|||||||
section: 'appearance',
|
section: 'appearance',
|
||||||
options: () => themeOptions(),
|
options: () => themeOptions(),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
themeAutoDetect: {
|
themeAutoDetect: {
|
||||||
@ -771,6 +807,7 @@ class Setting extends BaseModel {
|
|||||||
public: true,
|
public: true,
|
||||||
label: () => _('Automatically switch theme to match system theme'),
|
label: () => _('Automatically switch theme to match system theme'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
preferredLightTheme: {
|
preferredLightTheme: {
|
||||||
@ -786,6 +823,7 @@ class Setting extends BaseModel {
|
|||||||
section: 'appearance',
|
section: 'appearance',
|
||||||
options: () => themeOptions(),
|
options: () => themeOptions(),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
preferredDarkTheme: {
|
preferredDarkTheme: {
|
||||||
@ -801,6 +839,7 @@ class Setting extends BaseModel {
|
|||||||
section: 'appearance',
|
section: 'appearance',
|
||||||
options: () => themeOptions(),
|
options: () => themeOptions(),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
notificationPermission: {
|
notificationPermission: {
|
||||||
@ -809,7 +848,7 @@ class Setting extends BaseModel {
|
|||||||
public: false,
|
public: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false, advanced: true, appTypes: [AppType.Desktop], label: () => _('Show note counts') },
|
showNoteCounts: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false, advanced: true, appTypes: [AppType.Desktop], label: () => _('Show note counts') },
|
||||||
|
|
||||||
layoutButtonSequence: {
|
layoutButtonSequence: {
|
||||||
value: Setting.LAYOUT_ALL,
|
value: Setting.LAYOUT_ALL,
|
||||||
@ -824,9 +863,10 @@ class Setting extends BaseModel {
|
|||||||
[Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')),
|
[Setting.LAYOUT_VIEWER_SPLIT]: _('%s / %s', _('Viewer'), _('Split View')),
|
||||||
}),
|
}),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') },
|
uncompletedTodosOnTop: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Uncompleted to-dos on top') },
|
||||||
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') },
|
showCompletedTodos: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, appTypes: [AppType.Cli], label: () => _('Show completed to-dos') },
|
||||||
'notes.sortOrder.field': {
|
'notes.sortOrder.field': {
|
||||||
value: 'user_updated_time',
|
value: 'user_updated_time',
|
||||||
type: SettingItemType.String,
|
type: SettingItemType.String,
|
||||||
@ -845,6 +885,7 @@ class Setting extends BaseModel {
|
|||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'editor.autoMatchingBraces': {
|
'editor.autoMatchingBraces': {
|
||||||
value: true,
|
value: true,
|
||||||
@ -854,8 +895,9 @@ class Setting extends BaseModel {
|
|||||||
appTypes: [AppType.Desktop],
|
appTypes: [AppType.Desktop],
|
||||||
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
'notes.sortOrder.reverse': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
||||||
// NOTE: A setting whose name starts with 'notes.sortOrder' is special,
|
// NOTE: A setting whose name starts with 'notes.sortOrder' is special,
|
||||||
// which implies changing the setting automatically triggers the reflesh of notes.
|
// which implies changing the setting automatically triggers the reflesh of notes.
|
||||||
// See lib/BaseApplication.ts/generalMiddleware() for details.
|
// See lib/BaseApplication.ts/generalMiddleware() for details.
|
||||||
@ -868,6 +910,7 @@ class Setting extends BaseModel {
|
|||||||
label: () => _('Show sort order buttons'),
|
label: () => _('Show sort order buttons'),
|
||||||
// description: () => _('If true, sort order buttons (field + reverse) for notes are shown at the top of Note List.'),
|
// description: () => _('If true, sort order buttons (field + reverse) for notes are shown at the top of Note List.'),
|
||||||
appTypes: [AppType.Desktop],
|
appTypes: [AppType.Desktop],
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'notes.perFieldReversalEnabled': {
|
'notes.perFieldReversalEnabled': {
|
||||||
value: true,
|
value: true,
|
||||||
@ -931,8 +974,8 @@ class Setting extends BaseModel {
|
|||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
},
|
},
|
||||||
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
'folders.sortOrder.reverse': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Reverse sort order'), appTypes: [AppType.Cli] },
|
||||||
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, public: true, label: () => _('Save geo-location with notes') },
|
trackLocation: { value: true, type: SettingItemType.Bool, section: 'note', storage: SettingStorage.File, isGlobal: true, public: true, label: () => _('Save geo-location with notes') },
|
||||||
|
|
||||||
// 2020-10-29: For now disable the beta editor due to
|
// 2020-10-29: For now disable the beta editor due to
|
||||||
// underlying bugs in the TextInput component which we cannot
|
// underlying bugs in the TextInput component which we cannot
|
||||||
@ -947,6 +990,8 @@ class Setting extends BaseModel {
|
|||||||
appTypes: [AppType.Mobile],
|
appTypes: [AppType.Mobile],
|
||||||
label: () => 'Opt-in to the editor beta',
|
label: () => 'Opt-in to the editor beta',
|
||||||
description: () => 'This beta adds list continuation and syntax highlighting. If you find bugs, please report them in the Discourse forum.',
|
description: () => 'This beta adds list continuation and syntax highlighting. If you find bugs, please report them in the Discourse forum.',
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
newTodoFocus: {
|
newTodoFocus: {
|
||||||
@ -964,6 +1009,7 @@ class Setting extends BaseModel {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
newNoteFocus: {
|
newNoteFocus: {
|
||||||
value: 'body',
|
value: 'body',
|
||||||
@ -980,6 +1026,7 @@ class Setting extends BaseModel {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'plugins.states': {
|
'plugins.states': {
|
||||||
@ -1005,31 +1052,31 @@ class Setting extends BaseModel {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Deprecated - use markdown.plugin.*
|
// Deprecated - use markdown.plugin.*
|
||||||
'markdown.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
'markdown.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||||
'markdown.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
'markdown.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, public: false, appTypes: [AppType.Mobile, AppType.Desktop] },
|
||||||
// Deprecated
|
// Deprecated
|
||||||
|
|
||||||
'markdown.plugin.softbreaks': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
|
'markdown.plugin.softbreaks': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable soft breaks')}${wysiwygYes}` },
|
||||||
'markdown.plugin.typographer': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
|
'markdown.plugin.typographer': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable typographer support')}${wysiwygYes}` },
|
||||||
'markdown.plugin.linkify': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
|
'markdown.plugin.linkify': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Linkify')}${wysiwygYes}` },
|
||||||
|
|
||||||
'markdown.plugin.katex': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
|
'markdown.plugin.katex': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable math expressions')}${wysiwygYes}` },
|
||||||
'markdown.plugin.fountain': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
|
'markdown.plugin.fountain': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Fountain syntax support')}${wysiwygYes}` },
|
||||||
'markdown.plugin.mermaid': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
|
'markdown.plugin.mermaid': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable Mermaid diagrams support')}${wysiwygYes}` },
|
||||||
|
|
||||||
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` },
|
'markdown.plugin.audioPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable audio player')}${wysiwygNo}` },
|
||||||
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` },
|
'markdown.plugin.videoPlayer': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable video player')}${wysiwygNo}` },
|
||||||
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
|
'markdown.plugin.pdfViewer': { storage: SettingStorage.File, isGlobal: true, value: !mobilePlatform, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Desktop], label: () => `${_('Enable PDF viewer')}${wysiwygNo}` },
|
||||||
'markdown.plugin.mark': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` },
|
'markdown.plugin.mark': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` },
|
||||||
'markdown.plugin.footnote': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
|
'markdown.plugin.footnote': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable footnotes')}${wysiwygNo}` },
|
||||||
'markdown.plugin.toc': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
|
'markdown.plugin.toc': { storage: SettingStorage.File, isGlobal: true, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` },
|
||||||
'markdown.plugin.sub': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` },
|
'markdown.plugin.sub': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` },
|
||||||
'markdown.plugin.sup': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` },
|
'markdown.plugin.sup': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` },
|
||||||
'markdown.plugin.deflist': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
|
'markdown.plugin.deflist': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` },
|
||||||
'markdown.plugin.abbr': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
|
'markdown.plugin.abbr': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` },
|
||||||
'markdown.plugin.emoji': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
|
'markdown.plugin.emoji': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` },
|
||||||
'markdown.plugin.insert': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
|
'markdown.plugin.insert': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` },
|
||||||
'markdown.plugin.multitable': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
'markdown.plugin.multitable': { storage: SettingStorage.File, isGlobal: true, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: [AppType.Mobile, AppType.Desktop], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` },
|
||||||
|
|
||||||
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
// Tray icon (called AppIndicator) doesn't work in Ubuntu
|
||||||
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
// http://www.webupd8.org/2017/04/fix-appindicator-not-working-for.html
|
||||||
@ -1046,9 +1093,10 @@ class Setting extends BaseModel {
|
|||||||
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : _('This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.');
|
return platform === 'linux' ? _('Note: Does not work in all desktop environments.') : _('This will allow Joplin to run in the background. It is recommended to enable this setting so that your notes are constantly being synchronised, thus reducing the number of conflicts.');
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') },
|
startMinimized: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: true, appTypes: [AppType.Desktop], label: () => _('Start application minimised in the tray icon') },
|
||||||
|
|
||||||
collapsedFolderIds: { value: [], type: SettingItemType.Array, public: false },
|
collapsedFolderIds: { value: [], type: SettingItemType.Array, public: false },
|
||||||
|
|
||||||
@ -1072,9 +1120,9 @@ class Setting extends BaseModel {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Deprecated in favour of windowContentZoomFactor
|
// Deprecated in favour of windowContentZoomFactor
|
||||||
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
|
'style.zoom': { value: 100, type: SettingItemType.Int, public: false, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => '', minimum: 50, maximum: 500, step: 10 },
|
||||||
|
|
||||||
'style.editor.fontSize': { value: 15, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
|
'style.editor.fontSize': { value: 15, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor font size'), minimum: 4, maximum: 50, step: 1 },
|
||||||
'style.editor.fontFamily':
|
'style.editor.fontFamily':
|
||||||
(mobilePlatform) ?
|
(mobilePlatform) ?
|
||||||
({
|
({
|
||||||
@ -1101,6 +1149,7 @@ class Setting extends BaseModel {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
}) : {
|
}) : {
|
||||||
value: '',
|
value: '',
|
||||||
type: SettingItemType.String,
|
type: SettingItemType.String,
|
||||||
@ -1111,6 +1160,7 @@ class Setting extends BaseModel {
|
|||||||
description: () =>
|
description: () =>
|
||||||
_('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'),
|
_('Used for most text in the markdown editor. If not found, a generic proportional (variable width) font is used.'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'style.editor.monospaceFontFamily': {
|
'style.editor.monospaceFontFamily': {
|
||||||
value: '',
|
value: '',
|
||||||
@ -1122,9 +1172,10 @@ class Setting extends BaseModel {
|
|||||||
description: () =>
|
description: () =>
|
||||||
_('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'),
|
_('Used where a fixed width font is needed to lay out text legibly (e.g. tables, checkboxes, code). If not found, a generic monospace (fixed width) font is used.'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
'style.editor.contentMaxWidth': { value: 0, type: SettingItemType.Int, public: true, storage: SettingStorage.File, isGlobal: true,appTypes: [AppType.Desktop], section: 'appearance', label: () => _('Editor maximum width'), description: () => _('Set it to 0 to make it take the complete available space. Recommended width is 600.') },
|
||||||
|
|
||||||
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
|
'ui.layout': { value: {}, type: SettingItemType.Object, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
|
||||||
|
|
||||||
@ -1149,6 +1200,8 @@ class Setting extends BaseModel {
|
|||||||
label: () => _('Custom stylesheet for rendered Markdown'),
|
label: () => _('Custom stylesheet for rendered Markdown'),
|
||||||
section: 'appearance',
|
section: 'appearance',
|
||||||
advanced: true,
|
advanced: true,
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'style.customCss.joplinApp': {
|
'style.customCss.joplinApp': {
|
||||||
value: null,
|
value: null,
|
||||||
@ -1167,6 +1220,8 @@ class Setting extends BaseModel {
|
|||||||
section: 'appearance',
|
section: 'appearance',
|
||||||
advanced: true,
|
advanced: true,
|
||||||
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
|
description: () => 'CSS file support is provided for your convenience, but they are advanced settings, and styles you define may break from one version to the next. If you want to use them, please know that it might require regular development work from you to keep them working. The Joplin team cannot make a commitment to keep the application HTML structure stable.',
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'sync.clearLocalSyncStateButton': {
|
'sync.clearLocalSyncStateButton': {
|
||||||
@ -1192,9 +1247,9 @@ class Setting extends BaseModel {
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, section: 'application', public: platform !== 'linux', appTypes: [AppType.Desktop], label: () => _('Automatically check for updates') },
|
||||||
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Desktop], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') },
|
||||||
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, isGlobal: true, public: false },
|
||||||
'sync.interval': {
|
'sync.interval': {
|
||||||
value: 300,
|
value: 300,
|
||||||
type: SettingItemType.Int,
|
type: SettingItemType.Int,
|
||||||
@ -1214,6 +1269,7 @@ class Setting extends BaseModel {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'sync.mobileWifiOnly': {
|
'sync.mobileWifiOnly': {
|
||||||
value: false,
|
value: false,
|
||||||
@ -1223,12 +1279,13 @@ class Setting extends BaseModel {
|
|||||||
label: () => _('Synchronise only over WiFi connection'),
|
label: () => _('Synchronise only over WiFi connection'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
appTypes: [AppType.Mobile],
|
appTypes: [AppType.Mobile],
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, public: false, appTypes: [AppType.Desktop] },
|
noteVisiblePanes: { value: ['editor', 'viewer'], type: SettingItemType.Array, storage: SettingStorage.File, isGlobal: true, public: false, appTypes: [AppType.Desktop] },
|
||||||
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
|
tagHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
|
||||||
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
|
folderHeaderIsExpanded: { value: true, type: SettingItemType.Bool, public: false, appTypes: [AppType.Desktop] },
|
||||||
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
editor: { value: '', type: SettingItemType.String, subType: 'file_path_and_args', storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli, AppType.Desktop], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') },
|
||||||
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
|
'export.pdfPageSize': { value: 'A4', type: SettingItemType.String, advanced: true, storage: SettingStorage.File, isGlobal: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page size for PDF export'), options: () => {
|
||||||
return {
|
return {
|
||||||
'A4': _('A4'),
|
'A4': _('A4'),
|
||||||
'Letter': _('Letter'),
|
'Letter': _('Letter'),
|
||||||
@ -1238,7 +1295,7 @@ class Setting extends BaseModel {
|
|||||||
'Legal': _('Legal'),
|
'Legal': _('Legal'),
|
||||||
};
|
};
|
||||||
} },
|
} },
|
||||||
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => {
|
'export.pdfPageOrientation': { value: 'portrait', type: SettingItemType.String, storage: SettingStorage.File, isGlobal: true, advanced: true, isEnum: true, public: true, appTypes: [AppType.Desktop], label: () => _('Page orientation for PDF export'), options: () => {
|
||||||
return {
|
return {
|
||||||
'portrait': _('Portrait'),
|
'portrait': _('Portrait'),
|
||||||
'landscape': _('Landscape'),
|
'landscape': _('Landscape'),
|
||||||
@ -1261,6 +1318,7 @@ class Setting extends BaseModel {
|
|||||||
return output;
|
return output;
|
||||||
},
|
},
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'editor.spellcheckBeta': {
|
'editor.spellcheckBeta': {
|
||||||
@ -1270,6 +1328,8 @@ class Setting extends BaseModel {
|
|||||||
appTypes: [AppType.Desktop],
|
appTypes: [AppType.Desktop],
|
||||||
label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)',
|
label: () => 'Enable spell checking in Markdown editor? (WARNING BETA feature)',
|
||||||
description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)',
|
description: () => 'Spell checker in the Markdown editor was previously unstable (cursor location was not stable, sometimes edits would not be saved or reflected in the viewer, etc.) however it appears to be more reliable now. If you notice any issue, please report it on GitHub or the Joplin Forum (Help -> Joplin Forum)',
|
||||||
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'net.customCertificates': {
|
'net.customCertificates': {
|
||||||
@ -1324,8 +1384,8 @@ class Setting extends BaseModel {
|
|||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
},
|
},
|
||||||
|
|
||||||
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File },
|
'api.token': { value: null, type: SettingItemType.String, public: false, storage: SettingStorage.File, isGlobal: true },
|
||||||
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, public: true, appTypes: [AppType.Cli], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
'api.port': { value: null, type: SettingItemType.Int, storage: SettingStorage.File, isGlobal: true, public: true, appTypes: [AppType.Cli], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||||
|
|
||||||
'resourceService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
'resourceService.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
||||||
'searchEngine.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
'searchEngine.lastProcessedChangeId': { value: 0, type: SettingItemType.Int, public: false },
|
||||||
@ -1357,8 +1417,8 @@ class Setting extends BaseModel {
|
|||||||
'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] },
|
'camera.type': { value: 0, type: SettingItemType.Int, public: false, appTypes: [AppType.Mobile] },
|
||||||
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] },
|
'camera.ratio': { value: '4:3', type: SettingItemType.String, public: false, appTypes: [AppType.Mobile] },
|
||||||
|
|
||||||
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, public: false },
|
'spellChecker.enabled': { value: true, type: SettingItemType.Bool, isGlobal: true, storage: SettingStorage.File, public: false },
|
||||||
'spellChecker.language': { value: '', type: SettingItemType.String, storage: SettingStorage.File, public: false },
|
'spellChecker.language': { value: '', type: SettingItemType.String, isGlobal: true, storage: SettingStorage.File, public: false },
|
||||||
|
|
||||||
windowContentZoomFactor: {
|
windowContentZoomFactor: {
|
||||||
value: 100,
|
value: 100,
|
||||||
@ -1369,6 +1429,7 @@ class Setting extends BaseModel {
|
|||||||
maximum: 300,
|
maximum: 300,
|
||||||
step: 10,
|
step: 10,
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'layout.folderList.factor': {
|
'layout.folderList.factor': {
|
||||||
@ -1384,6 +1445,7 @@ class Setting extends BaseModel {
|
|||||||
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
||||||
'Restart app to see changes.'),
|
'Restart app to see changes.'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'layout.noteList.factor': {
|
'layout.noteList.factor': {
|
||||||
value: 1,
|
value: 1,
|
||||||
@ -1398,6 +1460,7 @@ class Setting extends BaseModel {
|
|||||||
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
||||||
'Restart app to see changes.'),
|
'Restart app to see changes.'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
'layout.note.factor': {
|
'layout.note.factor': {
|
||||||
value: 2,
|
value: 2,
|
||||||
@ -1412,6 +1475,7 @@ class Setting extends BaseModel {
|
|||||||
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
'Thus an item with a factor of 2 will take twice as much space as an item with a factor of 1.' +
|
||||||
'Restart app to see changes.'),
|
'Restart app to see changes.'),
|
||||||
storage: SettingStorage.File,
|
storage: SettingStorage.File,
|
||||||
|
isGlobal: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
'syncInfoCache': {
|
'syncInfoCache': {
|
||||||
@ -1452,9 +1516,17 @@ class Setting extends BaseModel {
|
|||||||
|
|
||||||
this.metadata_ = Object.assign(this.metadata_, this.customMetadata_);
|
this.metadata_ = Object.assign(this.metadata_, this.customMetadata_);
|
||||||
|
|
||||||
|
if (this.value('env') === Env.Dev) this.validateMetadata(this.metadata_);
|
||||||
|
|
||||||
return this.metadata_;
|
return this.metadata_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static validateMetadata(md: SettingItems) {
|
||||||
|
for (const [k, v] of Object.entries(md)) {
|
||||||
|
if (v.isGlobal && v.storage !== SettingStorage.File) throw new Error(`Setting "${k}" is global but storage is not "file"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static skipDefaultMigrations() {
|
public static skipDefaultMigrations() {
|
||||||
logger.info('Skipping all default migrations...');
|
logger.info('Skipping all default migrations...');
|
||||||
|
|
||||||
@ -1594,10 +1666,17 @@ class Setting extends BaseModel {
|
|||||||
// Low-level method to load a setting directly from the database. Should not be used in most cases.
|
// Low-level method to load a setting directly from the database. Should not be used in most cases.
|
||||||
public static async loadOne(key: string): Promise<CacheItem | null> {
|
public static async loadOne(key: string): Promise<CacheItem | null> {
|
||||||
if (this.keyStorage(key) === SettingStorage.File) {
|
if (this.keyStorage(key) === SettingStorage.File) {
|
||||||
const fromFile = await this.fileHandler.load();
|
let fileSettings = await this.fileHandler.load();
|
||||||
|
|
||||||
|
const md = this.settingMetadata(key);
|
||||||
|
if (md.isGlobal) {
|
||||||
|
const rootFileSettings = await this.rootFileHandler.load();
|
||||||
|
fileSettings = mergeGlobalAndLocalSettings(rootFileSettings, fileSettings);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
value: fromFile[key],
|
value: fileSettings[key],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1664,11 +1743,17 @@ class Setting extends BaseModel {
|
|||||||
const itemsFromFile: CacheItem[] = [];
|
const itemsFromFile: CacheItem[] = [];
|
||||||
|
|
||||||
if (this.canUseFileStorage()) {
|
if (this.canUseFileStorage()) {
|
||||||
const fromFile = await this.fileHandler.load();
|
let fileSettings = await this.fileHandler.load();
|
||||||
for (const k of Object.keys(fromFile)) {
|
|
||||||
|
if (this.value('isSubProfile')) {
|
||||||
|
const rootFileSettings = await this.rootFileHandler.load();
|
||||||
|
fileSettings = mergeGlobalAndLocalSettings(rootFileSettings, fileSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k of Object.keys(fileSettings)) {
|
||||||
itemsFromFile.push({
|
itemsFromFile.push({
|
||||||
key: k,
|
key: k,
|
||||||
value: fromFile[k],
|
value: fileSettings[k],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2018,7 +2103,15 @@ class Setting extends BaseModel {
|
|||||||
|
|
||||||
await BaseModel.db().transactionExecBatch(queries);
|
await BaseModel.db().transactionExecBatch(queries);
|
||||||
|
|
||||||
if (this.canUseFileStorage()) await this.fileHandler.save(valuesForFile);
|
if (this.canUseFileStorage()) {
|
||||||
|
if (this.value('isSubProfile')) {
|
||||||
|
const { globalSettings, localSettings } = splitGlobalAndLocalSettings(valuesForFile);
|
||||||
|
await this.rootFileHandler.save(globalSettings);
|
||||||
|
await this.fileHandler.save(localSettings);
|
||||||
|
} else {
|
||||||
|
await this.fileHandler.save(valuesForFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug('Settings have been saved.');
|
logger.debug('Settings have been saved.');
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"@joplin/renderer": "~2.7",
|
"@joplin/renderer": "~2.7",
|
||||||
"@joplin/turndown": "^4.0.61",
|
"@joplin/turndown": "^4.0.61",
|
||||||
"@joplin/turndown-plugin-gfm": "^1.0.43",
|
"@joplin/turndown-plugin-gfm": "^1.0.43",
|
||||||
|
"@types/nanoid": "^3.0.0",
|
||||||
"async-mutex": "^0.1.3",
|
"async-mutex": "^0.1.3",
|
||||||
"base-64": "^0.1.0",
|
"base-64": "^0.1.0",
|
||||||
"base64-stream": "^1.0.0",
|
"base64-stream": "^1.0.0",
|
||||||
|
@ -5,6 +5,7 @@ import Note from './models/Note';
|
|||||||
import Folder from './models/Folder';
|
import Folder from './models/Folder';
|
||||||
import BaseModel from './BaseModel';
|
import BaseModel from './BaseModel';
|
||||||
import { Store } from 'redux';
|
import { Store } from 'redux';
|
||||||
|
import { ProfileConfig } from './services/profileConfig/types';
|
||||||
const ArrayUtils = require('./ArrayUtils.js');
|
const ArrayUtils = require('./ArrayUtils.js');
|
||||||
const { ALL_NOTES_FILTER_ID } = require('./reserved-ids');
|
const { ALL_NOTES_FILTER_ID } = require('./reserved-ids');
|
||||||
const { createSelectorCreator, defaultMemoize } = require('reselect');
|
const { createSelectorCreator, defaultMemoize } = require('reselect');
|
||||||
@ -92,6 +93,7 @@ export interface State {
|
|||||||
isInsertingNotes: boolean;
|
isInsertingNotes: boolean;
|
||||||
hasEncryptedItems: boolean;
|
hasEncryptedItems: boolean;
|
||||||
needApiAuth: boolean;
|
needApiAuth: boolean;
|
||||||
|
profileConfig: ProfileConfig;
|
||||||
|
|
||||||
// Extra reducer keys go here:
|
// Extra reducer keys go here:
|
||||||
pluginService: PluginServiceState;
|
pluginService: PluginServiceState;
|
||||||
@ -162,6 +164,7 @@ export const defaultState: State = {
|
|||||||
isInsertingNotes: false,
|
isInsertingNotes: false,
|
||||||
hasEncryptedItems: false,
|
hasEncryptedItems: false,
|
||||||
needApiAuth: false,
|
needApiAuth: false,
|
||||||
|
profileConfig: null,
|
||||||
|
|
||||||
pluginService: pluginServiceDefaultState,
|
pluginService: pluginServiceDefaultState,
|
||||||
shareService: shareServiceDefaultState,
|
shareService: shareServiceDefaultState,
|
||||||
@ -1138,6 +1141,10 @@ const reducer = produce((draft: Draft<State> = defaultState, action: any) => {
|
|||||||
draft.needApiAuth = action.value;
|
draft.needApiAuth = action.value;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'PROFILE_CONFIG_SET':
|
||||||
|
draft.profileConfig = action.value;
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
error.message = `In reducer: ${error.message} Action: ${JSON.stringify(action)}`;
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import Logger from '../Logger';
|
import Logger from '../Logger';
|
||||||
import Setting from '../models/Setting';
|
import Setting from '../models/Setting';
|
||||||
import shim from '../shim';
|
import shim from '../shim';
|
||||||
import { fileExtension, basename, toSystemSlashes } from '../path-utils';
|
import { basename, toSystemSlashes } from '../path-utils';
|
||||||
import time from '../time';
|
import time from '../time';
|
||||||
import { NoteEntity } from './database/types';
|
import { NoteEntity } from './database/types';
|
||||||
|
|
||||||
import Note from '../models/Note';
|
import Note from '../models/Note';
|
||||||
|
import { openFileWithExternalEditor } from './ExternalEditWatcher/utils';
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
const { splitCommandString } = require('../string-utils');
|
|
||||||
const spawn = require('child_process').spawn;
|
|
||||||
const chokidar = require('chokidar');
|
const chokidar = require('chokidar');
|
||||||
const { ErrorNotFound } = require('./rest/utils/errors');
|
const { ErrorNotFound } = require('./rest/utils/errors');
|
||||||
|
|
||||||
@ -213,68 +211,6 @@ export default class ExternalEditWatcher {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
textEditorCommand() {
|
|
||||||
const editorCommand = Setting.value('editor');
|
|
||||||
if (!editorCommand) return null;
|
|
||||||
|
|
||||||
const s = splitCommandString(editorCommand, { handleEscape: false });
|
|
||||||
const path = s.splice(0, 1);
|
|
||||||
if (!path.length) throw new Error(`Invalid editor command: ${editorCommand}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
path: path[0],
|
|
||||||
args: s,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async spawnCommand(path: string, args: string[], options: any) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// App bundles need to be opened using the `open` command.
|
|
||||||
// Additional args can be specified after --args, and the
|
|
||||||
// -n flag is needed to ensure that the app is always launched
|
|
||||||
// with the arguments. Without it, if the app is already opened,
|
|
||||||
// it will just bring it to the foreground without opening the file.
|
|
||||||
// So the full command is:
|
|
||||||
//
|
|
||||||
// open -n /path/to/editor.app --args -app-flag -bla /path/to/file.md
|
|
||||||
//
|
|
||||||
if (shim.isMac() && fileExtension(path) === 'app') {
|
|
||||||
args = args.slice();
|
|
||||||
args.splice(0, 0, '--args');
|
|
||||||
args.splice(0, 0, path);
|
|
||||||
args.splice(0, 0, '-n');
|
|
||||||
path = 'open';
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapError = (error: any) => {
|
|
||||||
if (!error) return error;
|
|
||||||
const msg = error.message ? [error.message] : [];
|
|
||||||
msg.push(`Command was: "${path}" ${args.join(' ')}`);
|
|
||||||
error.message = msg.join('\n\n');
|
|
||||||
return error;
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subProcess = spawn(path, args, options);
|
|
||||||
|
|
||||||
const iid = shim.setInterval(() => {
|
|
||||||
if (subProcess && subProcess.pid) {
|
|
||||||
this.logger().debug(`Started editor with PID ${subProcess.pid}`);
|
|
||||||
shim.clearInterval(iid);
|
|
||||||
resolve(null);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
subProcess.on('error', (error: any) => {
|
|
||||||
shim.clearInterval(iid);
|
|
||||||
reject(wrapError(error));
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw wrapError(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async openAndWatch(note: NoteEntity) {
|
async openAndWatch(note: NoteEntity) {
|
||||||
if (!note || !note.id) {
|
if (!note || !note.id) {
|
||||||
this.logger().warn('ExternalEditWatcher: Cannot open note: ', note);
|
this.logger().warn('ExternalEditWatcher: Cannot open note: ', note);
|
||||||
@ -285,13 +221,7 @@ export default class ExternalEditWatcher {
|
|||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
this.watch(filePath);
|
this.watch(filePath);
|
||||||
|
|
||||||
const cmd = this.textEditorCommand();
|
await openFileWithExternalEditor(filePath, this.bridge_());
|
||||||
if (!cmd) {
|
|
||||||
this.bridge_().openExternal(`file://${filePath}`);
|
|
||||||
} else {
|
|
||||||
cmd.args.push(filePath);
|
|
||||||
await this.spawnCommand(cmd.path, cmd.args, { detached: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: 'NOTE_FILE_WATCHER_ADD',
|
type: 'NOTE_FILE_WATCHER_ADD',
|
||||||
|
82
packages/lib/services/ExternalEditWatcher/utils.ts
Normal file
82
packages/lib/services/ExternalEditWatcher/utils.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
|
||||||
|
const { splitCommandString } = require('../../string-utils');
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import Logger from '../../Logger';
|
||||||
|
import Setting from '../../models/Setting';
|
||||||
|
import { fileExtension } from '../../path-utils';
|
||||||
|
import shim from '../../shim';
|
||||||
|
|
||||||
|
const logger = Logger.create('ExternalEditWatcher/utils');
|
||||||
|
|
||||||
|
const spawnCommand = async (path: string, args: string[], options: any) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// App bundles need to be opened using the `open` command.
|
||||||
|
// Additional args can be specified after --args, and the
|
||||||
|
// -n flag is needed to ensure that the app is always launched
|
||||||
|
// with the arguments. Without it, if the app is already opened,
|
||||||
|
// it will just bring it to the foreground without opening the file.
|
||||||
|
// So the full command is:
|
||||||
|
//
|
||||||
|
// open -n /path/to/editor.app --args -app-flag -bla /path/to/file.md
|
||||||
|
//
|
||||||
|
if (shim.isMac() && fileExtension(path) === 'app') {
|
||||||
|
args = args.slice();
|
||||||
|
args.splice(0, 0, '--args');
|
||||||
|
args.splice(0, 0, path);
|
||||||
|
args.splice(0, 0, '-n');
|
||||||
|
path = 'open';
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapError = (error: any) => {
|
||||||
|
if (!error) return error;
|
||||||
|
const msg = error.message ? [error.message] : [];
|
||||||
|
msg.push(`Command was: "${path}" ${args.join(' ')}`);
|
||||||
|
error.message = msg.join('\n\n');
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subProcess = spawn(path, args, options);
|
||||||
|
|
||||||
|
const iid = shim.setInterval(() => {
|
||||||
|
if (subProcess && subProcess.pid) {
|
||||||
|
logger.debug(`Started editor with PID ${subProcess.pid}`);
|
||||||
|
shim.clearInterval(iid);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
subProcess.on('error', (error: any) => {
|
||||||
|
shim.clearInterval(iid);
|
||||||
|
reject(wrapError(error));
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw wrapError(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const textEditorCommand = () => {
|
||||||
|
const editorCommand = Setting.value('editor');
|
||||||
|
if (!editorCommand) return null;
|
||||||
|
|
||||||
|
const s = splitCommandString(editorCommand, { handleEscape: false });
|
||||||
|
const path = s.splice(0, 1);
|
||||||
|
if (!path.length) throw new Error(`Invalid editor command: ${editorCommand}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: path[0],
|
||||||
|
args: s,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const openFileWithExternalEditor = async (filePath: string, bridge: any) => {
|
||||||
|
const cmd = textEditorCommand();
|
||||||
|
if (!cmd) {
|
||||||
|
bridge.openExternal(`file://${filePath}`);
|
||||||
|
} else {
|
||||||
|
cmd.args.push(filePath);
|
||||||
|
await spawnCommand(cmd.path, cmd.args, { detached: true });
|
||||||
|
}
|
||||||
|
};
|
@ -55,6 +55,9 @@ const defaultKeymapItems = {
|
|||||||
{ accelerator: 'Option+Cmd+A', command: 'editor.sortSelectedLines' },
|
{ accelerator: 'Option+Cmd+A', command: 'editor.sortSelectedLines' },
|
||||||
{ accelerator: 'Option+Up', command: 'editor.swapLineUp' },
|
{ accelerator: 'Option+Up', command: 'editor.swapLineUp' },
|
||||||
{ accelerator: 'Option+Down', command: 'editor.swapLineDown' },
|
{ accelerator: 'Option+Down', command: 'editor.swapLineDown' },
|
||||||
|
{ accelerator: 'Option+Cmd+1', command: 'switchProfile1' },
|
||||||
|
{ accelerator: 'Option+Cmd+2', command: 'switchProfile2' },
|
||||||
|
{ accelerator: 'Option+Cmd+3', command: 'switchProfile3' },
|
||||||
],
|
],
|
||||||
default: [
|
default: [
|
||||||
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
||||||
@ -97,6 +100,9 @@ const defaultKeymapItems = {
|
|||||||
{ accelerator: 'Ctrl+Alt+S', command: 'editor.sortSelectedLines' },
|
{ accelerator: 'Ctrl+Alt+S', command: 'editor.sortSelectedLines' },
|
||||||
{ accelerator: 'Alt+Up', command: 'editor.swapLineUp' },
|
{ accelerator: 'Alt+Up', command: 'editor.swapLineUp' },
|
||||||
{ accelerator: 'Alt+Down', command: 'editor.swapLineDown' },
|
{ accelerator: 'Alt+Down', command: 'editor.swapLineDown' },
|
||||||
|
{ accelerator: 'Ctrl+Alt+1', command: 'switchProfile1' },
|
||||||
|
{ accelerator: 'Ctrl+Alt+2', command: 'switchProfile2' },
|
||||||
|
{ accelerator: 'Ctrl+Alt+3', command: 'switchProfile3' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,13 +6,15 @@ import propsHaveChanged from './propsHaveChanged';
|
|||||||
const { createSelectorCreator, defaultMemoize } = require('reselect');
|
const { createSelectorCreator, defaultMemoize } = require('reselect');
|
||||||
const { createCachedSelector } = require('re-reselect');
|
const { createCachedSelector } = require('re-reselect');
|
||||||
|
|
||||||
interface MenuItem {
|
export interface MenuItem {
|
||||||
id: string;
|
id?: string;
|
||||||
label: string;
|
label?: string;
|
||||||
click: Function;
|
click?: Function;
|
||||||
role?: any;
|
role?: any;
|
||||||
|
type?: string;
|
||||||
accelerator?: string;
|
accelerator?: string;
|
||||||
enabled: boolean;
|
checked?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuItems {
|
interface MenuItems {
|
||||||
|
@ -30,6 +30,7 @@ export interface WhenClauseContext {
|
|||||||
folderIsShared: boolean;
|
folderIsShared: boolean;
|
||||||
folderIsShareRoot: boolean;
|
folderIsShareRoot: boolean;
|
||||||
joplinServerConnected: boolean;
|
joplinServerConnected: boolean;
|
||||||
|
hasMultiProfiles: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function stateToWhenClauseContext(state: State, options: WhenClauseContextOptions = null): WhenClauseContext {
|
export default function stateToWhenClauseContext(state: State, options: WhenClauseContextOptions = null): WhenClauseContext {
|
||||||
@ -82,5 +83,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
|
|||||||
folderIsShared: commandFolder ? !!commandFolder.share_id : false,
|
folderIsShared: commandFolder ? !!commandFolder.share_id : false,
|
||||||
|
|
||||||
joplinServerConnected: [9, 10].includes(state.settings['sync.target']),
|
joplinServerConnected: [9, 10].includes(state.settings['sync.target']),
|
||||||
|
|
||||||
|
hasMultiProfiles: state.profileConfig && state.profileConfig.profiles.length > 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
85
packages/lib/services/profileConfig/index.test.ts
Normal file
85
packages/lib/services/profileConfig/index.test.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { writeFile } from 'fs-extra';
|
||||||
|
import { createNewProfile, getProfileFullPath, loadProfileConfig, saveProfileConfig } from '.';
|
||||||
|
import { tempFilePath } from '../../testing/test-utils';
|
||||||
|
import { defaultProfile, defaultProfileConfig, ProfileConfig } from './types';
|
||||||
|
|
||||||
|
describe('profileConfig/index', () => {
|
||||||
|
|
||||||
|
it('should load a default profile config', async () => {
|
||||||
|
const filePath = tempFilePath('json');
|
||||||
|
const config = await loadProfileConfig(filePath);
|
||||||
|
expect(config).toEqual(defaultProfileConfig());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load a profile config', async () => {
|
||||||
|
const filePath = tempFilePath('json');
|
||||||
|
const config = {
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
name: 'Testing',
|
||||||
|
path: '.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
await writeFile(filePath, JSON.stringify(config), 'utf8');
|
||||||
|
|
||||||
|
const loadedConfig = await loadProfileConfig(filePath);
|
||||||
|
|
||||||
|
const expected: ProfileConfig = {
|
||||||
|
version: 1,
|
||||||
|
currentProfile: 0,
|
||||||
|
profiles: [
|
||||||
|
{
|
||||||
|
name: 'Testing',
|
||||||
|
path: '.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(loadedConfig).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should load a save a config', async () => {
|
||||||
|
const filePath = tempFilePath('json');
|
||||||
|
const config = defaultProfileConfig();
|
||||||
|
await saveProfileConfig(filePath, config);
|
||||||
|
|
||||||
|
const loadedConfig = await loadProfileConfig(filePath);
|
||||||
|
expect(config).toEqual(loadedConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a profile full path', async () => {
|
||||||
|
const profile1 = {
|
||||||
|
...defaultProfile(),
|
||||||
|
path: 'profile-abcd',
|
||||||
|
};
|
||||||
|
|
||||||
|
const profile2 = {
|
||||||
|
...defaultProfile(),
|
||||||
|
path: '.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const profile3 = {
|
||||||
|
...defaultProfile(),
|
||||||
|
path: 'profiles/pro/',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(getProfileFullPath(profile1, '/test/root')).toBe('/test/root/profile-abcd');
|
||||||
|
expect(getProfileFullPath(profile2, '/test/root')).toBe('/test/root');
|
||||||
|
expect(getProfileFullPath(profile3, '/test/root')).toBe('/test/root/profiles/pro');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a new profile', async () => {
|
||||||
|
let config = defaultProfileConfig();
|
||||||
|
config = createNewProfile(config, 'new profile 1');
|
||||||
|
config = createNewProfile(config, 'new profile 2');
|
||||||
|
|
||||||
|
expect(config.profiles.length).toBe(3);
|
||||||
|
expect(config.profiles[1].name).toBe('new profile 1');
|
||||||
|
expect(config.profiles[2].name).toBe('new profile 2');
|
||||||
|
|
||||||
|
expect(config.profiles[1].path).not.toBe(config.profiles[2].path);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
64
packages/lib/services/profileConfig/index.ts
Normal file
64
packages/lib/services/profileConfig/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { rtrimSlashes, trimSlashes } from '../../path-utils';
|
||||||
|
import shim from '../../shim';
|
||||||
|
import { defaultProfile, defaultProfileConfig, Profile, ProfileConfig } from './types';
|
||||||
|
import { customAlphabet } from 'nanoid/non-secure';
|
||||||
|
|
||||||
|
export const loadProfileConfig = async (profileConfigPath: string): Promise<ProfileConfig> => {
|
||||||
|
if (!(await shim.fsDriver().exists(profileConfigPath))) {
|
||||||
|
return defaultProfileConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configContent = await shim.fsDriver().readFile(profileConfigPath, 'utf8');
|
||||||
|
const parsed = JSON.parse(configContent) as ProfileConfig;
|
||||||
|
if (!parsed.profiles || !parsed.profiles.length) throw new Error(`Profile config should contain at least one profile: ${profileConfigPath}`);
|
||||||
|
|
||||||
|
const output: ProfileConfig = {
|
||||||
|
...defaultProfileConfig(),
|
||||||
|
...parsed,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < output.profiles.length; i++) {
|
||||||
|
output.profiles[i] = {
|
||||||
|
...defaultProfile(),
|
||||||
|
...output.profiles[i],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.currentProfile < 0 || output.currentProfile >= output.profiles.length) throw new Error(`Profile index out of range: ${output.currentProfile}`);
|
||||||
|
return output;
|
||||||
|
} catch (error) {
|
||||||
|
error.message = `Could not parse profile configuration: ${profileConfigPath}: ${error.message}`;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveProfileConfig = async (profileConfigPath: string, config: ProfileConfig) => {
|
||||||
|
await shim.fsDriver().writeFile(profileConfigPath, JSON.stringify(config, null, '\t'), 'utf8');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentProfile = (config: ProfileConfig): Profile => {
|
||||||
|
return { ...config.profiles[config.currentProfile] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getProfileFullPath = (profile: Profile, rootProfilePath: string): string => {
|
||||||
|
let p = trimSlashes(profile.path);
|
||||||
|
if (p === '.') p = '';
|
||||||
|
return rtrimSlashes(`${rtrimSlashes(rootProfilePath)}/${p}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const profileIdGenerator = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8);
|
||||||
|
|
||||||
|
export const createNewProfile = (config: ProfileConfig, profileName: string) => {
|
||||||
|
const newConfig = {
|
||||||
|
...config,
|
||||||
|
profiles: config.profiles.slice(),
|
||||||
|
};
|
||||||
|
|
||||||
|
newConfig.profiles.push({
|
||||||
|
name: profileName,
|
||||||
|
path: `profile-${profileIdGenerator()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
};
|
16
packages/lib/services/profileConfig/initProfile.ts
Normal file
16
packages/lib/services/profileConfig/initProfile.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { getCurrentProfile, getProfileFullPath, loadProfileConfig } from '.';
|
||||||
|
import Setting from '../../models/Setting';
|
||||||
|
|
||||||
|
export default async (rootProfileDir: string) => {
|
||||||
|
const profileConfig = await loadProfileConfig(`${rootProfileDir}/profiles.json`);
|
||||||
|
const profileDir = getProfileFullPath(getCurrentProfile(profileConfig), rootProfileDir);
|
||||||
|
const isSubProfile = profileConfig.currentProfile !== 0;
|
||||||
|
Setting.setConstant('isSubProfile', isSubProfile);
|
||||||
|
Setting.setConstant('rootProfileDir', rootProfileDir);
|
||||||
|
Setting.setConstant('profileDir', profileDir);
|
||||||
|
return {
|
||||||
|
profileConfig,
|
||||||
|
profileDir,
|
||||||
|
isSubProfile,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import Setting from '../../models/Setting';
|
||||||
|
|
||||||
|
export default (rootSettings: Record<string, any>, subProfileSettings: Record<string, any>) => {
|
||||||
|
const output: Record<string, any> = { ...subProfileSettings };
|
||||||
|
|
||||||
|
for (const k of Object.keys(output)) {
|
||||||
|
const md = Setting.settingMetadata(k);
|
||||||
|
if (md.isGlobal) {
|
||||||
|
delete output[k];
|
||||||
|
if (k in rootSettings) output[k] = rootSettings[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const k of Object.keys(rootSettings)) {
|
||||||
|
const md = Setting.settingMetadata(k);
|
||||||
|
if (md.isGlobal) {
|
||||||
|
output[k] = rootSettings[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import Setting from '../../models/Setting';
|
||||||
|
import { SettingValues } from '../../models/settings/FileHandler';
|
||||||
|
|
||||||
|
export default (settings: SettingValues) => {
|
||||||
|
const globalSettings: SettingValues = {};
|
||||||
|
const localSettings: SettingValues = {};
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(settings)) {
|
||||||
|
const md = Setting.settingMetadata(k);
|
||||||
|
|
||||||
|
if (md.isGlobal) {
|
||||||
|
globalSettings[k] = v;
|
||||||
|
} else {
|
||||||
|
localSettings[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { globalSettings, localSettings };
|
||||||
|
};
|
27
packages/lib/services/profileConfig/types.ts
Normal file
27
packages/lib/services/profileConfig/types.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export interface Profile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileConfig {
|
||||||
|
version: number;
|
||||||
|
currentProfile: number;
|
||||||
|
profiles: Profile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultProfile = (): Profile => {
|
||||||
|
return {
|
||||||
|
name: 'Default',
|
||||||
|
path: '.',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultProfileConfig = (): ProfileConfig => {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
currentProfile: 0,
|
||||||
|
profiles: [defaultProfile()],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProfileSwitchClickHandler = (profileIndex: number)=> void;
|
@ -106,6 +106,7 @@ const supportDir = `${oldTestDir}/support`;
|
|||||||
// various space-in-path issues.
|
// various space-in-path issues.
|
||||||
const dataDir = `${oldTestDir}/test data/${suiteName_}`;
|
const dataDir = `${oldTestDir}/test data/${suiteName_}`;
|
||||||
const profileDir = `${dataDir}/profile`;
|
const profileDir = `${dataDir}/profile`;
|
||||||
|
const rootProfileDir = profileDir;
|
||||||
|
|
||||||
fs.mkdirpSync(logDir);
|
fs.mkdirpSync(logDir);
|
||||||
fs.mkdirpSync(baseTempDir);
|
fs.mkdirpSync(baseTempDir);
|
||||||
@ -185,6 +186,7 @@ Setting.setConstant('tempDir', baseTempDir);
|
|||||||
Setting.setConstant('cacheDir', baseTempDir);
|
Setting.setConstant('cacheDir', baseTempDir);
|
||||||
Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`);
|
Setting.setConstant('pluginDataDir', `${profileDir}/profile/plugin-data`);
|
||||||
Setting.setConstant('profileDir', profileDir);
|
Setting.setConstant('profileDir', profileDir);
|
||||||
|
Setting.setConstant('rootProfileDir', rootProfileDir);
|
||||||
Setting.setConstant('env', 'dev');
|
Setting.setConstant('env', 'dev');
|
||||||
|
|
||||||
BaseService.logger_ = logger;
|
BaseService.logger_ = logger;
|
||||||
@ -271,6 +273,8 @@ async function switchClient(id: number, options: any = null) {
|
|||||||
await Setting.reset();
|
await Setting.reset();
|
||||||
Setting.settingFilename = `settings-${id}.json`;
|
Setting.settingFilename = `settings-${id}.json`;
|
||||||
|
|
||||||
|
Setting.setConstant('profileDir', rootProfileDir);
|
||||||
|
Setting.setConstant('rootProfileDir', rootProfileDir);
|
||||||
Setting.setConstant('resourceDirName', resourceDirName(id));
|
Setting.setConstant('resourceDirName', resourceDirName(id));
|
||||||
Setting.setConstant('resourceDir', resourceDir(id));
|
Setting.setConstant('resourceDir', resourceDir(id));
|
||||||
Setting.setConstant('pluginDir', pluginDir(id));
|
Setting.setConstant('pluginDir', pluginDir(id));
|
||||||
@ -330,6 +334,9 @@ async function setupDatabase(id: number = null, options: any = null) {
|
|||||||
// running.
|
// running.
|
||||||
await Setting.reset();
|
await Setting.reset();
|
||||||
|
|
||||||
|
Setting.setConstant('profileDir', rootProfileDir);
|
||||||
|
Setting.setConstant('rootProfileDir', rootProfileDir);
|
||||||
|
|
||||||
if (databases_[id]) {
|
if (databases_[id]) {
|
||||||
BaseModel.setDb(databases_[id]);
|
BaseModel.setDb(databases_[id]);
|
||||||
await clearDatabase(id);
|
await clearDatabase(id);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const createUuidV4 = require('uuid/v4');
|
const createUuidV4 = require('uuid/v4');
|
||||||
const { customAlphabet } = require('nanoid/non-secure');
|
import { customAlphabet } from 'nanoid/non-secure';
|
||||||
|
|
||||||
// https://zelark.github.io/nano-id-cc/
|
// https://zelark.github.io/nano-id-cc/
|
||||||
// https://security.stackexchange.com/a/41749/1873
|
// https://security.stackexchange.com/a/41749/1873
|
||||||
|
19
yarn.lock
19
yarn.lock
@ -3118,6 +3118,7 @@ __metadata:
|
|||||||
"@types/fs-extra": ^9.0.6
|
"@types/fs-extra": ^9.0.6
|
||||||
"@types/jest": ^26.0.15
|
"@types/jest": ^26.0.15
|
||||||
"@types/js-yaml": ^4.0.2
|
"@types/js-yaml": ^4.0.2
|
||||||
|
"@types/nanoid": ^3.0.0
|
||||||
"@types/node": ^14.14.6
|
"@types/node": ^14.14.6
|
||||||
"@types/node-rsa": ^1.1.1
|
"@types/node-rsa": ^1.1.1
|
||||||
"@types/react": ^17.0.20
|
"@types/react": ^17.0.20
|
||||||
@ -5576,6 +5577,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/nanoid@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "@types/nanoid@npm:3.0.0"
|
||||||
|
dependencies:
|
||||||
|
nanoid: "*"
|
||||||
|
checksum: 6e84d71ce2b8a2e23b20a018249a2e6a1c36b4ea0f45ff0265e8db514010ccf13141d87d6892f35b8e50ca5832d1d04ddc27062b5e69f91db2d5b3c5321c8c56
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/node-fetch@npm:1.6.9":
|
"@types/node-fetch@npm:1.6.9":
|
||||||
version: 1.6.9
|
version: 1.6.9
|
||||||
resolution: "@types/node-fetch@npm:1.6.9"
|
resolution: "@types/node-fetch@npm:1.6.9"
|
||||||
@ -21819,6 +21829,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"nanoid@npm:*":
|
||||||
|
version: 3.3.2
|
||||||
|
resolution: "nanoid@npm:3.3.2"
|
||||||
|
bin:
|
||||||
|
nanoid: bin/nanoid.cjs
|
||||||
|
checksum: 376717f0685251fad77850bd84c6b8d57837c71eeb1c05be7c742140cc1835a5a2953562add05166d6dbc8fb65f3fdffa356213037b967a470e1691dc3e7b9cc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"nanoid@npm:^2.1.1":
|
"nanoid@npm:^2.1.1":
|
||||||
version: 2.1.11
|
version: 2.1.11
|
||||||
resolution: "nanoid@npm:2.1.11"
|
resolution: "nanoid@npm:2.1.11"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user