├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc.json
├── LICENSE.md
├── README.md
├── esbuild.config.mjs
├── images
├── Filter-Demo.gif
├── Startup-Demo.gif
├── commands.gif
├── group-creation.gif
├── manager-group.gif
└── startup-group.gif
├── main.ts
├── manifest-beta.json
├── manifest.json
├── package-lock.json
├── package.json
├── src
├── Components
│ ├── BaseComponents
│ │ ├── ActionableComponent.ts
│ │ ├── ConfirmationPopupModal.ts
│ │ ├── DropdownActionButton.ts
│ │ ├── HtmlComponent.ts
│ │ ├── RemovableChip.ts
│ │ ├── ReorderableList.ts
│ │ ├── SettingsList.ts
│ │ ├── TabComponent.ts
│ │ ├── TabGroupComponent.ts
│ │ └── TogglableList.ts
│ ├── DescriptionsList.ts
│ ├── DeviceSelectionModal.ts
│ ├── EditPluginList.ts
│ ├── FilteredGroupsList.ts
│ ├── Modals
│ │ ├── GroupEditModal.ts
│ │ ├── GroupSettingsMenu.ts
│ │ └── PluginModal.ts
│ ├── PluginListTogglable.ts
│ ├── ReorderablePluginList.ts
│ └── Settings
│ │ ├── AdvancedSettings.ts
│ │ ├── GroupSettings.ts
│ │ └── PluginsSettings.ts
├── DataStructures
│ ├── PgPlugin.ts
│ └── PluginGroup.ts
├── GroupEditModal
│ ├── GroupEditGeneralTab.ts
│ ├── GroupEditGroupsTab.ts
│ └── GroupEditPluginsTab.ts
├── Managers
│ ├── CommandManager.ts
│ ├── Manager.ts
│ └── PluginManager.ts
├── PluginGroupSettings.ts
└── Utils
│ ├── Constants.ts
│ ├── Types.ts
│ └── Utilities.ts
├── styles.css
├── tsconfig.json
├── version-bump.mjs
└── versions.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": ["@typescript-eslint"],
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "parserOptions": {
12 | "sourceType": "module"
13 | },
14 | "rules": {
15 | "no-unused-vars": "off",
16 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
17 | "@typescript-eslint/ban-ts-comment": "off",
18 | "no-prototype-builtins": "off",
19 | "@typescript-eslint/no-empty-function": "off"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian plugin
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | env:
9 | PLUGIN_NAME: obsidian-plugin-groups
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v2
17 | - name: Use Node.js
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: '16.x'
21 |
22 | - name: Build
23 | id: build
24 | run: |
25 | npm install
26 | npm run build
27 | mkdir ${{ env.PLUGIN_NAME }}
28 | cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }}
29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }}
30 | ls
31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)"
32 |
33 | - name: Create Release
34 | id: create_release
35 | uses: actions/create-release@v1
36 | env:
37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38 | VERSION: ${{ github.ref }}
39 | with:
40 | tag_name: ${{ github.ref }}
41 | release_name: ${{ github.ref }}
42 | draft: false
43 | prerelease: false
44 |
45 | - name: Upload zip file
46 | id: upload-zip
47 | uses: actions/upload-release-asset@v1
48 | env:
49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | with:
51 | upload_url: ${{ steps.create_release.outputs.upload_url }}
52 | asset_path: ./${{ env.PLUGIN_NAME }}.zip
53 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip
54 | asset_content_type: application/zip
55 |
56 | - name: Upload main.js
57 | id: upload-main
58 | uses: actions/upload-release-asset@v1
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 | with:
62 | upload_url: ${{ steps.create_release.outputs.upload_url }}
63 | asset_path: ./main.js
64 | asset_name: main.js
65 | asset_content_type: text/javascript
66 |
67 | - name: Upload manifest.json
68 | id: upload-manifest
69 | uses: actions/upload-release-asset@v1
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72 | with:
73 | upload_url: ${{ steps.create_release.outputs.upload_url }}
74 | asset_path: ./manifest.json
75 | asset_name: manifest.json
76 | asset_content_type: application/json
77 |
78 | - name: Upload styles.css
79 | id: upload-css
80 | uses: actions/upload-release-asset@v1
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | with:
84 | upload_url: ${{ steps.create_release.outputs.upload_url }}
85 | asset_path: ./styles.css
86 | asset_name: styles.css
87 | asset_content_type: text/css
88 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # Exclude sourcemaps
16 | *.map
17 |
18 | # obsidian
19 | data.json
20 |
21 | # Exclude macOS Finder (System Explorer) View States
22 | .DS_Store
23 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
4 |
5 | # vscode
6 | .vscode
7 |
8 | # Intellij
9 | *.iml
10 | .idea
11 |
12 | # npm
13 | node_modules
14 |
15 | # Don't include the compiled main.js file in the repo.
16 | # They should be uploaded to GitHub releases instead.
17 | main.js
18 |
19 | # Exclude sourcemaps
20 | *.map
21 |
22 | # obsidian
23 | data.json
24 |
25 | # Exclude macOS Finder (System Explorer) View States
26 | .DS_Store
27 |
28 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 4,
4 | "semi": true,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Mocca101
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Plugin Groups for Obsidian
2 |
3 | A plugin that allows you to easily group and manage your other plugins in Obsidian.
4 |
5 | ## Features
6 |
7 | - **Grouping:** Organize your plugins into logical groups to make it easier to manage them.
8 | - **Bulk actions:** Enable or disable the plugins in a group with a single click or command.
9 | - **Delayed Group Loading:** You can set your groups to load after a custom delay following Obsidian's startup. This can be useful for reducing the load on your system during startup, resulting in shorter loading times.
10 | - **Group Nesting:** Manage your groups with groups. E.g. you have a group for style related plugin and one for Plugins you use often but don't need immediately on start up. Create both of them and add them To a parent group that loads these delayed on start up, so you can get to work quickly without having to wait for all of them to load.
11 |
12 | ## Limitations
13 |
14 | Some plugins don't work with delayed loading, as they need to load before the workspace is loaded. In the future, I hope it will be possible to load those delayed as well. Until then here's a list of plugins that don't work with delayed loading:
15 |
16 | - [Pane Relief](https://github.com/pjeby/pane-relief)
17 | - [Hidden Folder](https://github.com/ptrsvltns/hidden-folder-obsidian)
18 |
19 | Some plugins will also have minor issues (proper view not loading). This can be resolved by closing and reopening the affected pane.
20 | E.g.:
21 |
22 | - [Media Extended](https://github.com/aidenlx/media-extended)
23 |
24 | Sometimes the pane will automatically reload, however this will only happen after the plugin has loaded that is the case for e.g.:
25 |
26 | - [Kanban](https://github.com/mgmeyers/obsidian-kanban)
27 | - [Outliner](https://github.com/vslinko/obsidian-outliner)
28 |
29 | _If you notice a plugin that has issues with delayed loading, please let me know or open a PR with the Plugin added to the list in this README.md_
30 |
31 | Unfortunately it is not possible yet to set the order of starting plugins within a group. Therefore if plugins depend on one another and a plugin needs to be loaded before another one, I advise putting them in different groups and loading those accordingly. Though it might work without doing that I'd advise against it just to be safe.
32 |
33 | ## Installation
34 |
35 | Keep in mind this is an early version of this plugin so there might be some kinks left to iron out. If you encounter any, please do let me know!
36 |
37 | To install Plugin Groups, follow these steps:
38 |
39 | 1. Head to the releases tab and download the latest version.
40 | 2. Open your plugins folder in the `.obsidian` folder of your vault.
41 | 3. Create a new folder named `obsidian-plugin-groups` and paste the `manifest.json`, `style.css` and `main.js` into it. Or directly copy the whole folder from the zipped file.
42 | 4. Done
43 |
44 | Note: As soon as this plugin is available on the public obsidian plugin repository it will also be available from the Community plugin list directly.
45 |
46 | ## Usage
47 |
48 | To use Obsidian Groups, enable it from the Community Plugins Menu and start organizing and managing your plugins by creating groups.
49 |
50 | ### Creating a Group
51 |
52 | 
53 |
54 | To create a new group head to the plugin settings, Enter a name for the group and click the "+" button. You can then;
55 |
56 | - Toggle your plugins on/off for the group to in- or exclude them from the group.
57 | - Choose whether commands should be generated for the group.
58 | - Set the group to launch on Obsidian's startup (with or without delay).
59 | - Include other groups to be controlled by this group.
60 | Click "Save" to finish the creation process.
61 |
62 | ### Editing a Group
63 |
64 | To edit an existing group, click on the pen icon next to the group name in the plugin settings. From here, you can edit the group the same way you created it.
65 | Alternatively you can choose which groups a plugin should belong to from the plugins list within the Settings.
66 |
67 | ### Enabling/Disabling Groups
68 |
69 | You can enable or disable a group by clicking the "On" & "Off" buttons next to the group name in the plugin settings. If enabled, sou can also use the following commands in the command panel to enable or disable your groups:
70 |
71 | - Plugin Groups Enable: "Your Group Name"
72 | - Plugin Groups Disable: "Your Group Name"
73 |
74 | 
75 |
76 | ### Lazy Loading (Delayed loading on Obsidians Startup)
77 |
78 | To enable loading your plugins delayed you'll need to do the following:
79 |
80 | 1. Manually disable the plugins you want to load through lazy load in the community plugins tab. Or, even better delete the id's of the plugins in the file: `.obsidian/community-plugins.json`.
81 | Explanation: When the plugins are enabled in manually they are written in the file and therefore load on obsidian's startup (not through plugin groups).
82 | 2. In the groups that contain the plugins you want to load delayed, toggle the "Load on Startup" button.
83 | 1. Choose the desired behaviour (Enable or Disable).
84 | 2. Set the delay for the plugins
85 | 3. Done! On your next startup you should see an improvement in your startup time.
86 |
87 | 
88 |
89 | ## Support
90 |
91 | If you find the Plugin Groups to be a useful tool, please consider supporting me through a donation via Buy Me a Coffee or starring this project on GitHub. Alternatively consider [Donating to the Internet Archive](https://archive.org/donate/) an awesome project, preserving and providing access to digital media and information, now and for future generations!
92 | Your support helps me to continue developing and maintaining this plugin.
93 |
94 |
95 |
96 | If you have any questions, feedback, issues or bugs, please don't hesitate to contact me or create an issue in this Repository.
97 |
98 | Thank you for using Plugin Groups I hope it makes your life easier!
99 |
100 | ## Help me with the documentation:
101 |
102 | Even though I try my best to keep the documentation up to date in the Readme, there may be things that I miss, such as spelling mistakes or features that could be explained more clearly. If you notice any of these issues, please let me know. Or, even better, if you are able to fix the mistakes or write clearer explanations, you can create a pull request with your changes. This helps me focus more on development and allows other users to benefit from improved usage documentation!
103 |
104 | If you do end up helping me like that, thank you so much! If you make use of Images, put them into the images folder. The theme used is the light version of the obsidian standard theme.
105 |
106 | Also if you've got any usages or tips you think might benefit other don't hesitate to create a pullrequest for those as well.
107 |
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from 'esbuild';
2 | import process from 'process';
3 | import builtins from 'builtin-modules';
4 |
5 | const banner = `/*
6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
7 | if you want to view the source, please visit the github repository of this plugin
8 | */
9 | `;
10 |
11 | const prod = process.argv[2] === 'production';
12 |
13 | esbuild
14 | .build({
15 | banner: {
16 | js: banner,
17 | },
18 | entryPoints: ['main.ts'],
19 | bundle: true,
20 | external: [
21 | 'obsidian',
22 | 'electron',
23 | '@codemirror/autocomplete',
24 | '@codemirror/collab',
25 | '@codemirror/commands',
26 | '@codemirror/language',
27 | '@codemirror/lint',
28 | '@codemirror/search',
29 | '@codemirror/state',
30 | '@codemirror/view',
31 | '@lezer/common',
32 | '@lezer/highlight',
33 | '@lezer/lr',
34 | ...builtins,
35 | ],
36 | format: 'cjs',
37 | watch: !prod,
38 | target: 'es2018',
39 | logLevel: 'info',
40 | sourcemap: prod ? false : 'inline',
41 | treeShaking: true,
42 | outfile: 'main.js',
43 | })
44 | .catch(() => process.exit(1));
45 |
--------------------------------------------------------------------------------
/images/Filter-Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mocca101/obsidian-plugin-groups/23d4f9615646537a42c1d461185bb8545c8359ec/images/Filter-Demo.gif
--------------------------------------------------------------------------------
/images/Startup-Demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mocca101/obsidian-plugin-groups/23d4f9615646537a42c1d461185bb8545c8359ec/images/Startup-Demo.gif
--------------------------------------------------------------------------------
/images/commands.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mocca101/obsidian-plugin-groups/23d4f9615646537a42c1d461185bb8545c8359ec/images/commands.gif
--------------------------------------------------------------------------------
/images/group-creation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mocca101/obsidian-plugin-groups/23d4f9615646537a42c1d461185bb8545c8359ec/images/group-creation.gif
--------------------------------------------------------------------------------
/images/manager-group.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mocca101/obsidian-plugin-groups/23d4f9615646537a42c1d461185bb8545c8359ec/images/manager-group.gif
--------------------------------------------------------------------------------
/images/startup-group.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mocca101/obsidian-plugin-groups/23d4f9615646537a42c1d461185bb8545c8359ec/images/startup-group.gif
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { Notice, Plugin } from 'obsidian';
2 | import PluginGroupSettings from './src/PluginGroupSettings';
3 | import { disableStartupTimeout } from './src/Utils/Constants';
4 | import Manager from './src/Managers/Manager';
5 | import CommandManager from './src/Managers/CommandManager';
6 | import PluginManager from './src/Managers/PluginManager';
7 |
8 | export default class PgMain extends Plugin {
9 | async onload() {
10 | const times: {
11 | label: string;
12 | time: number;
13 | }[] = [];
14 |
15 | times.push({ label: 'Time on Load', time: this.getCurrentTime() });
16 |
17 | await Manager.getInstance().init(this);
18 | this.logTime('Manager Setup', times);
19 |
20 | await PluginManager.loadNewPlugins();
21 | this.logTime('Loading new plugins', times);
22 |
23 | this.addSettingTab(new PluginGroupSettings(this.app, this));
24 | this.logTime('Creating the Settings Tab', times);
25 |
26 | Manager.getInstance().updateStatusbarItem();
27 |
28 | if (!Manager.getInstance().groupsMap) {
29 | this.displayTimeNotice(times);
30 |
31 | return; // Exit early if there are no groups yet, no need to load the rest.
32 | }
33 |
34 | if (Manager.getInstance().generateCommands) {
35 | Manager.getInstance().groupsMap.forEach((group) =>
36 | CommandManager.getInstance().AddGroupCommands(group.id)
37 | );
38 | if (Manager.getInstance().devLog) {
39 | this.logTime('Generated Commands for Groups in', times);
40 | }
41 | }
42 |
43 | // TODO: Improve hacky solution if possible
44 | if (window.performance.now() < disableStartupTimeout) {
45 | Manager.getInstance().groupsMap.forEach((group) => {
46 | if (group.loadAtStartup) group.startup();
47 | });
48 |
49 | if (Manager.getInstance().devLog) {
50 | this.logTime('Dispatching Groups for delayed start in', times);
51 | }
52 | }
53 |
54 | this.displayTimeNotice(times);
55 | }
56 |
57 | private logTime(label: string, times: { label: string; time: number }[]) {
58 | if (Manager.getInstance().devLog) {
59 | times.push({ label, time: this.elapsedTime(times) });
60 | }
61 | }
62 |
63 | private displayTimeNotice(times: { label: string; time: number }[]) {
64 | if (!Manager.getInstance().devLog || times.length === 0) {
65 | return;
66 | }
67 | const totalTime = Math.round(this.accTime(times.slice(1)));
68 |
69 | new Notice(
70 | times
71 | .map((item) => item.label + ': ' + item.time + ' ms')
72 | .join('\n') +
73 | '\nTotal Time: ' +
74 | totalTime +
75 | ' ms',
76 | 10000
77 | );
78 | }
79 |
80 | private elapsedTime(times: { label: string; time: number }[]): number {
81 | if (times.length > 1) {
82 | return this.getCurrentTime() - this.accTime(times);
83 | }
84 | return this.getCurrentTime() - times[0].time;
85 | }
86 |
87 | private accTime(times: { label: string; time: number }[]): number {
88 | return times
89 | .map((item) => item.time)
90 | .reduce((prev, curr) => prev + curr);
91 | }
92 |
93 | private getCurrentTime(): number {
94 | return Date.now();
95 | }
96 |
97 | onunload() {}
98 | }
99 |
--------------------------------------------------------------------------------
/manifest-beta.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-plugin-groups",
3 | "name": "Plugin Groups",
4 | "version": "2.1.0",
5 | "minAppVersion": "0.15.0",
6 | "description": " Manage your Plugins through groups: Enable and disable multiple plugins through a single command, or delay the startup of plugins to speed up your Obsidian start up time.",
7 | "author": "Mocca101",
8 | "authorUrl": "https://github.com/Mocca101",
9 | "isDesktopOnly": false,
10 | "fundingUrl": "https://www.buymeacoffee.com/Mocca101"
11 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-plugin-groups",
3 | "name": "Plugin Groups",
4 | "version": "2.1.0",
5 | "minAppVersion": "0.15.0",
6 | "description": "Manage your Plugins through groups: Enable and disable multiple plugins through a single command, or delay the startup of plugins to speed up your Obsidian start up time.",
7 | "author": "Mocca101",
8 | "authorUrl": "https://github.com/Mocca101",
9 | "isDesktopOnly": false,
10 | "fundingUrl": "https://www.buymeacoffee.com/Mocca101"
11 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-sample-plugin",
3 | "version": "2.1.0",
4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "version": "node version-bump.mjs && git add manifest.json versions.json manifest-beta.json",
10 | "lint": "eslint --fix --ext .js,.jsx,.ts ./src"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@types/node": "^16.11.6",
17 | "@typescript-eslint/eslint-plugin": "5.29.0",
18 | "@typescript-eslint/parser": "5.29.0",
19 | "builtin-modules": "3.3.0",
20 | "esbuild": "0.14.47",
21 | "obsidian": "latest",
22 | "prettier": "2.8.3",
23 | "tslib": "2.4.0",
24 | "typescript": "4.7.4"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/ActionableComponent.ts:
--------------------------------------------------------------------------------
1 | import HtmlComponent from './HtmlComponent';
2 |
3 | export default abstract class ActionableComponent<
4 | Type
5 | > extends HtmlComponent {
6 | private listener: EventListener;
7 | private eventTarget: EventTarget;
8 |
9 | eventType = 'pg-action';
10 | outwardEvent: CustomEvent;
11 |
12 | protected constructor(parentElement: HTMLElement) {
13 | super(parentElement);
14 | }
15 |
16 | subscribe(onActionListener: EventListener) {
17 | this.eventTarget.addEventListener(this.eventType, onActionListener);
18 | }
19 |
20 | unsubscribe(onActionListener: EventListener) {
21 | this.eventTarget.removeEventListener(this.eventType, onActionListener);
22 | }
23 |
24 | private emit() {
25 | this.eventTarget.dispatchEvent(this.outwardEvent);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/ConfirmationPopupModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting } from 'obsidian';
2 |
3 | export default class ConfirmationPopupModal extends Modal {
4 | onConfirm: Event = new Event('onConfirm');
5 |
6 | eventTarget: EventTarget;
7 |
8 | headerText: string;
9 |
10 | cancelText: string;
11 |
12 | confirmText: string;
13 |
14 | private onConfirmListener?: EventListener;
15 |
16 | constructor(
17 | app: App,
18 | headerText: string,
19 | cancelText?: string,
20 | confirmText?: string,
21 | onConfirmListener?: EventListener
22 | ) {
23 | super(app);
24 | this.headerText = headerText;
25 | this.eventTarget = new EventTarget();
26 | this.cancelText = cancelText ?? 'Cancel';
27 | this.confirmText = confirmText ?? 'Confirm';
28 |
29 | this.onConfirmListener = onConfirmListener;
30 | if (this.onConfirmListener) {
31 | this.eventTarget.addEventListener(
32 | this.onConfirm.type,
33 | this.onConfirmListener
34 | );
35 | }
36 | }
37 |
38 | onOpen() {
39 | const { contentEl } = this;
40 |
41 | contentEl.empty();
42 |
43 | contentEl.createEl('h2', { text: this.headerText });
44 |
45 | new Setting(contentEl)
46 | .addButton((btn) => {
47 | btn.setButtonText(this.cancelText);
48 | btn.onClick(() => this.close());
49 | })
50 | .addButton((btn) => {
51 | btn.setButtonText(this.confirmText);
52 | btn.onClick(() => {
53 | this.eventTarget.dispatchEvent(this.onConfirm);
54 | if (this.onConfirmListener) {
55 | this.eventTarget.removeEventListener(
56 | this.onConfirm.type,
57 | this.onConfirmListener
58 | );
59 | }
60 | this.close();
61 | });
62 | });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/DropdownActionButton.ts:
--------------------------------------------------------------------------------
1 | import { setIcon } from 'obsidian';
2 | import HtmlComponent from './HtmlComponent';
3 |
4 | interface DropdownActionButtonOptions {
5 | mainLabel: MainLabelOptions;
6 | dropDownOptions: DropdownOption[];
7 | minWidth?: string;
8 | drpIcon?: string;
9 | }
10 |
11 | export default class DropdownActionButton extends HtmlComponent {
12 | private drpList: HTMLElement;
13 |
14 | constructor(
15 | parentElement: HTMLElement,
16 | options: DropdownActionButtonOptions
17 | ) {
18 | super(parentElement, options);
19 |
20 | this.generateComponent();
21 | }
22 |
23 | protected generateContainer() {
24 | this.mainEl = this.parentEl.createEl('button', {
25 | cls: 'pg-drp-btn pg-has-dropdown-single',
26 | });
27 | }
28 |
29 | protected generateContent() {
30 | if (!this.mainEl) {
31 | return;
32 | }
33 |
34 | const { dropDownOptions, mainLabel, drpIcon, minWidth } = this.options;
35 | if (minWidth) {
36 | this.mainEl.style.minWidth = minWidth;
37 | }
38 |
39 | const activeOptionBtn = this.mainEl.createSpan({
40 | cls: 'pg-drp-btn-main-label',
41 | });
42 | this.setElementTextOrIcon(
43 | activeOptionBtn,
44 | mainLabel.label,
45 | mainLabel.icon
46 | );
47 |
48 | if (drpIcon) {
49 | const iconSpan = this.mainEl.createSpan();
50 | setIcon(iconSpan, drpIcon);
51 | iconSpan.style.paddingTop = '12px';
52 | } else {
53 | this.mainEl.createSpan({ text: '▼' });
54 | }
55 |
56 | this.drpList = this.mainEl.createEl('ul', { cls: 'pg-dropdown' });
57 | dropDownOptions.forEach((option) => {
58 | const item = this.drpList.createEl('li', {
59 | cls: 'pg-dropdown-item',
60 | });
61 | this.setElementTextOrIcon(item, option.label, option.icon);
62 |
63 | item.onClickEvent(() => {
64 | option.func();
65 | });
66 | });
67 | }
68 |
69 | private setElementTextOrIcon(
70 | element: HTMLElement,
71 | label: string,
72 | icon?: string
73 | ) {
74 | if (icon) {
75 | setIcon(element.createSpan(), icon);
76 | } else {
77 | element.setText(label);
78 | }
79 | }
80 | }
81 |
82 | export interface DropdownOption {
83 | label: string;
84 | func: () => void;
85 | icon?: string;
86 | }
87 |
88 | interface MainLabelOptions {
89 | label: string;
90 | icon?: string;
91 | }
92 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/HtmlComponent.ts:
--------------------------------------------------------------------------------
1 | export default abstract class HtmlComponent {
2 | mainEl?: HTMLElement;
3 |
4 | options: OptionsType;
5 |
6 | protected parentEl: HTMLElement;
7 | protected constructor(parentElement: HTMLElement, options?: OptionsType) {
8 | this.parentEl = parentElement;
9 | if (options) {
10 | this.options = options;
11 | }
12 | }
13 |
14 | update(options?: OptionsType): void {
15 | if (options) {
16 | this.options = options;
17 | }
18 | this.render();
19 | }
20 |
21 | render() {
22 | if (!this.mainEl) {
23 | this.generateComponent();
24 | } else {
25 | this.clear();
26 | this.generateContent();
27 | }
28 | }
29 |
30 | protected generateComponent(): void {
31 | this.generateContainer();
32 | this.generateContent();
33 | }
34 |
35 | protected abstract generateContainer(): void;
36 |
37 | protected abstract generateContent(): void;
38 |
39 | protected clear(): void {
40 | if (this.mainEl) {
41 | this.mainEl.textContent = '';
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/RemovableChip.ts:
--------------------------------------------------------------------------------
1 | import { setIcon } from 'obsidian';
2 | import HtmlComponent from './HtmlComponent';
3 |
4 | interface RemovableChipOptions {
5 | label: string;
6 | onClose: () => void;
7 | }
8 | export default class RemovableChip extends HtmlComponent {
9 | constructor(parentEl: HTMLElement, options: RemovableChipOptions) {
10 | super(parentEl);
11 | this.parentEl = parentEl;
12 | this.options = options;
13 |
14 | this.render();
15 | }
16 |
17 | protected generateContent(): void {
18 | if (!this.mainEl) {
19 | return;
20 | }
21 |
22 | this.mainEl.createSpan({ text: this.options.label });
23 | const closeBtn = this.mainEl.createDiv({ cls: 'pg-chip-close-btn' });
24 | setIcon(closeBtn, 'x', 1);
25 | closeBtn.onClickEvent(() => {
26 | this.options.onClose();
27 | this.mainEl?.remove();
28 | });
29 | }
30 |
31 | protected generateContainer(): void {
32 | this.mainEl = this.parentEl.createDiv({ cls: 'pg-chip' });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/ReorderableList.ts:
--------------------------------------------------------------------------------
1 | import SettingsList from './SettingsList';
2 | import { setIcon, Setting } from 'obsidian';
3 |
4 | export default abstract class ReorderableList<
5 | ItemType,
6 | OptionsType extends { items: ItemType[] }
7 | > extends SettingsList {
8 | moveItemUp(item: ItemType): void {
9 | const currentIndex = this.findIndexInItems(item);
10 |
11 | if (currentIndex < this.options.items.length - 1 && currentIndex > -1) {
12 | this.options.items[currentIndex] =
13 | this.options.items[currentIndex + 1];
14 | this.options.items[currentIndex + 1] = item;
15 | }
16 | this.render();
17 | }
18 |
19 | moveItemDown(item: ItemType): void {
20 | const currentIndex = this.findIndexInItems(item);
21 |
22 | if (currentIndex > 0) {
23 | this.options.items[currentIndex] =
24 | this.options.items[currentIndex - 1];
25 | this.options.items[currentIndex - 1] = item;
26 | }
27 | this.render();
28 | }
29 |
30 | protected findIndexInItems(item: ItemType): number {
31 | return this.options.items.findIndex((listItem) => listItem === item);
32 | }
33 |
34 | generateListItem(listEl: HTMLElement, item: ItemType): Setting {
35 | const itemEl = new Setting(listEl)
36 | .addButton((btn) => {
37 | setIcon(btn.buttonEl, 'arrow-down');
38 | btn.onClick(() => {
39 | this.moveItemUp(item);
40 | });
41 | })
42 | .addButton((btn) => {
43 | setIcon(btn.buttonEl, 'arrow-up');
44 | btn.onClick(() => {
45 | this.moveItemDown(item);
46 | });
47 | });
48 |
49 | return itemEl;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/SettingsList.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from 'obsidian';
2 | import HtmlComponent from './HtmlComponent';
3 |
4 | export default abstract class SettingsList<
5 | ItemType,
6 | OptionsType extends { items: ItemType[] }
7 | > extends HtmlComponent {
8 | constructor(parentEL: HTMLElement, options: OptionsType) {
9 | super(parentEL, options);
10 | this.generateComponent();
11 | }
12 |
13 | protected generateComponent() {
14 | this.generateContainer();
15 | this.generateContent();
16 | }
17 |
18 | abstract generateListItem(listEl: HTMLElement, item: ItemType): Setting;
19 |
20 | protected generateContainer(): void {
21 | this.mainEl = this.parentEl.createEl('div');
22 | this.mainEl.addClass('pg-settings-list');
23 | }
24 |
25 | protected generateContent() {
26 | if (!this.mainEl) {
27 | return;
28 | }
29 |
30 | const container = this.mainEl;
31 |
32 | this.options.items.forEach((item) => {
33 | this.generateListItem(container, item);
34 | });
35 | }
36 |
37 | protected clear() {
38 | if (this.mainEl && this.mainEl.hasClass('pg-settings-list')) {
39 | this.mainEl.textContent = '';
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/TabComponent.ts:
--------------------------------------------------------------------------------
1 | export default interface TabComponent {
2 | title: string;
3 |
4 | content: HTMLElement;
5 | }
6 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/TabGroupComponent.ts:
--------------------------------------------------------------------------------
1 | import HtmlComponent from './HtmlComponent';
2 | import TabComponent from './TabComponent';
3 |
4 | export default class TabGroupComponent extends HtmlComponent {
5 | activeTab: HTMLElement;
6 | activeContent: HTMLElement;
7 |
8 | constructor(parentEL: HTMLElement, options: TabGroupOptions) {
9 | super(parentEL, options);
10 | this.generateComponent();
11 | }
12 |
13 | private switchActiveTab(
14 | newActiveTab: HTMLElement,
15 | newActiveContent: HTMLElement
16 | ) {
17 | this.activeTab?.removeClass('is-active');
18 | this.activeContent?.removeClass('is-active');
19 |
20 | this.activeTab = newActiveTab;
21 | this.activeContent = newActiveContent;
22 |
23 | this.activeTab?.addClass('is-active');
24 | this.activeContent?.addClass('is-active');
25 | }
26 |
27 | protected generateContent(): void {
28 | if (!this.mainEl) {
29 | return;
30 | }
31 |
32 | const tabContainer = this.mainEl.createDiv({ cls: 'pg-tabs' });
33 |
34 | const contentContainer = this.mainEl.createDiv();
35 |
36 | this.options.tabs.forEach((tab, index) => {
37 | const tabEl = tabContainer?.createDiv({ cls: 'pg-tab' });
38 | tabEl.createSpan({ text: tab.title });
39 |
40 | const contentEl = contentContainer.appendChild(tab.content);
41 | contentEl.addClass('pg-tabbed-content');
42 | tabEl.onClickEvent(() => this.switchActiveTab(tabEl, contentEl));
43 |
44 | if (index === 0) {
45 | this.switchActiveTab(tabEl, contentEl);
46 | }
47 | });
48 | }
49 |
50 | protected generateContainer(): void {
51 | this.mainEl = this.parentEl.createDiv();
52 | }
53 | }
54 |
55 | export interface TabGroupOptions {
56 | tabs: TabComponent[];
57 | }
58 |
--------------------------------------------------------------------------------
/src/Components/BaseComponents/TogglableList.ts:
--------------------------------------------------------------------------------
1 | import SettingsList from './SettingsList';
2 | import { ButtonComponent, Setting } from 'obsidian';
3 | import { Named } from '../../Utils/Types';
4 |
5 | export interface ToggleListOptions {
6 | items: ItemType[];
7 |
8 | /**
9 | * The function needs to be a reference or a '() =>' function to make sure that 'this.' references the parent object if needed
10 | * @param item
11 | */
12 | toggle: (item: ItemType) => void;
13 |
14 | /**
15 | * The function needs to be a reference or a '() =>' function to make sure that 'this.' references the parent object if needed
16 | * @param item
17 | */
18 | getToggleState: (item: ItemType) => boolean;
19 | }
20 |
21 | export default class TogglableList
22 | extends SettingsList>
23 | implements IToggleableList
24 | {
25 | generateListItem(listEl: HTMLElement, item: ItemType): Setting {
26 | const settingItem = new Setting(listEl);
27 |
28 | settingItem.setName(item.name);
29 |
30 | this.addToggleButton(settingItem, item);
31 |
32 | return settingItem;
33 | }
34 |
35 | addToggleButton(setting: Setting, item: ItemType): void {
36 | setting.addButton((btn) => {
37 | const currentState = this.getItemState(item);
38 | this.setToggleIcon(btn, currentState);
39 | btn.onClick(() => {
40 | this.toggleItem(item);
41 | this.setToggleIcon(btn, this.getItemState(item));
42 | });
43 | });
44 | }
45 |
46 | toggleItem(item: ItemType): void {
47 | this.options.toggle(item);
48 | }
49 |
50 | getItemState(item: ItemType): boolean {
51 | return this.options.getToggleState(item);
52 | }
53 |
54 | setToggleIcon(btn: ButtonComponent, value: boolean): void {
55 | btn.setIcon(value ? 'check-circle' : 'circle');
56 | }
57 | }
58 |
59 | export interface IToggleableList {
60 | toggleItem(item: ItemType, value: boolean): void;
61 |
62 | addToggleButton(setting: Setting, item: ItemType): void;
63 |
64 | getItemState(item: ItemType): boolean;
65 |
66 | setToggleIcon(btn: ButtonComponent, value: boolean): void;
67 | }
68 |
--------------------------------------------------------------------------------
/src/Components/DescriptionsList.ts:
--------------------------------------------------------------------------------
1 | import SettingsList from './BaseComponents/SettingsList';
2 | import { Setting } from 'obsidian';
3 | import { Named } from '../Utils/Types';
4 |
5 | export default class DescriptionsList<
6 | ItemType extends Named
7 | > extends SettingsList<
8 | ItemAndDescription,
9 | { items: ItemAndDescription[] }
10 | > {
11 | constructor(
12 | parentEL: HTMLElement,
13 | options: { items: ItemAndDescription[] }
14 | ) {
15 | super(parentEL, options);
16 | }
17 |
18 | generateListItem(
19 | listEl: HTMLElement,
20 | plugin: ItemAndDescription
21 | ): Setting {
22 | const item = new Setting(listEl).setName(plugin.item.name);
23 | if (plugin.description) {
24 | item.setDesc(plugin.description);
25 | }
26 | return item;
27 | }
28 | }
29 |
30 | export interface ItemAndDescription {
31 | item: ItemType;
32 | description?: string;
33 | }
34 |
--------------------------------------------------------------------------------
/src/Components/DeviceSelectionModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting } from 'obsidian';
2 | import Manager from '../Managers/Manager';
3 |
4 | export default class DeviceSelectionModal extends Modal {
5 | headerText: string;
6 |
7 | cancelText: string;
8 |
9 | confirmText: string;
10 |
11 | eventTarget: EventTarget = new EventTarget();
12 |
13 | onConfirm: CustomEvent = new CustomEvent('onConfirm', {
14 | detail: {
15 | devices: [],
16 | },
17 | });
18 |
19 | selectedDevices: Set = new Set();
20 |
21 | constructor(
22 | app: App,
23 | onConfirmSelectionListener: EventListener,
24 | selectedDevices?: string[]
25 | ) {
26 | super(app);
27 | this.headerText = 'New device detected please enter a unique name.';
28 | this.cancelText = 'Cancel';
29 | this.confirmText = 'Confirm';
30 |
31 | this.selectedDevices = new Set(selectedDevices);
32 |
33 | if (this.selectedDevices?.size > 0) {
34 | this.onConfirm.detail.devices = Array.from(
35 | this.selectedDevices.values()
36 | );
37 | }
38 |
39 | this.eventTarget.addEventListener(
40 | this.onConfirm.type,
41 | onConfirmSelectionListener
42 | );
43 | }
44 |
45 | onOpen() {
46 | const { contentEl } = this;
47 |
48 | contentEl.empty();
49 |
50 | contentEl.createEl('h2', { text: this.headerText });
51 |
52 | contentEl.createEl('h6', { text: 'Existing Devices' });
53 |
54 | Manager.getInstance().devices.forEach((device) => {
55 | new Setting(contentEl).setName(device).addButton((tgl) => {
56 | tgl.setIcon(
57 | this.selectedDevices.has(device) ? 'check-circle' : 'circle'
58 | ).onClick(() => {
59 | if (this.selectedDevices.has(device)) {
60 | this.selectedDevices.delete(device);
61 | tgl.setIcon('circle');
62 | } else {
63 | this.selectedDevices.add(device);
64 | tgl.setIcon('check-circle');
65 | }
66 | });
67 | });
68 | });
69 |
70 | new Setting(contentEl)
71 | .addButton((btn) => {
72 | btn.setButtonText(this.cancelText);
73 | btn.onClick(() => this.close());
74 | })
75 | .addButton((btn) => {
76 | btn.setButtonText(this.confirmText);
77 | btn.onClick(() => {
78 | this.onConfirm.detail.devices = Array.from(
79 | this.selectedDevices.values()
80 | );
81 | this.eventTarget.dispatchEvent(this.onConfirm);
82 | this.close();
83 | });
84 | });
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Components/EditPluginList.ts:
--------------------------------------------------------------------------------
1 | import DescriptionsList, { ItemAndDescription } from './DescriptionsList';
2 | import { Setting } from 'obsidian';
3 | import PluginModal from './Modals/PluginModal';
4 | import Manager from '../Managers/Manager';
5 | import { PgPlugin } from '../DataStructures/PgPlugin';
6 |
7 | export default class EditPluginList extends DescriptionsList {
8 | onEditFinished?: () => void;
9 |
10 | constructor(
11 | parentEL: HTMLElement,
12 | options: { items: ItemAndDescription[] },
13 | onEditFinished?: () => void
14 | ) {
15 | super(parentEL, options);
16 | this.onEditFinished = onEditFinished;
17 | }
18 |
19 | override generateListItem(
20 | listEl: HTMLElement,
21 | plugin: ItemAndDescription
22 | ): Setting {
23 | const item = super.generateListItem(listEl, plugin);
24 |
25 | item.addButton((btn) => {
26 | btn.setIcon('pencil');
27 | btn.onClick(() => {
28 | new PluginModal(
29 | Manager.getInstance().pluginInstance.app,
30 | plugin.item,
31 | () => {
32 | if (this.onEditFinished) {
33 | this.onEditFinished();
34 | }
35 | }
36 | ).open();
37 | });
38 | });
39 |
40 | return item;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Components/FilteredGroupsList.ts:
--------------------------------------------------------------------------------
1 | import { PluginGroup } from '../DataStructures/PluginGroup';
2 | import RemovableChip from './BaseComponents/RemovableChip';
3 |
4 | export default class FilteredGroupsList {
5 | private parentEl: HTMLElement;
6 |
7 | listEL: HTMLElement;
8 |
9 | private groups: Map;
10 | private onChipClosed: () => void;
11 |
12 | constructor(
13 | parentEl: HTMLElement,
14 | groups: Map,
15 | onChipClosed: () => void
16 | ) {
17 | this.groups = groups;
18 | this.parentEl = parentEl;
19 | this.onChipClosed = onChipClosed;
20 |
21 | this.render();
22 | }
23 |
24 | public update(groups: Map) {
25 | this.listEL.remove();
26 | this.groups = groups;
27 | this.render();
28 | }
29 |
30 | private render() {
31 | this.listEL = this.parentEl.createDiv({ cls: 'pg-group-filter-list' });
32 | this.listEL.createSpan({ text: 'Filters:' });
33 |
34 | this.groups.forEach((group) => {
35 | new RemovableChip(this.listEL, {
36 | label: group.name,
37 | onClose: () => {
38 | this.groups.delete(group.id);
39 | this.onChipClosed();
40 | },
41 | });
42 | });
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Components/Modals/GroupEditModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting } from 'obsidian';
2 | import { generateGroupID } from '../../Utils/Utilities';
3 | import ConfirmationPopupModal from '../BaseComponents/ConfirmationPopupModal';
4 | import { PluginGroup } from '../../DataStructures/PluginGroup';
5 | import GroupEditPluginsTab from '../../GroupEditModal/GroupEditPluginsTab';
6 | import GroupEditGroupsTab from '../../GroupEditModal/GroupEditGroupsTab';
7 | import GroupEditGeneralTab from '../../GroupEditModal/GroupEditGeneralTab';
8 | import Manager from '../../Managers/Manager';
9 | import CommandManager from '../../Managers/CommandManager';
10 | import TabGroupComponent from '../BaseComponents/TabGroupComponent';
11 | import GroupSettings from '../Settings/GroupSettings';
12 |
13 | export default class GroupEditModal extends Modal {
14 | groupToEdit: PluginGroup;
15 |
16 | groupToEditCache: string;
17 | discardChanges = true;
18 |
19 | groupSettings: GroupSettings;
20 |
21 | constructor(app: App, settingsTab: GroupSettings, group: PluginGroup) {
22 | super(app);
23 | this.groupSettings = settingsTab;
24 | this.groupToEdit = group;
25 | this.groupToEditCache = JSON.stringify(group);
26 | }
27 |
28 | onOpen() {
29 | const { modalEl } = this;
30 |
31 | modalEl.empty();
32 |
33 | const contentEl = modalEl.createDiv();
34 |
35 | const nameSettingNameEl = new Setting(contentEl)
36 | .addText((txt) => {
37 | txt.setValue(this.groupToEdit.name);
38 | txt.onChange((val) => {
39 | this.groupToEdit.name = val;
40 | nameSettingNameEl.setText(
41 | 'Editing "' + this.groupToEdit.name + '"'
42 | );
43 | });
44 | })
45 | .nameEl.createEl('h2', {
46 | text: 'Editing "' + this.groupToEdit.name + '"',
47 | });
48 |
49 | const tabGroup: TabGroupComponent = new TabGroupComponent(modalEl, {
50 | tabs: [
51 | {
52 | title: 'General',
53 | content: new GroupEditGeneralTab(
54 | this.groupToEdit,
55 | contentEl
56 | ).containerEl,
57 | },
58 | {
59 | title: 'Plugins',
60 | content:
61 | new GroupEditPluginsTab(contentEl, {
62 | group: this.groupToEdit,
63 | }).mainEl ??
64 | modalEl.createSpan(
65 | 'Plugins Not Loaded, please contact Dev.'
66 | ),
67 | },
68 | {
69 | title: 'Groups',
70 | content: new GroupEditGroupsTab(this.groupToEdit, contentEl)
71 | .containerEl,
72 | },
73 | ],
74 | });
75 |
76 | this.generateFooter(modalEl);
77 | }
78 |
79 | private generateFooter(parentElement: HTMLElement) {
80 | const footer = parentElement.createEl('div');
81 |
82 | footer.addClass('pg-edit-modal-footer');
83 |
84 | new Setting(footer)
85 | .addButton((btn) => {
86 | btn.setButtonText('Delete');
87 | btn.onClick(() =>
88 | new ConfirmationPopupModal(
89 | this.app,
90 | 'You are about to delete: ' + this.groupToEdit.name,
91 | void 0,
92 | 'Delete',
93 | () => this.deleteGroup()
94 | ).open()
95 | );
96 | })
97 | .addButton((btn) => {
98 | btn.setButtonText('Cancel');
99 | btn.onClick(() => this.close());
100 | })
101 | .addButton((btn) => {
102 | btn.setButtonText('Save');
103 | btn.onClick(() => this.saveChanges());
104 | })
105 | .addExtraButton((btn) => {
106 | btn.setIcon('copy')
107 | .setTooltip('Duplicate this group')
108 | .onClick(() => this.duplicate());
109 | })
110 | .settingEl.addClass('modal-footer');
111 | }
112 |
113 | onClose() {
114 | const { contentEl } = this;
115 | contentEl.empty();
116 |
117 | if (
118 | Manager.getInstance().groupsMap.has(this.groupToEdit.id) &&
119 | this.discardChanges
120 | ) {
121 | Object.assign(this.groupToEdit, JSON.parse(this.groupToEditCache));
122 | }
123 | }
124 |
125 | async saveChanges() {
126 | this.discardChanges = false;
127 | if (Manager.getInstance().groupsMap.has(this.groupToEdit.id)) {
128 | await this.editGroup(this.groupToEdit);
129 | } else {
130 | await this.addGroup(this.groupToEdit);
131 | }
132 | }
133 |
134 | async duplicate() {
135 | const duplicateGroup = new PluginGroup(this.groupToEdit);
136 | const groupMap = Manager.getInstance().groupsMap;
137 |
138 | if (!groupMap) {
139 | return;
140 | }
141 | duplicateGroup.name += '-Duplicate';
142 | const genId = generateGroupID(duplicateGroup.name);
143 |
144 | if (!genId) {
145 | return;
146 | }
147 | duplicateGroup.id = genId;
148 |
149 | await this.addGroup(duplicateGroup);
150 | }
151 |
152 | async addGroup(group: PluginGroup) {
153 | Manager.getInstance().groupsMap.set(group.id, group);
154 |
155 | CommandManager.getInstance().AddGroupCommands(group.id);
156 |
157 | await this.persistChangesAndClose();
158 | }
159 |
160 | async editGroup(group: PluginGroup) {
161 | Manager.getInstance().groupsMap.set(group.id, group);
162 | CommandManager.getInstance().updateCommand(group.id);
163 | await this.persistChangesAndClose();
164 | }
165 |
166 | async persistChangesAndClose() {
167 | await Manager.getInstance().saveSettings();
168 | this.groupSettings.render();
169 | this.close();
170 | }
171 |
172 | async deleteGroup() {
173 | Manager.getInstance().groupsMap.delete(this.groupToEdit.id);
174 | await Manager.getInstance().saveSettings();
175 | this.groupSettings.render();
176 | this.close();
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/Components/Modals/GroupSettingsMenu.ts:
--------------------------------------------------------------------------------
1 | import GroupSettings from '../Settings/GroupSettings';
2 | import HtmlComponent from '../BaseComponents/HtmlComponent';
3 |
4 | interface GroupSettingMenuOptions {}
5 |
6 | export default class GroupSettingsMenu extends HtmlComponent {
7 | constructor(parentEl: HTMLElement, options: GroupSettingMenuOptions) {
8 | super(parentEl, options);
9 | this.generateComponent();
10 | }
11 |
12 | protected generateContainer(): void {
13 | this.mainEl = this.parentEl.createDiv({ cls: 'pg-settings-window' });
14 | }
15 |
16 | protected generateContent(): void {
17 | if (!this.mainEl) {
18 | return;
19 | }
20 |
21 | new GroupSettings(this.mainEl, { maxListHeight: 340 });
22 |
23 | this.updatePosition();
24 | }
25 |
26 | public updatePosition() {
27 | if (!this.mainEl) {
28 | return;
29 | }
30 |
31 | this.mainEl.style.transform = 'translate(0px, 0px)';
32 |
33 | let xOffset = -this.mainEl.getBoundingClientRect().width / 2;
34 | const yOffset = -this.mainEl.clientHeight / 2 - 30;
35 |
36 | const diff =
37 | window.innerWidth - this.mainEl.getBoundingClientRect().right - 16;
38 |
39 | if (diff < 0) {
40 | xOffset = diff;
41 | }
42 |
43 | this.mainEl.style.transform =
44 | 'translate(' + xOffset + 'px, ' + yOffset + 'px)';
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Components/Modals/PluginModal.ts:
--------------------------------------------------------------------------------
1 | import { App, Modal, Setting } from 'obsidian';
2 | import { PgPlugin } from '../../DataStructures/PgPlugin';
3 | import Manager from '../../Managers/Manager';
4 | import TogglableList from '../BaseComponents/TogglableList';
5 | import { PluginGroup } from '../../DataStructures/PluginGroup';
6 |
7 | export default class PluginModal extends Modal {
8 | private pluginToEdit: PgPlugin;
9 |
10 | private memberGroupIds: string[];
11 |
12 | private memberGroupsIdsCache: string[];
13 |
14 | private onCloseActions?: () => void;
15 |
16 | discardChanges = true;
17 |
18 | constructor(app: App, pluginToEdit: PgPlugin, onCloseActions?: () => void) {
19 | super(app);
20 | this.pluginToEdit = pluginToEdit;
21 | this.onCloseActions = onCloseActions;
22 |
23 | this.memberGroupIds = Manager.getInstance()
24 | .getGroupsOfPlugin(pluginToEdit.id)
25 | .map((g) => g.id);
26 |
27 | this.memberGroupsIdsCache = this.memberGroupIds.map((id) => id);
28 | }
29 |
30 | onOpen() {
31 | const { modalEl } = this;
32 |
33 | modalEl.empty();
34 |
35 | const contentEl = modalEl.createDiv();
36 |
37 | const title = contentEl.createEl('h4');
38 | title.textContent = 'Editing Plugin: ';
39 | title.createEl('b').textContent = this.pluginToEdit.name;
40 |
41 | if (this.memberGroupIds) {
42 | const groupsList = new TogglableList(contentEl, {
43 | items: Array.from(Manager.getInstance().groupsMap.values()),
44 | getToggleState: (item) => {
45 | return this.getToggleState(item);
46 | },
47 | toggle: (item) => {
48 | this.toggleItem(item);
49 | },
50 | });
51 | }
52 | this.generateFooter(contentEl);
53 | }
54 |
55 | toggleItem(item: PluginGroup) {
56 | if (!this.memberGroupIds.contains(item.id)) {
57 | this.memberGroupIds.push(item.id);
58 | } else {
59 | this.memberGroupIds.remove(item.id);
60 | }
61 | }
62 |
63 | getToggleState(item: PluginGroup): boolean {
64 | return this.memberGroupIds && this.memberGroupIds.contains(item.id);
65 | }
66 |
67 | onClose() {
68 | const { contentEl } = this;
69 |
70 | if (this.onCloseActions) {
71 | this.onCloseActions();
72 | }
73 |
74 | contentEl.empty();
75 | }
76 |
77 | private generateFooter(parentElement: HTMLElement) {
78 | const footer = parentElement.createEl('div');
79 |
80 | footer.addClass('pg-edit-modal-footer');
81 |
82 | new Setting(footer)
83 | .addButton((btn) => {
84 | btn.setButtonText('Cancel');
85 | btn.onClick(() => this.close());
86 | })
87 | .addButton((btn) => {
88 | btn.setButtonText('Save');
89 | btn.onClick(() => this.saveChanges());
90 | })
91 | .settingEl.addClass('modal-footer');
92 | }
93 |
94 | private async saveChanges() {
95 | const removedGroupIds = this.memberGroupsIdsCache.filter(
96 | (id) => !this.memberGroupIds.includes(id)
97 | );
98 |
99 | removedGroupIds.forEach((id) =>
100 | Manager.getInstance()
101 | .groupsMap.get(id)
102 | ?.removePlugin(this.pluginToEdit)
103 | );
104 |
105 | const addedGroupIds = this.memberGroupIds.filter(
106 | (id) => !this.memberGroupsIdsCache.includes(id)
107 | );
108 |
109 | addedGroupIds.forEach((id) => {
110 | Manager.getInstance()
111 | .groupsMap.get(id)
112 | ?.addPlugin(this.pluginToEdit);
113 | });
114 |
115 | await Manager.getInstance().saveSettings();
116 |
117 | this.close();
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/Components/PluginListTogglable.ts:
--------------------------------------------------------------------------------
1 | import { PgPlugin } from '../DataStructures/PgPlugin';
2 | import { ButtonComponent, Setting } from 'obsidian';
3 | import { PluginGroup } from '../DataStructures/PluginGroup';
4 |
5 | export default class PluginListTogglable {
6 | pluginListEl: HTMLElement;
7 |
8 | private pluginListTarget: EventTarget = new EventTarget();
9 |
10 | private plugins: PgPlugin[];
11 |
12 | private ownerGroup?: PluginGroup;
13 |
14 | private parentEL: HTMLElement;
15 |
16 | constructor(
17 | parentEL: HTMLElement,
18 | pluginsToDisplay: PgPlugin[],
19 | actionOption?: ActionOption
20 | ) {
21 | this.plugins = pluginsToDisplay;
22 | this.parentEL = parentEL;
23 |
24 | if (actionOption) {
25 | this.ownerGroup = actionOption?.group;
26 |
27 | this.pluginListTarget.addEventListener(
28 | 'listToggleClicked',
29 | (evt: CustomEvent) => {
30 | actionOption.onClickAction(evt.detail);
31 | }
32 | );
33 | }
34 | this.generateList();
35 | }
36 |
37 | public updateList(pluginsToDisplay: PgPlugin[]) {
38 | this.plugins = pluginsToDisplay;
39 | this.render();
40 | }
41 |
42 | public render() {
43 | this.pluginListEl.remove();
44 | this.generateList();
45 | }
46 |
47 | private generateList() {
48 | this.pluginListEl = this.parentEL.createEl('div');
49 | this.pluginListEl.addClass('pg-settings-list');
50 |
51 | this.plugins.forEach((plugin) => {
52 | const setting = new Setting(this.pluginListEl).setName(plugin.name);
53 | if (this.ownerGroup) {
54 | const btn: ButtonComponent = new ButtonComponent(
55 | setting.settingEl
56 | );
57 | this.setIconForPluginBtn(btn, plugin.id);
58 | btn.onClick(() => {
59 | this.pluginListTarget.dispatchEvent(
60 | new CustomEvent('listToggleClicked', { detail: plugin })
61 | );
62 | this.setIconForPluginBtn(btn, plugin.id);
63 | });
64 | }
65 | });
66 | }
67 |
68 | private setIconForPluginBtn(btn: ButtonComponent, pluginId: string) {
69 | if (!this.ownerGroup) {
70 | return;
71 | }
72 |
73 | btn.setIcon(
74 | this.ownerGroup.plugins.map((p) => p.id).contains(pluginId)
75 | ? 'check-circle'
76 | : 'circle'
77 | );
78 | }
79 | }
80 |
81 | interface ActionOption {
82 | group: PluginGroup;
83 | onClickAction: (plugin: PgPlugin) => void;
84 | }
85 |
--------------------------------------------------------------------------------
/src/Components/ReorderablePluginList.ts:
--------------------------------------------------------------------------------
1 | import ReorderableList from './BaseComponents/ReorderableList';
2 | import { Setting } from 'obsidian';
3 | import { PgPlugin } from '../DataStructures/PgPlugin';
4 |
5 | export default class ReorderablePluginList extends ReorderableList<
6 | PgPlugin,
7 | { items: PgPlugin[] }
8 | > {
9 | generateListItem(listEl: HTMLElement, item: PgPlugin): Setting {
10 | const itemEl = super.generateListItem(listEl, item).setName(item.name);
11 |
12 | return itemEl;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/Components/Settings/AdvancedSettings.ts:
--------------------------------------------------------------------------------
1 | import HtmlComponent from '../BaseComponents/HtmlComponent';
2 | import { Setting, TextComponent } from 'obsidian';
3 | import Manager from '../../Managers/Manager';
4 | import { makeCollapsible } from '../../Utils/Utilities';
5 |
6 | export interface AdvancedSettingOptions {
7 | collapsible?: boolean;
8 | }
9 |
10 | export default class AdvancedSettings extends HtmlComponent {
11 | newGroupName: string;
12 |
13 | groupNameField: TextComponent;
14 |
15 | constructor(parentEL: HTMLElement, options: AdvancedSettingOptions) {
16 | super(parentEL, options);
17 | this.generateComponent();
18 | }
19 |
20 | protected generateContent(): void {
21 | if (!this.mainEl) {
22 | return;
23 | }
24 |
25 | const header = this.mainEl.createEl('h5', {
26 | text: 'Advanced Settings',
27 | });
28 |
29 | const content = this.mainEl.createDiv();
30 |
31 | if (this.options.collapsible) {
32 | makeCollapsible(header, content);
33 | }
34 |
35 | new Setting(content).setName('Development Logs').addToggle((tgl) => {
36 | tgl.setValue(Manager.getInstance().devLog);
37 | tgl.onChange(async (value) => {
38 | Manager.getInstance().devLog = value;
39 | await Manager.getInstance().saveSettings();
40 | });
41 | });
42 |
43 | new Setting(content).setName('Load Synchronously').addToggle((tgl) => {
44 | tgl.setValue(Manager.getInstance().doLoadSynchronously);
45 | tgl.onChange(async (value) => {
46 | Manager.getInstance().doLoadSynchronously = value;
47 | await Manager.getInstance().saveSettings();
48 | });
49 | });
50 | }
51 |
52 | protected generateContainer(): void {
53 | this.mainEl = this.parentEl.createDiv();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Components/Settings/GroupSettings.ts:
--------------------------------------------------------------------------------
1 | import HtmlComponent from '../BaseComponents/HtmlComponent';
2 | import { Setting, TextComponent } from 'obsidian';
3 | import Manager from '../../Managers/Manager';
4 | import { generateGroupID, makeCollapsible } from '../../Utils/Utilities';
5 | import { PluginGroup } from '../../DataStructures/PluginGroup';
6 | import GroupEditModal from '../Modals/GroupEditModal';
7 |
8 | export interface GroupSettingOptions {
9 | collapsible?: boolean;
10 | startOpened?: boolean;
11 | maxListHeight?: number;
12 | }
13 |
14 | export default class GroupSettings extends HtmlComponent {
15 | newGroupName: string;
16 |
17 | groupNameField: TextComponent;
18 |
19 | constructor(parentEL: HTMLElement, options: GroupSettingOptions) {
20 | super(parentEL, options);
21 | this.generateComponent();
22 | }
23 |
24 | protected generateContent(): void {
25 | if (!this.mainEl) {
26 | return;
27 | }
28 |
29 | const header = this.mainEl.createEl('h5', { text: 'Groups' });
30 |
31 | const content = this.mainEl.createDiv();
32 |
33 | if (this.options.collapsible) {
34 | makeCollapsible(header, content, this.options.startOpened);
35 | }
36 |
37 | let addBtnEl: HTMLButtonElement;
38 |
39 | new Setting(content)
40 | .setName('Add Group')
41 | .addText((text) => {
42 | this.groupNameField = text;
43 | this.groupNameField
44 | .setPlaceholder('Enter group name...')
45 | .setValue(this.newGroupName)
46 | .onChange((val) => {
47 | this.newGroupName = val;
48 | if (addBtnEl) {
49 | val.replace(' ', '').length > 0
50 | ? addBtnEl.removeClass('btn-disabled')
51 | : addBtnEl.addClass('btn-disabled');
52 | }
53 | }).inputEl.onkeydown = async (e) => {
54 | if (e.key === 'Enter') {
55 | await this.addNewGroup();
56 | }
57 | };
58 | })
59 | .addButton((btn) => {
60 | btn.setIcon('plus').onClick(() => this.addNewGroup());
61 | addBtnEl = btn.buttonEl;
62 | addBtnEl.addClass('btn-disabled');
63 | });
64 |
65 | const listContainer = content.createDiv();
66 | listContainer.style.overflow = 'scroll';
67 | listContainer.style.maxHeight =
68 | (this.options.maxListHeight?.toString() ?? '') + 'px';
69 |
70 | this.GenerateGroupList(listContainer);
71 | }
72 |
73 | protected generateContainer(): void {
74 | this.mainEl = this.parentEl.createDiv();
75 | }
76 |
77 | GenerateGroupList(groupParent: HTMLElement) {
78 | Manager.getInstance().groupsMap.forEach((group) => {
79 | const groupSetting = new Setting(groupParent)
80 | .setName(group.name)
81 | .addButton((btn) => {
82 | btn.setButtonText('Enable');
83 | btn.setIcon('power');
84 | btn.onClick(async () => {
85 | await group.enable();
86 | });
87 | group.groupActive()
88 | ? btn.buttonEl.removeClass('btn-disabled')
89 | : btn.buttonEl.addClass('btn-disabled');
90 | })
91 | .addButton((btn) => {
92 | btn.setButtonText('Disable');
93 | btn.setIcon('power-off');
94 | btn.onClick(() => group.disable());
95 | group.groupActive()
96 | ? btn.buttonEl.removeClass('btn-disabled')
97 | : btn.buttonEl.addClass('btn-disabled');
98 | })
99 | .addButton((btn) => {
100 | btn.setIcon('pencil');
101 | btn.onClick(() => this.editGroup(group));
102 | });
103 | if (group.loadAtStartup) {
104 | const descFrag = new DocumentFragment();
105 | const startupEl = descFrag.createEl('span');
106 | startupEl.createEl('b', {
107 | text: 'Startup: ',
108 | });
109 | startupEl.createEl('span', {
110 | text: 'Delayed by ' + group.delay + ' seconds',
111 | });
112 |
113 | if (!group.groupActive()) {
114 | const activeEl = descFrag.createEl('span');
115 | activeEl.createEl('br');
116 | activeEl.createEl('b', { text: 'Inactive: ' });
117 | activeEl.createEl('span', {
118 | text: 'Not enabled for current Device',
119 | });
120 | }
121 |
122 | groupSetting.setDesc(descFrag);
123 | }
124 | });
125 | }
126 |
127 | async addNewGroup() {
128 | const id = generateGroupID(this.newGroupName);
129 |
130 | if (!id) {
131 | console.error(
132 | 'Failed to create Group, please choose a different Name as there have been to many groups with the same name'
133 | );
134 | return;
135 | }
136 |
137 | const newGroup = new PluginGroup({
138 | id: id,
139 | name: this.newGroupName,
140 | });
141 | new GroupEditModal(
142 | Manager.getInstance().pluginInstance.app,
143 | this,
144 | newGroup
145 | ).open();
146 | this.newGroupName = '';
147 | if (this.groupNameField) {
148 | this.groupNameField.setValue('');
149 | }
150 | }
151 |
152 | editGroup(group: PluginGroup) {
153 | new GroupEditModal(
154 | Manager.getInstance().pluginInstance.app,
155 | this,
156 | group
157 | ).open();
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/Components/Settings/PluginsSettings.ts:
--------------------------------------------------------------------------------
1 | import { makeCollapsible } from "src/Utils/Utilities";
2 | import HtmlComponent from "../BaseComponents/HtmlComponent";
3 | import { ExtraButtonComponent, Setting } from "obsidian";
4 | import EditPluginList from "../EditPluginList";
5 | import { PgPlugin } from "src/DataStructures/PgPlugin";
6 | import Manager from "src/Managers/Manager";
7 | import PluginManager from "src/Managers/PluginManager";
8 | import { ItemAndDescription } from "../DescriptionsList";
9 | import { PluginGroup } from "src/DataStructures/PluginGroup";
10 | import DropdownActionButton, { DropdownOption } from "../BaseComponents/DropdownActionButton";
11 | import FilteredGroupsList from "../FilteredGroupsList";
12 |
13 | export interface PluginSettingOptions {
14 | collapsible?: boolean;
15 | startOpened?: boolean;
16 | maxListHeight?: number;
17 | }
18 |
19 | export default class PluginSettings extends HtmlComponent {
20 |
21 | private readonly FilterModes = {
22 | notInGroup: 'Not in the Group(s)',
23 | inGroup: 'In the Groups',
24 | };
25 |
26 | activeFilterMode = this.FilterModes.inGroup;
27 |
28 | content: HTMLElement;
29 |
30 | private filteredPlugins: ItemAndDescription[];
31 |
32 | searchTerm: string;
33 |
34 | private pluginsList: EditPluginList;
35 |
36 | private filteredGroups: Map = new Map<
37 | string,
38 | PluginGroup
39 | >();
40 |
41 |
42 | constructor(parentEL: HTMLElement, options: PluginSettingOptions) {
43 | super(parentEL, options);
44 | this.generateComponent();
45 | }
46 |
47 | protected generateContainer(): void {
48 | this.mainEl = this.parentEl.createDiv();
49 | }
50 |
51 | protected generateContent(): void {
52 | if (!this.mainEl) {
53 | return;
54 | }
55 |
56 | const header = this.mainEl.createEl('h5', { text: 'Plugins' });
57 |
58 | this.content = this.mainEl.createDiv();
59 |
60 | makeCollapsible(header, this.content);
61 |
62 | if (this.options.collapsible) {
63 | makeCollapsible(header, this.content, this.options.startOpened);
64 | }
65 |
66 | this.FilterAndSort();
67 |
68 | this.GenerateFilterSection(this.content);
69 |
70 | const listContainer = this.content.createDiv();
71 | listContainer.style.overflow = 'scroll';
72 | listContainer.style.maxHeight =
73 | (this.options.maxListHeight?.toString() ?? '') + 'px';
74 |
75 |
76 | this.GeneratePluginsList(listContainer);
77 | }
78 |
79 | GeneratePluginsList(parentEl: HTMLElement) {
80 |
81 | const refresh = new ExtraButtonComponent(this.content);
82 | refresh.setIcon('refresh-cw');
83 | refresh.setTooltip(
84 | 'Refresh list for changes to the plugins and assigned groups.'
85 | );
86 |
87 | this.pluginsList = new EditPluginList(
88 | this.content,
89 | {
90 | items: this.filteredPlugins,
91 | },
92 | () => {
93 | this.pluginsList.update({
94 | items: this.filteredPlugins,
95 | });
96 | }
97 | );
98 |
99 | refresh.onClick(() => {
100 | this.pluginsList.update({
101 | items: this.filteredPlugins,
102 | });
103 | });
104 | }
105 |
106 | private getPluginsWithGroupsAsDescription(): ItemAndDescription[] {
107 | return PluginManager.getAllAvailablePlugins().map((plugin) => {
108 | const groups = Manager.getInstance().getGroupsOfPlugin(plugin.id);
109 |
110 | return {
111 | item: plugin,
112 | description: groups.map((group) => group.name).join(', '),
113 | };
114 | });
115 | }
116 |
117 | // ------------------------------------------------------------------------
118 |
119 | private GenerateFilterSection(parentEl: HTMLElement): HTMLElement {
120 | const filterSection = parentEl.createDiv();
121 | new Setting(filterSection).setName('Search').addText((txt) => {
122 | txt.setPlaceholder('Search for Plugin...');
123 | txt.onChange((search) => {
124 | this.searchPlugins(search);
125 | });
126 | });
127 |
128 | const filtersAndSelectionContainer = filterSection.createDiv({
129 | cls: 'pg-plugin-filter-container',
130 | });
131 | const filtersAndSelection = filtersAndSelectionContainer.createDiv({
132 | cls: 'pg-plugin-filter-section',
133 | });
134 | const filters = filtersAndSelection.createDiv();
135 |
136 | const filteredGroupsChips = new FilteredGroupsList(
137 | filtersAndSelectionContainer,
138 | this.filteredGroups,
139 | () => this.OnFilterOrSortUpdated()
140 | );
141 |
142 | const toggleGroupFilter = (group: PluginGroup) => {
143 | this.filteredGroups.has(group.id)
144 | ? this.filteredGroups.delete(group.id)
145 | : this.filteredGroups.set(group.id, group);
146 | };
147 | const updateGroupFilters = () => {
148 | filteredGroupsChips.update(this.filteredGroups);
149 | this.OnFilterOrSortUpdated();
150 | };
151 |
152 | const groupFilterOptions: DropdownOption[] = [
153 | {
154 | label: 'All groups',
155 | func: () => {
156 | if (
157 | this.filteredGroups.size ===
158 | Manager.getInstance().groupsMap.size
159 | ) {
160 | this.filteredGroups.clear();
161 | } else {
162 | this.filteredGroups = new Map(
163 | Manager.getInstance().groupsMap
164 | );
165 | }
166 | updateGroupFilters();
167 | },
168 | },
169 | ];
170 | Manager.getInstance().groupsMap.forEach((group) => {
171 | groupFilterOptions.push({
172 | label: group.name,
173 | func: () => {
174 | toggleGroupFilter(group);
175 | updateGroupFilters();
176 | },
177 | });
178 | });
179 |
180 | new DropdownActionButton(filters, {
181 | mainLabel: {
182 | label: 'Filter Groups',
183 | },
184 | dropDownOptions: groupFilterOptions,
185 | drpIcon: 'filter',
186 | });
187 |
188 | // TODO: Add Filter Mode Functionality: "Not In Selected Groups, In selected Groups"
189 |
190 | const sortButton = new DropdownActionButton(filters, {
191 | mainLabel: {
192 | label: this.activeFilterMode,
193 | },
194 | dropDownOptions: [
195 | {
196 | label: this.FilterModes.notInGroup,
197 | func: () => {
198 | this.onFilterModeChanged(
199 | this.FilterModes.notInGroup,
200 | sortButton
201 | );
202 | },
203 | },
204 | {
205 | label: this.FilterModes.inGroup,
206 | func: () => {
207 | this.onFilterModeChanged(
208 | this.FilterModes.inGroup,
209 | sortButton
210 | );
211 | },
212 | },
213 | ],
214 | minWidth: '80px',
215 | drpIcon: 'sort-desc',
216 | });
217 |
218 | return filterSection;
219 | }
220 |
221 | private onFilterModeChanged(
222 | filterMode: string,
223 | sortButton: DropdownActionButton) {
224 | this.activeFilterMode = filterMode;
225 | sortButton.options.mainLabel.label = filterMode;
226 | sortButton.update();
227 | this.OnFilterOrSortUpdated();
228 | }
229 |
230 | // Cumulative Filter function called from various points that acts depending on filter variables set at object level
231 | private OnFilterOrSortUpdated() {
232 | this.FilterAndSort();
233 |
234 | this.showFilteredPlugins();
235 | }
236 |
237 | private FilterAndSort() {
238 | this.filteredPlugins = this.getPluginsWithGroupsAsDescription();
239 | if (this.searchTerm && this.searchTerm !== '') {
240 | this.filteredPlugins = this.filteredPlugins.filter((p) => p.item.name.toLowerCase().contains(this.searchTerm.toLowerCase())
241 | );
242 | }
243 |
244 | if (this.filteredGroups.size > 0) {
245 | this.filteredPlugins = this.filterPluginsByGroups(
246 | this.filteredPlugins,
247 | this.filteredGroups
248 | );
249 | }
250 |
251 | this.filteredPlugins = this.sortPlugins(
252 | this.filteredPlugins
253 | );
254 | }
255 |
256 | private filterPluginsByGroups(
257 | pluginsToFilter: ItemAndDescription[],
258 | filterGroups: Map
259 | ): ItemAndDescription[] {
260 | const groupsOfPlugins =
261 | Manager.getInstance().mapOfPluginsDirectlyConnectedGroups;
262 |
263 | if(this.activeFilterMode === this.FilterModes.notInGroup){
264 | return this.filterByNotInGroup(pluginsToFilter, groupsOfPlugins, filterGroups);
265 | }
266 |
267 | if(this.activeFilterMode === this.FilterModes.inGroup){
268 | return this.filterByInGroup(pluginsToFilter, groupsOfPlugins, filterGroups);
269 | }
270 |
271 | return pluginsToFilter;
272 | }
273 |
274 |
275 | private filterByInGroup(
276 | pluginsToFilter: ItemAndDescription[],
277 | groupsOfPlugins: Map>,
278 | groupsToFilter: Map) {
279 |
280 | let filteredPlugins: ItemAndDescription[] = pluginsToFilter;
281 | filteredPlugins = filteredPlugins.filter(element => {
282 | return groupsOfPlugins.has(element.item.id);
283 | }
284 | ).filter(element => {
285 | const groupsOfPlugin = groupsOfPlugins.get(element.item.id);
286 | for(const groupToFilter of groupsToFilter.keys()){
287 | if(!groupsOfPlugin?.has(groupToFilter)){
288 | return false;
289 | }
290 | }
291 | return true;
292 | });
293 | return filteredPlugins;
294 | }
295 |
296 | private filterByNotInGroup(pluginsToFilter: ItemAndDescription[], groupsOfPlugins: Map>, groupsToFilter: Map) {
297 | const filteredPlugins: ItemAndDescription[] = pluginsToFilter.filter(element => {
298 | if(!groupsOfPlugins.has(element.item.id)) {
299 | return true;
300 | }
301 |
302 | for (const groupOfPlugin of groupsOfPlugins.get(element.item.id) ?? []) {
303 | if(groupsToFilter.has(groupOfPlugin)){
304 | return false;
305 | }
306 | }
307 |
308 | return true;
309 | });
310 | return filteredPlugins;
311 | }
312 |
313 | private searchPlugins(search: string) {
314 | this.searchTerm = search;
315 | this.OnFilterOrSortUpdated();
316 | this.showFilteredPlugins();
317 | }
318 |
319 | private showFilteredPlugins() {
320 | this.pluginsList.update({ items: this.filteredPlugins});
321 | }
322 |
323 | sortPlugins(plugins: ItemAndDescription[]): ItemAndDescription[] {
324 | if (!plugins || !(typeof plugins[Symbol.iterator] === 'function')) {
325 | return [];
326 | }
327 | const sortedArray = [...plugins];
328 |
329 | return sortedArray.sort((a, b) => a.item.name.localeCompare(b.item.name));
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/src/DataStructures/PgPlugin.ts:
--------------------------------------------------------------------------------
1 | import { PgComponent } from '../Utils/Types';
2 |
3 | export class PgPlugin implements PgComponent {
4 | id: string;
5 | name: string;
6 |
7 | constructor(id: string, name: string) {
8 | this.id = id;
9 | this.name = name;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/DataStructures/PluginGroup.ts:
--------------------------------------------------------------------------------
1 | import { PgComponent } from '../Utils/Types';
2 | import { PgPlugin } from './PgPlugin';
3 | import { Notice } from 'obsidian';
4 | import {
5 | devLog,
6 | getCurrentlyActiveDevice,
7 | groupFromId,
8 | } from '../Utils/Utilities';
9 | import Manager from '../Managers/Manager';
10 | import PluginManager from '../Managers/PluginManager';
11 |
12 | export class PluginGroup implements PluginGroupData {
13 | id: string;
14 | name: string;
15 |
16 | plugins: PgPlugin[];
17 | groupIds: string[];
18 |
19 | generateCommands: boolean;
20 |
21 | loadAtStartup = false;
22 | disableOnStartup = false;
23 | delay = 0;
24 |
25 | assignedDevices?: string[];
26 |
27 | autoAdd?: boolean;
28 | constructor(pgData: PluginGroupData) {
29 | this.id = pgData.id;
30 | this.name = pgData.name;
31 |
32 | this.assignAndLoadPlugins(pgData.plugins);
33 | this.groupIds = pgData.groupIds ?? [];
34 |
35 | this.loadAtStartup = pgData.loadAtStartup ?? false;
36 | this.disableOnStartup = pgData.disableOnStartup ?? false;
37 | this.delay = pgData.delay ?? 2;
38 | this.generateCommands = pgData.generateCommands ?? false;
39 |
40 | this.assignedDevices = pgData.assignedDevices;
41 | this.autoAdd = pgData.autoAdd;
42 | }
43 |
44 | groupActive(): boolean {
45 | if (!this.assignedDevices || this.assignedDevices.length === 0) {
46 | return true;
47 | }
48 |
49 | const activeDevice: string | null = getCurrentlyActiveDevice();
50 | if (!activeDevice) {
51 | return true;
52 | }
53 |
54 | return !!this.assignedDevices?.contains(activeDevice);
55 | }
56 |
57 | assignAndLoadPlugins(plugins?: PgPlugin[]) {
58 | this.plugins = plugins ?? [];
59 | }
60 |
61 | startup() {
62 | if (!this.loadAtStartup) {
63 | return;
64 | }
65 |
66 | if (this.disableOnStartup) {
67 | setTimeout(async () => {
68 | await this.disable();
69 | }, this.delay * 1000);
70 | return;
71 | }
72 |
73 | setTimeout(async () => {
74 | await this.enable();
75 | }, this.delay * 1000);
76 | return;
77 | }
78 |
79 | async enable() {
80 | if (!this.groupActive()) {
81 | return;
82 | }
83 |
84 | const pluginPromises: Promise[] = [];
85 |
86 | for (const plugin of this.plugins) {
87 | if (Manager.getInstance().doLoadSynchronously) {
88 | pluginPromises.push(PluginManager.queuePluginForEnable(plugin));
89 | } else {
90 | await PluginManager.queuePluginForEnable(plugin);
91 | }
92 | }
93 |
94 | await Promise.allSettled(pluginPromises);
95 |
96 | for (const groupId of this.groupIds) {
97 | await groupFromId(groupId)?.enable();
98 | }
99 | if (Manager.getInstance().showNoticeOnGroupLoad) {
100 | const messageString: string = 'Loaded ' + this.name;
101 |
102 | if (Manager.getInstance().showNoticeOnGroupLoad === 'short') {
103 | new Notice(messageString);
104 | } else if (
105 | Manager.getInstance().showNoticeOnGroupLoad === 'normal'
106 | ) {
107 | new Notice(messageString + '\n' + this.getGroupListString());
108 | }
109 | }
110 | }
111 |
112 | disable() {
113 | if (!this.groupActive()) {
114 | return;
115 | }
116 |
117 | this.plugins.forEach((plugin) => {
118 | PluginManager.queueDisablePlugin(plugin);
119 | });
120 |
121 | this.groupIds.forEach((groupId) => {
122 | groupFromId(groupId)?.disable();
123 | });
124 |
125 | if (Manager.getInstance().showNoticeOnGroupLoad !== 'none') {
126 | const messageString: string = 'Disabled ' + this.name;
127 |
128 | if (Manager.getInstance().showNoticeOnGroupLoad === 'short') {
129 | new Notice(messageString);
130 | } else if (
131 | Manager.getInstance().showNoticeOnGroupLoad === 'normal'
132 | ) {
133 | new Notice(messageString + '\n' + this.getGroupListString());
134 | }
135 | }
136 | }
137 |
138 | getGroupListString(): string {
139 | const existingPluginsInGroup =
140 | PluginManager.getAllAvailablePlugins().filter((p) =>
141 | this.plugins.map((p) => p.id).contains(p.id)
142 | );
143 | let messageString = '';
144 | this.plugins && this.plugins.length > 0
145 | ? (messageString +=
146 | '- Plugins:\n' +
147 | existingPluginsInGroup
148 | .map((p) => ' - ' + p.name + '\n')
149 | .join(''))
150 | : (messageString += '');
151 |
152 | this.groupIds && this.groupIds.length > 0
153 | ? (messageString +=
154 | '- Groups:\n' +
155 | this.groupIds
156 | .map((g) => {
157 | const group = groupFromId(g);
158 | if (group && group.groupActive()) {
159 | return ' - ' + group.name + '\n';
160 | }
161 | })
162 | .join(''))
163 | : (messageString += '');
164 |
165 | return messageString;
166 | }
167 |
168 | addPlugin(plugin: PgPlugin): boolean {
169 | if (this.plugins.map((p) => p.id).contains(plugin.id)) return false;
170 |
171 | this.plugins.push(plugin);
172 | return true;
173 | }
174 |
175 | addGroup(group: PluginGroup): boolean {
176 | if (!group.wouldHaveCyclicGroups(this.id)) {
177 | if (this.groupIds.contains(group.id)) return false;
178 |
179 | this.groupIds.push(group.id);
180 | return true;
181 | } else {
182 | new Notice(
183 | "Couldn't add this group, it would create a loop of group activations:\n Group A → Group B → Group A",
184 | 4000
185 | );
186 | }
187 | return false;
188 | }
189 |
190 | removePlugin(plugin: PgPlugin): boolean {
191 | const indexOfPlugin: number = this.plugins
192 | .map((p) => p.id)
193 | .indexOf(plugin.id);
194 |
195 | if (indexOfPlugin === -1) return true;
196 |
197 | return this.plugins.splice(indexOfPlugin, 1).length > 0;
198 | }
199 |
200 | removeGroup(group: PluginGroup): boolean {
201 | const indexOfGroup = this.groupIds.indexOf(group.id);
202 |
203 | if (indexOfGroup === -1) return true;
204 |
205 | return this.groupIds.splice(indexOfGroup, 1).length > 0;
206 | }
207 |
208 | wouldHaveCyclicGroups(idToCheck: string): boolean {
209 | if (this.id === idToCheck) {
210 | return true;
211 | }
212 |
213 | for (let i = 0; i < this.groupIds.length; i++) {
214 | const groupId = this.groupIds[i];
215 | if (groupFromId(groupId)?.wouldHaveCyclicGroups(idToCheck)) {
216 | return true;
217 | }
218 | }
219 | return false;
220 | }
221 |
222 | getAllPluginIdsControlledByGroup(): Set {
223 | let pluginsArr = this.plugins.map((plugin) => plugin.id);
224 |
225 | this.groupIds.forEach((gid) => {
226 | const group = groupFromId(gid);
227 | if (group) {
228 | pluginsArr = [
229 | ...pluginsArr,
230 | ...group.getAllPluginIdsControlledByGroup(),
231 | ];
232 | }
233 | });
234 | return new Set(pluginsArr);
235 | }
236 | }
237 |
238 | interface PluginGroupData extends PgComponent {
239 | plugins?: PgPlugin[];
240 | groupIds?: string[];
241 | generateCommands?: boolean;
242 | loadAtStartup?: boolean;
243 | disableOnStartup?: boolean;
244 | delay?: number;
245 | startupBehaviour?: string;
246 | assignedDevices?: string[];
247 | autoAdd?: boolean;
248 | }
249 |
--------------------------------------------------------------------------------
/src/GroupEditModal/GroupEditGeneralTab.ts:
--------------------------------------------------------------------------------
1 | import { PluginGroup } from '../DataStructures/PluginGroup';
2 | import { Setting } from 'obsidian';
3 | import DeviceSelectionModal from '../Components/DeviceSelectionModal';
4 | import { disableStartupTimeout } from '../Utils/Constants';
5 | import Manager from '../Managers/Manager';
6 |
7 | export default class GroupEditGeneralTab {
8 | containerEl: HTMLElement;
9 |
10 | private groupToEdit: PluginGroup;
11 |
12 | constructor(group: PluginGroup, parentEl: HTMLElement) {
13 | this.groupToEdit = group;
14 |
15 | this.containerEl = this.generateGeneralSettingsSection(parentEl);
16 | }
17 |
18 | private generateGeneralSettingsSection(
19 | contentEl: HTMLElement
20 | ): HTMLElement {
21 | const generalSettingsSection = contentEl.createDiv();
22 |
23 | generalSettingsSection.createEl('h5', { text: 'General' });
24 |
25 | new Setting(generalSettingsSection)
26 | .setName('Commands')
27 | .setDesc('Add Commands to enable/disable this group')
28 | .addToggle((tgl) => {
29 | tgl.setValue(this.groupToEdit.generateCommands);
30 | tgl.onChange(
31 | (value) => (this.groupToEdit.generateCommands = value)
32 | );
33 | });
34 |
35 | new Setting(generalSettingsSection)
36 | .setName('Auto Add')
37 | .setDesc('Automatically add new Plugins to this group')
38 | .addToggle((tgl) => {
39 | tgl.setValue(this.groupToEdit.autoAdd ?? false);
40 | tgl.onChange((value) => (this.groupToEdit.autoAdd = value));
41 | });
42 |
43 | const devicesSetting = new Setting(generalSettingsSection)
44 | .setName('Devices')
45 | .setDesc(this.getDevicesDescription())
46 | .addButton((btn) => {
47 | btn.setIcon('pencil').onClick(() => {
48 | new DeviceSelectionModal(
49 | app,
50 | (evt: CustomEvent) => {
51 | this.groupToEdit.assignedDevices =
52 | evt.detail.devices;
53 | devicesSetting.setDesc(
54 | this.getDevicesDescription()
55 | );
56 | },
57 | this.groupToEdit.assignedDevices
58 | ).open();
59 | });
60 | });
61 |
62 | this.GenerateStartupSettings(generalSettingsSection);
63 |
64 | return generalSettingsSection;
65 | }
66 |
67 | getDevicesDescription() {
68 | let description = 'Active on All devices';
69 |
70 | if (!this.groupToEdit.assignedDevices) {
71 | return description;
72 | }
73 | const arr: string[] = this.groupToEdit.assignedDevices.filter(
74 | (device) => Manager.getInstance().devices.contains(device)
75 | );
76 | if (arr?.length > 0) {
77 | description =
78 | 'Active on: ' +
79 | arr.reduce((acc, curr, i, arr) => {
80 | if (i < 3) {
81 | return acc + ', ' + curr;
82 | } else if (i === arr.length - 1) {
83 | return (
84 | acc +
85 | ', ... and ' +
86 | (i - 2) +
87 | ' other' +
88 | (i - 2 > 1 ? 's' : '')
89 | );
90 | }
91 | return acc;
92 | });
93 | }
94 | return description;
95 | }
96 |
97 | private GenerateStartupSettings(contentEl: HTMLElement) {
98 | const startupParent = contentEl.createEl('div');
99 | startupParent.createEl('h6', { text: 'Startup' });
100 |
101 | // eslint-disable-next-line prefer-const
102 | let delaySetting: Setting;
103 |
104 | // eslint-disable-next-line prefer-const
105 | let behaviourElement: HTMLElement;
106 |
107 | const ChangeOptionVisibility = () => {
108 | if (delaySetting) {
109 | this.groupToEdit.loadAtStartup
110 | ? delaySetting.settingEl.show()
111 | : delaySetting.settingEl.hide();
112 | }
113 | if (behaviourElement) {
114 | this.groupToEdit.loadAtStartup
115 | ? behaviourElement.show()
116 | : behaviourElement.hide();
117 | }
118 | };
119 |
120 | new Setting(startupParent)
121 | .setName('Load on Startup')
122 | .addDropdown((drp) => {
123 | behaviourElement = drp.selectEl;
124 | drp.addOption('enable', 'Enable');
125 | drp.addOption('disable', 'Disable');
126 | drp.setValue(
127 | this.groupToEdit.disableOnStartup ? 'disable' : 'enable'
128 | );
129 | drp.onChange((value) => {
130 | value === 'disable'
131 | ? (this.groupToEdit.disableOnStartup = true)
132 | : (this.groupToEdit.disableOnStartup = false);
133 | });
134 | })
135 | .addToggle((tgl) => {
136 | tgl.onChange((value) => {
137 | this.groupToEdit.loadAtStartup = value;
138 | ChangeOptionVisibility();
139 | });
140 | tgl.setValue(this.groupToEdit.loadAtStartup);
141 | });
142 |
143 | delaySetting = new Setting(startupParent)
144 | .setName('Delay')
145 | .addSlider((slider) => {
146 | slider.setValue(this.groupToEdit.delay);
147 | slider.setLimits(0, disableStartupTimeout / 1000, 1);
148 | slider.onChange((value) => {
149 | this.groupToEdit.delay = value;
150 | delaySetting.setDesc(value.toString());
151 | });
152 | })
153 | .setDesc(this.groupToEdit.delay.toString());
154 |
155 | ChangeOptionVisibility();
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/GroupEditModal/GroupEditGroupsTab.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from 'obsidian';
2 | import { PluginGroup } from '../DataStructures/PluginGroup';
3 | import Manager from '../Managers/Manager';
4 |
5 | export default class GroupEditGroupsTab {
6 | containerEl: HTMLElement;
7 |
8 | private groupToEdit: PluginGroup;
9 |
10 | private availableGroups: PluginGroup[];
11 |
12 | private groupListElements: Map = new Map<
13 | string,
14 | Setting
15 | >();
16 |
17 | constructor(group: PluginGroup, parentEl: HTMLElement) {
18 | this.groupToEdit = group;
19 |
20 | this.availableGroups = Array.from(
21 | Manager.getInstance().groupsMap.values()
22 | ).filter((g) => g.id !== group.id);
23 |
24 | this.containerEl = this.generateGroupsSection(parentEl);
25 | }
26 |
27 | private generateGroupsSection(parentElement: HTMLElement): HTMLElement {
28 | const groupSection: HTMLElement = parentElement.createDiv();
29 |
30 | groupSection.createEl('h5', { text: 'Groups' });
31 |
32 | const searchAndList: HTMLElement = groupSection.createEl('div');
33 |
34 | new Setting(searchAndList).setName('Search').addText((txt) => {
35 | txt.setPlaceholder('Search for Groups...');
36 | txt.onChange((search) => {
37 | this.searchGroups(search);
38 | });
39 | });
40 |
41 | const groupList = searchAndList.createEl('div');
42 | groupList.addClass('pg-settings-list');
43 |
44 | this.groupListElements = new Map();
45 |
46 | this.sortGroups(this.availableGroups).forEach((pluginGroup) => {
47 | const setting = new Setting(groupList)
48 | .setName(pluginGroup.name)
49 | .addButton((btn) => {
50 | btn.setIcon(
51 | this.groupToEdit.groupIds.contains(pluginGroup.id)
52 | ? 'check-circle'
53 | : 'circle'
54 | ).onClick(() => {
55 | this.toggleGroupForGroup(pluginGroup);
56 | btn.setIcon(
57 | this.groupToEdit.groupIds.contains(pluginGroup.id)
58 | ? 'check-circle'
59 | : 'circle'
60 | );
61 | });
62 | });
63 | this.groupListElements.set(pluginGroup.id, setting);
64 | });
65 | return groupSection;
66 | }
67 |
68 | private searchGroups(search: string) {
69 | const hits = this.availableGroups
70 | .filter((p) => p.name.toLowerCase().contains(search.toLowerCase()))
71 | .map((p) => p.id);
72 | this.groupListElements.forEach((group) => group.settingEl.hide());
73 | hits.forEach((id) => this.groupListElements.get(id)?.settingEl.show());
74 | }
75 |
76 | sortGroups(groups: PluginGroup[]): PluginGroup[] {
77 | return groups.sort((a, b) => {
78 | const aInGroup = this.isGroupInGroup(a);
79 | const bInGroup = this.isGroupInGroup(b);
80 | if (aInGroup && !bInGroup) return -1;
81 | else if (!aInGroup && bInGroup) return 1;
82 | else {
83 | return a.name.localeCompare(b.name);
84 | }
85 | });
86 | }
87 |
88 | isGroupInGroup(plugin: PluginGroup): boolean {
89 | return this.groupToEdit.plugins.map((p) => p.id).contains(plugin.id);
90 | }
91 |
92 | toggleGroupForGroup(group: PluginGroup) {
93 | if (this.groupToEdit.groupIds.contains(group.id)) {
94 | return this.groupToEdit.removeGroup(group);
95 | } else {
96 | return this.groupToEdit.addGroup(group);
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/GroupEditModal/GroupEditPluginsTab.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from 'obsidian';
2 | import { PgPlugin } from '../DataStructures/PgPlugin';
3 | import { PluginGroup } from '../DataStructures/PluginGroup';
4 | import DropdownActionButton, {
5 | DropdownOption,
6 | } from '../Components/BaseComponents/DropdownActionButton';
7 | import PluginManager from '../Managers/PluginManager';
8 | import PluginListTogglable from '../Components/PluginListTogglable';
9 | import Manager from '../Managers/Manager';
10 | import FilteredGroupsList from '../Components/FilteredGroupsList';
11 | import ReorderablePluginList from '../Components/ReorderablePluginList';
12 | import HtmlComponent from '../Components/BaseComponents/HtmlComponent';
13 | import TabGroupComponent from '../Components/BaseComponents/TabGroupComponent';
14 |
15 | interface PluginTabOptions {
16 | group: PluginGroup;
17 | }
18 |
19 | export default class GroupEditPluginsTab extends HtmlComponent {
20 | private readonly availablePlugins: PgPlugin[] =
21 | PluginManager.getAllAvailablePlugins();
22 |
23 | private filteredPlugins: PgPlugin[];
24 |
25 | private readonly sortModes = {
26 | byName: 'By Name',
27 | byNameAndSelected: 'By Name & Selected',
28 | };
29 |
30 | private selectedSortMode = this.sortModes.byNameAndSelected;
31 |
32 | searchTerm: string;
33 |
34 | private pluginsList: PluginListTogglable;
35 |
36 | private filteredGroups: Map = new Map<
37 | string,
38 | PluginGroup
39 | >();
40 |
41 | constructor(parentElement: HTMLElement, options: PluginTabOptions) {
42 | super(parentElement, options);
43 | this.filteredPlugins = this.availablePlugins;
44 |
45 | this.generateComponent();
46 | }
47 |
48 | protected generateContainer(): void {
49 | this.mainEl = this.parentEl.createDiv();
50 |
51 | this.mainEl.createEl('h5', { text: 'Plugins' });
52 | }
53 |
54 | protected generateContent(): void {
55 | if (!this.mainEl) {
56 | return;
57 | }
58 |
59 | const mainPluginSection: HTMLElement = this.mainEl.createEl('div');
60 |
61 | const filterSection: HTMLElement =
62 | this.createFilterSection(mainPluginSection);
63 |
64 | this.pluginsList = new PluginListTogglable(
65 | mainPluginSection,
66 | this.sortPlugins(this.filteredPlugins, this.selectedSortMode),
67 | {
68 | group: this.options.group,
69 | onClickAction: (plugin: PgPlugin) =>
70 | this.togglePluginForGroup(plugin),
71 | }
72 | );
73 |
74 | const reorderPluginSection = new ReorderablePluginList(
75 | this.mainEl.createDiv(),
76 | {
77 | items: this.options.group.plugins,
78 | }
79 | ).mainEl;
80 |
81 | new TabGroupComponent(this.mainEl, {
82 | tabs: [
83 | {
84 | title: 'Main',
85 | content: mainPluginSection,
86 | },
87 | {
88 | title: 'Order',
89 | content:
90 | reorderPluginSection ??
91 | createSpan('No Plugins loaded, please contact Dev'),
92 | },
93 | ],
94 | });
95 | }
96 |
97 | private createFilterSection(parentEl: HTMLElement): HTMLElement {
98 | const filterSection = parentEl.createDiv();
99 | new Setting(filterSection).setName('Search').addText((txt) => {
100 | txt.setPlaceholder('Search for Plugin...');
101 | txt.onChange((search) => {
102 | this.searchPlugins(search);
103 | });
104 | });
105 |
106 | const filtersAndSelectionContainer = filterSection.createDiv({
107 | cls: 'pg-plugin-filter-container',
108 | });
109 | const filtersAndSelection = filtersAndSelectionContainer.createDiv({
110 | cls: 'pg-plugin-filter-section',
111 | });
112 | const filters = filtersAndSelection.createDiv();
113 |
114 | const filteredGroupsChips = new FilteredGroupsList(
115 | filtersAndSelectionContainer,
116 | this.filteredGroups,
117 | () => this.filterAndSortPlugins()
118 | );
119 |
120 | const toggleGroupFilter = (group: PluginGroup) => {
121 | this.filteredGroups.has(group.id)
122 | ? this.filteredGroups.delete(group.id)
123 | : this.filteredGroups.set(group.id, group);
124 | };
125 | const updateGroupFilters = () => {
126 | filteredGroupsChips.update(this.filteredGroups);
127 | this.filterAndSortPlugins();
128 | };
129 |
130 | const groupFilterOptions: DropdownOption[] = [
131 | {
132 | label: 'All groups',
133 | func: () => {
134 | if (
135 | this.filteredGroups.size ===
136 | Manager.getInstance().groupsMap.size
137 | ) {
138 | this.filteredGroups.clear();
139 | } else {
140 | this.filteredGroups = new Map(
141 | Manager.getInstance().groupsMap
142 | );
143 | }
144 | updateGroupFilters();
145 | },
146 | },
147 | ];
148 | Manager.getInstance().groupsMap.forEach((group) => {
149 | groupFilterOptions.push({
150 | label: group.name,
151 | func: () => {
152 | toggleGroupFilter(group);
153 | updateGroupFilters();
154 | },
155 | });
156 | });
157 |
158 | new DropdownActionButton(filters, {
159 | mainLabel: {
160 | label: 'Filter Groups',
161 | },
162 | dropDownOptions: groupFilterOptions,
163 | drpIcon: 'filter',
164 | });
165 |
166 | const sortButton = new DropdownActionButton(filters, {
167 | mainLabel: {
168 | label: 'Sort',
169 | },
170 | dropDownOptions: [
171 | {
172 | label: this.sortModes.byName,
173 | func: () => {
174 | this.onSortModeChanged(
175 | this.sortModes.byName,
176 | sortButton
177 | );
178 | },
179 | },
180 | {
181 | label: this.sortModes.byNameAndSelected,
182 | func: () => {
183 | this.onSortModeChanged(
184 | this.sortModes.byNameAndSelected,
185 | sortButton
186 | );
187 | },
188 | },
189 | ],
190 | minWidth: '80px',
191 | drpIcon: 'sort-desc',
192 | });
193 |
194 | new DropdownActionButton(filtersAndSelection, {
195 | mainLabel: {
196 | label: 'Bulk Select',
197 | },
198 | dropDownOptions: [
199 | {
200 | label: 'Select all',
201 | func: () => this.selectAllFilteredPlugins(),
202 | },
203 | {
204 | label: 'Deselect all',
205 | func: () => this.deselectAllFilteredPlugins(),
206 | },
207 | ],
208 | });
209 |
210 | return filterSection;
211 | }
212 |
213 | private onSortModeChanged(
214 | sortMode: string,
215 | sortButton: DropdownActionButton
216 | ) {
217 | this.selectedSortMode = sortMode;
218 | sortButton.options.mainLabel.label = sortMode;
219 | sortButton.update();
220 | this.filterAndSortPlugins();
221 | }
222 |
223 | // Cumulative Filter function called from various points that acts depending on filter variables set at object level
224 | private filterAndSortPlugins() {
225 | this.filteredPlugins = this.availablePlugins;
226 | if (this.searchTerm && this.searchTerm !== '') {
227 | this.filteredPlugins = this.filteredPlugins.filter((p) =>
228 | p.name.toLowerCase().contains(this.searchTerm.toLowerCase())
229 | );
230 | }
231 |
232 | if (this.filteredGroups.size > 0) {
233 | this.filteredPlugins = this.filterPluginsByGroups(
234 | this.filteredPlugins,
235 | this.filteredGroups
236 | );
237 | }
238 |
239 | this.filteredPlugins = this.sortPlugins(
240 | this.filteredPlugins,
241 | this.selectedSortMode
242 | );
243 |
244 | this.showFilteredPlugins();
245 | }
246 |
247 | private filterPluginsByGroups(
248 | pluginsToFilter: PgPlugin[],
249 | groupsToExclude: Map
250 | ): PgPlugin[] {
251 | const pluginMembershipMap =
252 | Manager.getInstance().mapOfPluginsDirectlyConnectedGroups;
253 |
254 | return pluginsToFilter.filter((plugin) => {
255 | if (!pluginMembershipMap.has(plugin.id)) {
256 | return true;
257 | }
258 |
259 | for (const groupId of pluginMembershipMap.get(plugin.id) ?? []) {
260 | if (groupsToExclude.has(groupId)) {
261 | return false;
262 | }
263 | }
264 | return true;
265 | });
266 | }
267 |
268 | private searchPlugins(search: string) {
269 | this.searchTerm = search;
270 | this.filterAndSortPlugins();
271 | this.showFilteredPlugins();
272 | }
273 |
274 | private showFilteredPlugins() {
275 | this.pluginsList.updateList(this.filteredPlugins);
276 | }
277 |
278 | private deselectAllFilteredPlugins() {
279 | this.filteredPlugins.forEach((plugin) =>
280 | this.options.group.removePlugin(plugin)
281 | );
282 | this.showFilteredPlugins();
283 | }
284 |
285 | private selectAllFilteredPlugins() {
286 | this.filteredPlugins.forEach((plugin) =>
287 | this.options.group.addPlugin(plugin)
288 | );
289 | this.showFilteredPlugins();
290 | }
291 |
292 | sortPlugins(plugins: PgPlugin[], sortMode: string): PgPlugin[] {
293 | if (!plugins || !(typeof plugins[Symbol.iterator] === 'function')) {
294 | return [];
295 | }
296 | const sortedArray = [...plugins];
297 |
298 | if (sortMode === this.sortModes.byName) {
299 | return sortedArray.sort((a, b) => a.name.localeCompare(b.name));
300 | }
301 |
302 | if (sortMode === this.sortModes.byNameAndSelected) {
303 | sortedArray.sort((a, b) => a.name.localeCompare(b.name));
304 | sortedArray.sort((a, b) => {
305 | const aInGroup = this.isPluginInGroup(a);
306 | const bInGroup = this.isPluginInGroup(b);
307 | if (aInGroup && !bInGroup) return -1;
308 | else if (!aInGroup && bInGroup) return 1;
309 | else return 0;
310 | });
311 | }
312 |
313 | return sortedArray;
314 | }
315 |
316 | isPluginInGroup(plugin: PgPlugin): boolean {
317 | return this.options.group.plugins.map((p) => p.id).contains(plugin.id);
318 | }
319 |
320 | togglePluginForGroup(plugin: PgPlugin) {
321 | const { group } = this.options;
322 | group.plugins.filter((p) => p.id === plugin.id).length > 0
323 | ? group.removePlugin(plugin)
324 | : group.addPlugin(plugin);
325 | }
326 | }
327 |
--------------------------------------------------------------------------------
/src/Managers/CommandManager.ts:
--------------------------------------------------------------------------------
1 | import { groupFromId } from '../Utils/Utilities';
2 | import { PluginGroup } from '../DataStructures/PluginGroup';
3 | import Manager from './Manager';
4 | import { Command } from 'obsidian';
5 |
6 | export default class CommandManager {
7 | private enableGroupCommandPrefix = 'plugin-groups-enable-';
8 | private disableGroupCommandPrefix = 'plugin-groups-disable-';
9 | private cnEnablePrefix = 'Plugin Groups: Enable ';
10 | private cnDisablePrefix = 'Plugin Groups: Disable ';
11 |
12 | private commandMap: Map = new Map();
13 |
14 | private static instance?: CommandManager;
15 | private constructor() {}
16 |
17 | public static getInstance(): CommandManager {
18 | if (!CommandManager.instance) {
19 | CommandManager.instance = new CommandManager();
20 | }
21 | return CommandManager.instance;
22 | }
23 |
24 | AddGroupCommands(groupID: string) {
25 | const group = groupFromId(groupID);
26 | if (!group) return;
27 | const enableId = this.enableGroupCommandPrefix + group.id;
28 |
29 | this.commandMap.set(
30 | enableId,
31 | Manager.getInstance().pluginInstance.addCommand({
32 | id: enableId,
33 | name: this.cnEnablePrefix + group.name,
34 | icon: 'power',
35 | checkCallback: (checking: boolean) => {
36 | if (!this.shouldShowCommand(group)) return false;
37 | if (checking) return true;
38 | group.enable();
39 | },
40 | })
41 | );
42 |
43 | const disableId = this.disableGroupCommandPrefix + group.id;
44 |
45 | this.commandMap.set(
46 | disableId,
47 | Manager.getInstance().pluginInstance.addCommand({
48 | id: disableId,
49 | name: this.cnDisablePrefix + group.name,
50 | icon: 'power-off',
51 | checkCallback: (checking: boolean) => {
52 | if (!this.shouldShowCommand(group)) return false;
53 | if (checking) return true;
54 | group.disable();
55 | },
56 | })
57 | );
58 | }
59 |
60 | shouldShowCommand(group: PluginGroup): boolean {
61 | if (!Manager.getInstance().groupsMap.has(group.id)) return false;
62 | if (!Manager.getInstance().generateCommands) return false;
63 | if (!group.groupActive()) {
64 | return false;
65 | }
66 | return group.generateCommands;
67 | }
68 |
69 | updateCommand(groupId: string) {
70 | const group = groupFromId(groupId);
71 | if (!group) {
72 | return;
73 | }
74 |
75 | let command = this.commandMap.get(
76 | this.enableGroupCommandPrefix + group.id
77 | );
78 | if (command) {
79 | command.name = this.cnEnablePrefix + group.name;
80 | }
81 |
82 | command = this.commandMap.get(
83 | this.disableGroupCommandPrefix + group.id
84 | );
85 | if (command) {
86 | command.name = this.cnDisablePrefix + group.name;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Managers/Manager.ts:
--------------------------------------------------------------------------------
1 | import { PersistentSettings, PluginGroupsSettings } from '../Utils/Types';
2 | import PgMain from '../../main';
3 | import { PluginGroup } from '../DataStructures/PluginGroup';
4 | import { pluginId } from '../Utils/Constants';
5 | import { setIcon } from 'obsidian';
6 | import GroupSettingsMenu from '../Components/Modals/GroupSettingsMenu';
7 |
8 | const DEFAULT_SETTINGS: PluginGroupsSettings = {
9 | groupsMap: new Map(),
10 | generateCommands: true,
11 | showNoticeOnGroupLoad: 'none',
12 | devLogs: false,
13 | devices: [],
14 | doLoadSynchronously: true,
15 | showStatusbarIcon: 'None',
16 | };
17 |
18 | export default class Manager {
19 | private static instance?: Manager;
20 |
21 | private settings: PluginGroupsSettings;
22 |
23 | private main: PgMain;
24 |
25 | private constructor() {}
26 |
27 | public static getInstance(): Manager {
28 | if (!Manager.instance) {
29 | Manager.instance = new Manager();
30 | }
31 | return Manager.instance;
32 | }
33 |
34 | async init(main: PgMain): Promise {
35 | this.main = main;
36 | await this.loadSettings();
37 | return this;
38 | }
39 |
40 | async loadSettings() {
41 | const savedSettings: PersistentSettings = await this.main.loadData();
42 |
43 | this.settings = Object.assign({}, DEFAULT_SETTINGS);
44 |
45 | if (!savedSettings) {
46 | return;
47 | }
48 |
49 | Object.keys(this.settings).forEach(function (key) {
50 | if (key in savedSettings) {
51 | // @ts-ignore
52 | Manager.getInstance().settings[key] = savedSettings[key];
53 | }
54 | });
55 |
56 | if (savedSettings.groups && Array.isArray(savedSettings.groups)) {
57 | this.settings.groupsMap = new Map();
58 | savedSettings.groups.forEach((g) => {
59 | this.groupsMap.set(g.id, new PluginGroup(g));
60 | });
61 | }
62 | }
63 |
64 | /***
65 | * Returns a map of each plugin that is in 1 or more groups, and it's connected groups.
66 | * Format: PluginID -> Set of connected groupsId's
67 | */
68 | public get mapOfPluginsConnectedGroupsIncludingParentGroups(): Map<
69 | string,
70 | Set
71 | > {
72 | const pluginsMemMap = new Map>();
73 |
74 | this.groupsMap.forEach((group) => {
75 | group.getAllPluginIdsControlledByGroup().forEach((plugin) => {
76 | if (!pluginsMemMap.has(pluginId)) {
77 | pluginsMemMap.set(plugin, new Set());
78 | }
79 | pluginsMemMap.get(plugin)?.add(group.id);
80 | });
81 | });
82 | return pluginsMemMap;
83 | }
84 |
85 | public get mapOfPluginsDirectlyConnectedGroups(): Map> {
86 | const pluginsMemMap = new Map>();
87 |
88 | this.groupsMap.forEach((group) => {
89 | group.plugins.forEach((plugin) => {
90 | if (!pluginsMemMap.has(plugin.id)) {
91 | pluginsMemMap.set(plugin.id, new Set());
92 | }
93 | pluginsMemMap.get(plugin.id)?.add(group.id);
94 | });
95 | });
96 | return pluginsMemMap;
97 | }
98 |
99 | public getGroupsOfPlugin(pluginId: string): PluginGroup[] {
100 | const groups: PluginGroup[] = [];
101 | for (const group of this.groupsMap.values()) {
102 | if (group.plugins.find((plugin) => plugin.id === pluginId)) {
103 | groups.push(group);
104 | }
105 | }
106 | return groups;
107 | }
108 |
109 | async saveSettings() {
110 | const persistentSettings: PersistentSettings = {
111 | groups: Array.from(this.groupsMap.values() ?? []),
112 | generateCommands:
113 | this.settings.generateCommands ??
114 | DEFAULT_SETTINGS.generateCommands,
115 | showNoticeOnGroupLoad:
116 | this.settings.showNoticeOnGroupLoad ??
117 | DEFAULT_SETTINGS.showNoticeOnGroupLoad,
118 | devLogs: this.settings.devLogs ?? DEFAULT_SETTINGS.devLogs,
119 | devices: this.settings.devices ?? DEFAULT_SETTINGS.devices,
120 | doLoadSynchronously:
121 | this.settings.doLoadSynchronously ??
122 | DEFAULT_SETTINGS.doLoadSynchronously,
123 | showStatusbarIcon:
124 | this.settings.showStatusbarIcon ??
125 | DEFAULT_SETTINGS.showStatusbarIcon,
126 | };
127 | await this.main.saveData(persistentSettings);
128 | }
129 |
130 | // Getters & Setters
131 |
132 | get doLoadSynchronously(): boolean {
133 | return this.settings.doLoadSynchronously;
134 | }
135 |
136 | set doLoadSynchronously(value: boolean) {
137 | this.settings.doLoadSynchronously = value;
138 | }
139 |
140 | get showStatusbarIcon() {
141 | return this.settings.showStatusbarIcon;
142 | }
143 |
144 | set showStatusbarIcon(value) {
145 | this.settings.showStatusbarIcon = value;
146 | }
147 |
148 | get devLog(): boolean {
149 | return this.settings.devLogs;
150 | }
151 | set devLog(value: boolean) {
152 | this.settings.devLogs = value;
153 | }
154 |
155 | get pluginInstance(): PgMain {
156 | return this.main;
157 | }
158 |
159 | public get pluginsManifests() {
160 | return this.obsidianPluginsObject.manifests;
161 | }
162 |
163 | public get obsidianPluginsObject() {
164 | // @ts-ignore
165 | return this.main.app.plugins;
166 | }
167 |
168 | get groupsMap(): Map {
169 | return this.settings.groupsMap;
170 | }
171 |
172 | get generateCommands(): boolean {
173 | return this.settings.generateCommands;
174 | }
175 |
176 | set shouldGenerateCommands(val: boolean) {
177 | this.settings.generateCommands = val;
178 | }
179 |
180 | get showNoticeOnGroupLoad(): string {
181 | return this.settings.showNoticeOnGroupLoad;
182 | }
183 |
184 | set showNoticeOnGroupLoad(val: string) {
185 | this.settings.showNoticeOnGroupLoad = val;
186 | }
187 |
188 | get devices(): string[] {
189 | return this.settings.devices;
190 | }
191 |
192 | private statusbarItem: HTMLElement;
193 |
194 | public updateStatusbarItem() {
195 | if (this.statusbarItem) {
196 | this.statusbarItem.remove();
197 | }
198 | if (this.showStatusbarIcon === 'None') {
199 | return;
200 | }
201 |
202 | this.statusbarItem = this.pluginInstance.addStatusBarItem();
203 | this.statusbarItem.addClasses(['pg-statusbar-icon', 'mod-clickable']);
204 | this.statusbarItem.tabIndex = 0;
205 |
206 | if (this.showStatusbarIcon === 'Text') {
207 | this.statusbarItem.textContent = 'Plugins';
208 | } else if (this.showStatusbarIcon === 'Icon') {
209 | setIcon(this.statusbarItem, 'boxes');
210 | }
211 |
212 | const menu = new GroupSettingsMenu(this.statusbarItem, {});
213 |
214 | this.statusbarItem.onfocus = () => {
215 | menu.updatePosition();
216 | };
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/src/Managers/PluginManager.ts:
--------------------------------------------------------------------------------
1 | import { PgPlugin } from '../DataStructures/PgPlugin';
2 | import {
3 | loadVaultLocalStorage,
4 | saveVaultLocalStorage,
5 | } from '../Utils/Utilities';
6 | import Manager from './Manager';
7 | import { knownPluginIdsKey, pluginId } from '../Utils/Constants';
8 |
9 | export default class PluginManager {
10 | private static enablePluginQueue: Set = new Set();
11 | private static disablePluginQueue: Set = new Set();
12 | private static pgEnabledPlugins: Set = new Set();
13 |
14 | public static async queuePluginForEnable(
15 | plugin: PgPlugin
16 | ): Promise {
17 | if (this.checkPluginEnabled(plugin)) {
18 | return false;
19 | }
20 |
21 | if (this.enablePluginQueue.has(plugin.id)) {
22 | return false;
23 | }
24 |
25 | this.enablePluginQueue.add(plugin.id);
26 | const enabled: boolean = await this.enablePlugin(plugin);
27 | if (enabled) {
28 | this.pgEnabledPlugins.add(plugin.id);
29 | }
30 | this.enablePluginQueue.delete(plugin.id);
31 |
32 | return true;
33 | }
34 |
35 | public static queueDisablePlugin(plugin: PgPlugin) {
36 | if (!this.checkPluginEnabled(plugin)) {
37 | return false;
38 | }
39 | if (this.disablePluginQueue.has(plugin.id)) {
40 | return;
41 | }
42 |
43 | this.disablePluginQueue.add(plugin.id);
44 | this.disablePlugin(plugin);
45 | this.pgEnabledPlugins.delete(plugin.id);
46 | this.disablePluginQueue.delete(plugin.id);
47 | }
48 |
49 | public static async loadNewPlugins() {
50 | if (PluginManager.getKnownPluginIds() === null) {
51 | PluginManager.setKnownPluginIds(
52 | PluginManager.getInstalledPluginIds()
53 | );
54 | } else {
55 | const knownPlugins = PluginManager.getKnownPluginIds();
56 | const installedPlugins: Set =
57 | PluginManager.getInstalledPluginIds();
58 |
59 | if (!installedPlugins) {
60 | return;
61 | }
62 |
63 | PluginManager.setKnownPluginIds(installedPlugins);
64 |
65 | installedPlugins?.forEach((id) => {
66 | if (!knownPlugins?.has(id)) {
67 | Manager.getInstance().groupsMap.forEach((g) => {
68 | if (g.autoAdd) {
69 | const plugin =
70 | PluginManager.getInstalledPluginFromId(id);
71 | if (plugin) {
72 | g.addPlugin(plugin);
73 | }
74 | }
75 | });
76 | }
77 | });
78 | return Manager.getInstance().saveSettings();
79 | }
80 | }
81 |
82 | public static getKnownPluginIds(): Set | null {
83 | const ids = loadVaultLocalStorage(knownPluginIdsKey);
84 | if (!ids) {
85 | return null;
86 | }
87 | return new Set(JSON.parse(ids));
88 | }
89 |
90 | public static setKnownPluginIds(ids: Set | null) {
91 | if (!ids) {
92 | return;
93 | }
94 | const setAsString = JSON.stringify([...ids]);
95 | saveVaultLocalStorage(knownPluginIdsKey, setAsString);
96 | }
97 |
98 | public static getInstalledPluginIds(): Set {
99 | const manifests = Manager.getInstance().pluginsManifests;
100 |
101 | const installedPlugins = new Set();
102 |
103 | for (const key in manifests) {
104 | installedPlugins.add(key);
105 | }
106 |
107 | return installedPlugins;
108 | }
109 |
110 | public static getInstalledPluginFromId(id: string): PgPlugin | null {
111 | if (!Manager.getInstance().obsidianPluginsObject) {
112 | return null;
113 | }
114 | if (!Manager.getInstance().pluginsManifests?.[id]) {
115 | return null;
116 | }
117 |
118 | return new PgPlugin(
119 | Manager.getInstance().pluginsManifests[id].id,
120 | Manager.getInstance().pluginsManifests[id].name
121 | );
122 | }
123 |
124 | public static getAllAvailablePlugins(): PgPlugin[] {
125 | const manifests = Manager.getInstance().pluginsManifests;
126 |
127 | const plugins: PgPlugin[] = [];
128 |
129 | for (const key in manifests) {
130 | if (manifests[key].id === pluginId) continue;
131 |
132 | const info: PgPlugin = new PgPlugin(
133 | manifests[key].id,
134 | manifests[key].name
135 | );
136 | plugins.push(info);
137 | }
138 |
139 | return plugins;
140 | }
141 |
142 | public static checkPluginEnabled(plugin: PgPlugin): boolean {
143 | return (
144 | // obsidianPluginsObject.getPlugin(id) would be the preferred method, however it does not work since,
145 | // for some reason it won't recognize the admonition plugin as active if it was loaded through obsidian
146 | Manager.getInstance().obsidianPluginsObject.enabledPlugins.has(
147 | plugin.id
148 | ) || this.checkPluginEnabledFromPluginGroups(plugin)
149 | );
150 | }
151 |
152 | private static checkPluginEnabledFromPluginGroups(
153 | plugin: PgPlugin
154 | ): boolean {
155 | return this.pgEnabledPlugins.has(plugin.id);
156 | }
157 |
158 | private static enablePlugin(plugin: PgPlugin): Promise {
159 | return Manager.getInstance().obsidianPluginsObject.enablePlugin(
160 | plugin.id
161 | );
162 | }
163 |
164 | private static disablePlugin(plugin: PgPlugin) {
165 | return Manager.getInstance().obsidianPluginsObject.disablePlugin(
166 | plugin.id
167 | );
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/PluginGroupSettings.ts:
--------------------------------------------------------------------------------
1 | import {
2 | App,
3 | ButtonComponent,
4 | Notice,
5 | PluginSettingTab,
6 | Setting,
7 | TextComponent,
8 | } from 'obsidian';
9 | import PgMain from '../main';
10 | import {
11 | getCurrentlyActiveDevice,
12 | makeCollapsible,
13 | setCurrentlyActiveDevice,
14 | } from './Utils/Utilities';
15 | import ConfirmationPopupModal from './Components/BaseComponents/ConfirmationPopupModal';
16 | import Manager from './Managers/Manager';
17 | import PluginManager from './Managers/PluginManager';
18 | import GroupSettings from './Components/Settings/GroupSettings';
19 | import AdvancedSettings from './Components/Settings/AdvancedSettings';
20 | import PluginSettings from './Components/Settings/PluginsSettings';
21 |
22 | export default class PluginGroupSettings extends PluginSettingTab {
23 | constructor(app: App, plugin: PgMain) {
24 | super(app, plugin);
25 | }
26 |
27 | async display(): Promise {
28 | await PluginManager.loadNewPlugins();
29 |
30 | const { containerEl } = this;
31 |
32 | containerEl.empty();
33 |
34 | this.generateGeneralSettings(containerEl);
35 |
36 | new GroupSettings(containerEl, {
37 | collapsible: true,
38 | startOpened: true,
39 | });
40 |
41 | this.GenerateDeviceList(containerEl);
42 |
43 | new PluginSettings(containerEl, {collapsible: true});
44 |
45 | new AdvancedSettings(containerEl, { collapsible: true });
46 | }
47 |
48 | private generateGeneralSettings(containerEl: HTMLElement) {
49 | const generalParent = containerEl.createDiv();
50 |
51 | const header = generalParent.createEl('h4', {
52 | text: 'General',
53 | cls: 'mod-clickable',
54 | });
55 |
56 | const content = generalParent.createDiv();
57 |
58 | makeCollapsible(header, content, true);
59 |
60 | new Setting(content)
61 | .setName('Generate Commands for Groups')
62 | .addToggle((tgl) => {
63 | tgl.setValue(Manager.getInstance().generateCommands ?? false);
64 | tgl.onChange(async (value) => {
65 | Manager.getInstance().shouldGenerateCommands = value;
66 | await Manager.getInstance().saveSettings();
67 | });
68 | });
69 |
70 | new Setting(content)
71 | .setName('Notice upon un-/loading groups')
72 | .addDropdown((drp) => {
73 | drp.addOption('none', 'None')
74 | .addOption('short', 'Short')
75 | .addOption('normal', 'Normal');
76 | drp.setValue(
77 | Manager.getInstance().showNoticeOnGroupLoad ?? 'none'
78 | );
79 | drp.onChange(async (value) => {
80 | Manager.getInstance().showNoticeOnGroupLoad = value;
81 | await Manager.getInstance().saveSettings();
82 | });
83 | });
84 |
85 | new Setting(content).setName('Statusbar Menu').addDropdown((drp) => {
86 | drp.addOption('None', 'None')
87 | .addOption('Icon', 'Icon')
88 | .addOption('Text', 'Text');
89 | drp.setValue(Manager.getInstance().showStatusbarIcon ?? 'None');
90 | drp.onChange(async (value) => {
91 | switch (value) {
92 | case 'Icon':
93 | Manager.getInstance().showStatusbarIcon = 'Icon';
94 | break;
95 | case 'Text':
96 | Manager.getInstance().showStatusbarIcon = 'Text';
97 | break;
98 | default:
99 | Manager.getInstance().showStatusbarIcon = 'None';
100 | break;
101 | }
102 | await Manager.getInstance().saveSettings();
103 | Manager.getInstance().updateStatusbarItem();
104 | });
105 | });
106 | }
107 |
108 | GenerateDeviceList(contentEl: HTMLElement) {
109 | let newDeviceName = '';
110 | const CreateNewDevice = async () => {
111 | if (!newDeviceName || newDeviceName.replace(' ', '') === '') {
112 | return;
113 | }
114 |
115 | if (Manager.getInstance().devices.contains(newDeviceName)) {
116 | new Notice('Name already in use for other device');
117 | return;
118 | }
119 |
120 | Manager.getInstance().devices.push(newDeviceName);
121 | await Manager.getInstance().saveSettings();
122 |
123 | if (!getCurrentlyActiveDevice()) {
124 | setCurrentlyActiveDevice(newDeviceName);
125 | }
126 |
127 | this.display();
128 |
129 | newDeviceName = '';
130 | newDevNameText.setValue(newDeviceName);
131 | };
132 |
133 | const header = contentEl.createEl('h4', { text: 'Devices' });
134 |
135 | const content = contentEl.createDiv();
136 |
137 | makeCollapsible(header, content);
138 |
139 | let deviceAddBtn: ButtonComponent;
140 |
141 | const deviceNameSetting = new Setting(content).setName('New Device');
142 |
143 | const newDevNameText = new TextComponent(deviceNameSetting.controlEl);
144 | newDevNameText
145 | .setValue(newDeviceName)
146 | .onChange((value) => {
147 | newDeviceName = value;
148 | if (deviceAddBtn) {
149 | value.replace(' ', '').length > 0
150 | ? deviceAddBtn.buttonEl.removeClass('btn-disabled')
151 | : deviceAddBtn.buttonEl.addClass('btn-disabled');
152 | }
153 | })
154 | .setPlaceholder('Device Name').inputEl.onkeydown = async (e) => {
155 | if (e.key === 'Enter') {
156 | await CreateNewDevice();
157 | }
158 | };
159 |
160 | deviceNameSetting.addButton((btn) => {
161 | deviceAddBtn = btn;
162 | deviceAddBtn
163 | .setIcon('plus')
164 | .onClick(async () => {
165 | await CreateNewDevice();
166 | })
167 | .buttonEl.addClass('btn-disabled');
168 | });
169 |
170 | Manager.getInstance().devices.forEach((device) => {
171 | const deviceSetting = new Setting(content).setName(device);
172 | if (getCurrentlyActiveDevice() === device) {
173 | deviceSetting.setDesc('Current Device').addButton((btn) => {
174 | btn.setIcon('trash');
175 | btn.onClick(() =>
176 | new ConfirmationPopupModal(
177 | this.app,
178 | 'This is the currently active device, are you sure?',
179 | void 0,
180 | 'Delete',
181 | () => {
182 | this.ResetCurrentDevice();
183 | }
184 | ).open()
185 | );
186 | });
187 | } else {
188 | deviceSetting
189 | .addButton((btn) => {
190 | btn.setButtonText('Set as Current');
191 | btn.onClick(() => {
192 | setCurrentlyActiveDevice(device);
193 | this.display();
194 | });
195 | })
196 | .addButton((btn) => {
197 | btn.setIcon('trash');
198 | btn.onClick(() =>
199 | new ConfirmationPopupModal(
200 | this.app,
201 | 'You are about to delete: ' + device,
202 | void 0,
203 | 'Delete',
204 | async () => {
205 | Manager.getInstance().devices.remove(
206 | device
207 | );
208 | await Manager.getInstance().saveSettings();
209 | this.display();
210 | }
211 | ).open()
212 | );
213 | });
214 | }
215 | });
216 | }
217 |
218 | ResetCurrentDevice() {
219 | const device: string | null = getCurrentlyActiveDevice();
220 |
221 | if (!device) {
222 | return;
223 | }
224 | Manager.getInstance().devices.remove(device);
225 | setCurrentlyActiveDevice(null);
226 | this.display();
227 | }
228 |
229 |
230 |
231 | }
232 |
--------------------------------------------------------------------------------
/src/Utils/Constants.ts:
--------------------------------------------------------------------------------
1 | export const disableStartupTimeout = 25 * 1000;
2 | export const pluginId = 'obsidian-plugin-groups';
3 | export const deviceNameKey = 'obsidian-plugin-groups-device-name';
4 | export const knownPluginIdsKey = 'obsidian-plugin-groups-known-plugins';
5 |
--------------------------------------------------------------------------------
/src/Utils/Types.ts:
--------------------------------------------------------------------------------
1 | import { PluginGroup } from '../DataStructures/PluginGroup';
2 |
3 | export interface Named {
4 | name: string;
5 | }
6 |
7 | export interface PgComponent extends Named {
8 | id: string;
9 | }
10 |
11 | export interface PluginGroupsSettings {
12 | groupsMap: Map;
13 | generateCommands: boolean;
14 | showNoticeOnGroupLoad: string;
15 | devices: string[];
16 | devLogs: boolean;
17 | doLoadSynchronously: boolean;
18 | showStatusbarIcon: 'None' | 'Icon' | 'Text';
19 | }
20 |
21 | export interface PersistentSettings {
22 | groups: PluginGroup[];
23 | generateCommands: boolean;
24 | showNoticeOnGroupLoad: string;
25 | devices: string[];
26 | devLogs: boolean;
27 | doLoadSynchronously: boolean;
28 | showStatusbarIcon: 'None' | 'Icon' | 'Text';
29 | }
30 |
--------------------------------------------------------------------------------
/src/Utils/Utilities.ts:
--------------------------------------------------------------------------------
1 | import { deviceNameKey } from './Constants';
2 | import { PluginGroup } from '../DataStructures/PluginGroup';
3 | import Manager from '../Managers/Manager';
4 | import { setIcon } from 'obsidian';
5 |
6 | export function generateGroupID(
7 | name: string,
8 | delay?: number
9 | ): string | undefined {
10 | let id = nameToId((delay ? 'stg-' : 'pg-') + name);
11 |
12 | const groupMap = Manager.getInstance().groupsMap;
13 |
14 | if (!groupMap) {
15 | return undefined;
16 | }
17 |
18 | if (!groupMap.has(id)) {
19 | return id;
20 | }
21 |
22 | for (let i = 0; i < 512; i++) {
23 | const nrdId = id + i.toString();
24 | id += i.toString();
25 | if (!groupMap.has(id)) {
26 | return delay ? nrdId + delay.toString() : nrdId;
27 | }
28 | }
29 | return undefined;
30 | }
31 |
32 | export function devLog(message?: any, ...data: any[]) {
33 | if (Manager.getInstance().devLog) {
34 | console.log(message, data);
35 | }
36 | }
37 |
38 | export function nameToId(name: string): string {
39 | return name.replace(/[\W_]/g, '').toLowerCase();
40 | }
41 |
42 | export function saveVaultLocalStorage(key: string, object: any): void {
43 | // @ts-ignore
44 | Manager.getInstance().pluginInstance.app.saveLocalStorage(key, object);
45 | }
46 |
47 | export function loadVaultLocalStorage(key: string): string | null | undefined {
48 | // @ts-ignore
49 | return Manager.getInstance().pluginInstance.app.loadLocalStorage(key);
50 | }
51 |
52 | export function getCurrentlyActiveDevice(): string | null {
53 | const device = loadVaultLocalStorage(deviceNameKey);
54 | if (typeof device === 'string') {
55 | return device as string;
56 | }
57 | return null;
58 | }
59 |
60 | export function setCurrentlyActiveDevice(device: string | null) {
61 | saveVaultLocalStorage(deviceNameKey, device);
62 | }
63 |
64 | export function groupFromId(id: string): PluginGroup | undefined {
65 | return Manager.getInstance().groupsMap.get(id);
66 | }
67 |
68 | export function makeCollapsible(
69 | foldClickElement: HTMLElement,
70 | content: HTMLElement,
71 | startOpened?: boolean
72 | ) {
73 | if (!content.hasClass('pg-collapsible-content')) {
74 | content.addClass('pg-collapsible-content');
75 | }
76 |
77 | if (!foldClickElement.hasClass('pg-collapsible-header')) {
78 | foldClickElement.addClass('pg-collapsible-header');
79 | }
80 |
81 | toggleCollapsibleIcon(foldClickElement);
82 |
83 | if (startOpened) {
84 | content.addClass('is-active');
85 | toggleCollapsibleIcon(foldClickElement);
86 | }
87 |
88 | foldClickElement.onclick = () => {
89 | content.hasClass('is-active')
90 | ? content.removeClass('is-active')
91 | : content.addClass('is-active');
92 |
93 | toggleCollapsibleIcon(foldClickElement);
94 | };
95 | }
96 |
97 | function toggleCollapsibleIcon(parentEl: HTMLElement) {
98 | let foldable: HTMLElement | null = parentEl.querySelector(
99 | ':scope > .pg-collapsible-icon'
100 | );
101 | if (!foldable) {
102 | foldable = parentEl.createSpan({ cls: 'pg-collapsible-icon' });
103 | }
104 | if (foldable.dataset.togglestate === 'up') {
105 | setIcon(foldable, 'chevron-down');
106 | foldable.dataset.togglestate = 'down';
107 | } else {
108 | setIcon(foldable, 'chevron-up');
109 | foldable.dataset.togglestate = 'up';
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .pg-settings-list {
2 | overflow: scroll;
3 | min-height: 1vh;
4 | max-height: 40vh;
5 | padding-top: 10px;
6 | padding-bottom: 5px;
7 | }
8 |
9 | .add-group-btn {
10 | }
11 |
12 | .btn-disabled {
13 | pointer-events: none;
14 | opacity: 0.3;
15 | }
16 |
17 | .pg-edit-modal-footer {
18 | position: sticky;
19 | width: 100%;
20 | left: 0;
21 | bottom: -16px;
22 | height: 30px;
23 | display: flex;
24 | justify-content: flex-end;
25 | background: var(--modal-background, '#1e1e1');
26 | align-items: center;
27 | padding-top: 30px;
28 | padding-bottom: 20px;
29 | }
30 |
31 | .pg-tabs {
32 | display: flex;
33 | flex: 0 1 auto;
34 | overflow: auto;
35 | margin: 6px -5px calc(var(--tab-outline-width) * -1);
36 | padding: 1px 15px 0;
37 | }
38 |
39 | .pg-tab {
40 | background: var(--color-base-20);
41 | align-items: center;
42 | display: flex;
43 | gap: var(--size-2-1);
44 | height: 100%;
45 | border-radius: 4px 4px 0 0;
46 | overflow: hidden;
47 | padding: 8px 8px;
48 | width: 100%;
49 | }
50 |
51 | .pg-tab:before {
52 | position: absolute;
53 | bottom: 0;
54 | content: '';
55 | width: calc(var(--tab-curve) * 2);
56 | height: calc(var(--tab-curve) * 2);
57 | border-radius: 100%;
58 | box-shadow: 0 0 0 calc(var(--tab-curve) * 3) transparent;
59 | }
60 |
61 | .pg-tab.is-active {
62 | background: var(--tab-background-active, darkgray);
63 | border-top: 1px solid var(--color-base-30, silver);
64 | border-right: 1px solid var(--color-base-30, silver);
65 | border-left: 1px solid var(--color-base-30, silver);
66 | }
67 |
68 | .pg-tab:hover {
69 | background: var(--interactive-hover, #363636);
70 | }
71 |
72 | .pg-tabbed-content {
73 | display: none;
74 | margin: 0 6px;
75 | padding: 1em 0;
76 | border-top: 1px solid var(--color-base-30, silver);
77 | }
78 |
79 | .pg-tabbed-content.is-active {
80 | display: block;
81 | }
82 |
83 | .pg-plugin-filter-section {
84 | display: flex;
85 | justify-content: space-between;
86 | }
87 |
88 | .pg-drp-btn {
89 | position: relative;
90 |
91 | padding: 1px 1px;
92 | margin: 2px;
93 | }
94 |
95 | .pg-drp-btn span {
96 | padding: 6px;
97 | padding-bottom: 8px;
98 | }
99 |
100 | .pg-drp-btn-main-label {
101 | width: 75%;
102 | }
103 |
104 | .pg-has-dropdown-single:focus .pg-dropdown {
105 | opacity: 1;
106 | display: flex;
107 | pointer-events: auto;
108 | }
109 |
110 | .pg-has-dropdown:focus-within .pg-dropdown {
111 | opacity: 1;
112 | display: flex;
113 | pointer-events: auto;
114 | }
115 |
116 | .pg-dropdown {
117 | opacity: 0;
118 | pointer-events: none;
119 |
120 | position: absolute;
121 |
122 | display: none;
123 | align-items: center;
124 | justify-content: center;
125 | flex-direction: column;
126 |
127 | z-index: 1;
128 | top: 2em;
129 | padding: 0;
130 | width: 100%;
131 | transition: opacity 0.15s ease-out;
132 |
133 | list-style: none;
134 |
135 | box-shadow: var(--input-shadow, rgba(211, 211, 211, 0.5));
136 |
137 | color: var(--text-normal, white);
138 | background-color: var(--interactive-normal, #363636);
139 |
140 | border-radius: var(--button-radius, 5px);
141 | border: 0;
142 | }
143 |
144 | .pg-dropdown-item {
145 | padding: 10px;
146 | white-space: normal;
147 | width: 100%;
148 | height: 100%;
149 | }
150 |
151 | .pg-dropdown-item:hover {
152 | box-shadow: rgba(17, 17, 26, 0.1) 0px 4px 16px,
153 | rgba(17, 17, 26, 0.05) 0px 8px 32px;
154 | background-color: var(--interactive-hover, #363636);
155 | border-radius: var(--button-radius, 5px);
156 | }
157 |
158 | .pg-drp-btn .pg-drp-btn-divider {
159 | padding: 0;
160 | margin: 4px 1px;
161 | }
162 |
163 | .pg-chip {
164 | background-color: var(--color-base-30, #363636);
165 | border-radius: 16px;
166 | font-size: 14px;
167 | display: inline-flex;
168 | align-items: center;
169 | }
170 |
171 | .pg-chip span {
172 | margin-left: 12px;
173 | }
174 |
175 | .pg-chip .pg-chip-close-btn {
176 | padding-top: 4px;
177 | padding-right: 4px;
178 | padding-left: 4px;
179 | border-radius: 16px;
180 | margin-left: 12px;
181 | }
182 |
183 | .pg-chip-close-btn:hover {
184 | background: var(--color-base-50, rgb(255 255 255 / 20%));
185 | }
186 |
187 | .pg-group-filter-list {
188 | display: flex;
189 | gap: 8px;
190 | margin-top: 8px;
191 | min-height: 1.8rem;
192 | align-items: center;
193 | flex-wrap: wrap;
194 | }
195 |
196 | .pg-plugin-filter-container {
197 | border-bottom: 1px solid var(--color-base-30, silver);
198 | margin-bottom: 1rem;
199 | padding-bottom: 0.6rem;
200 | }
201 |
202 | .pg-settings-window {
203 | height: 0;
204 | opacity: 0;
205 |
206 | transition: height 1s, opacity 250ms;
207 |
208 | overflow: scroll;
209 | position: fixed;
210 | background-color: var(--modal-background);
211 | }
212 |
213 | .pg-statusbar-icon:focus-within > .pg-settings-window {
214 | height: auto;
215 | max-height: 500px;
216 | min-height: 100px;
217 | min-width: 350px;
218 | opacity: 1;
219 |
220 | transition: height 1s, opacity 250ms;
221 |
222 | border-radius: var(--modal-radius);
223 | border: var(--modal-border-width) solid var(--modal-border-color);
224 | padding: 0 0 0 16px;
225 | }
226 |
227 | .pg-collapsible-content.is-active {
228 | display: block;
229 | }
230 |
231 | .pg-collapsible-content {
232 | display: none;
233 | }
234 |
235 | .pg-collapsible-header:hover {
236 | background-color: var(--interactive-hover, #363636);
237 | border-radius: var(--button-radius, 5px);
238 | }
239 |
240 | .pg-collapsible-header {
241 | padding-bottom: 8px;
242 | padding-top: 4px;
243 | }
244 |
245 | .pg-collapsible-icon {
246 | position: relative;
247 | top: 4px;
248 | left: 16px;
249 | }
250 |
251 | :root {
252 | --pg-edit-modal-footer-height: 60px;
253 | }
254 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "strictNullChecks": true,
14 | "lib": ["DOM", "ES5", "ES6", "ES7"]
15 | },
16 | "include": ["**/*.ts"]
17 | }
18 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from 'fs';
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t'));
10 |
11 | let manifestBeta = JSON.parse(readFileSync('manifest-beta.json', 'utf8'));
12 | manifestBeta.version = targetVersion;
13 | writeFileSync('manifest-beta.json', JSON.stringify(manifestBeta, null, '\t'));
14 |
15 | // update versions.json with target version and minAppVersion from manifest.json
16 | let versions = JSON.parse(readFileSync('versions.json', 'utf8'));
17 | versions[targetVersion] = minAppVersion;
18 | writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
19 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "1.0.0": "0.15.0",
3 | "1.0.1": "0.15.0",
4 | "1.2.0": "0.15.0",
5 | "1.2.1": "0.15.0",
6 | "1.2.2": "0.15.0",
7 | "1.3.0": "0.15.0",
8 | "2.0.0": "0.15.0",
9 | "2.0.1": "0.15.0",
10 | "2.0.2": "0.15.0",
11 | "2.1.0": "0.15.0"
12 | }
--------------------------------------------------------------------------------