Compare commits

...

12 commits

Author SHA1 Message Date
4Source
2a09294e56 Apply version 1.4.0 2026-03-17 22:36:41 +00:00
4Source
e0c6469396
[FEAT] In Use/Known lists (#19)
* Refactor patching

* Preparation for InUse/Known lists

* Remove "debug" from allowed console methods in ESLint configuration

* Move Monkey keys  from constants to files

* Merge patch files into single file per class

* Patch install, enable, disable for plugins/themes

* Update lists based on state of vault

* Disable debug logs

* Add aria labels to flairs + remove duplicated flair code

* Update README.md

* Fix Security Audit

* Fix Command names
2026-03-17 23:36:19 +01:00
4Source
663db4c648 Apply version 1.3.2 2026-03-13 16:08:21 +00:00
4Source
8c53184308
Extracting commands into separate files (#17)
* Extracting commands into separate files

* Fix linting

* Fix missing package-lock update of version-change.mjs
2026-03-13 17:08:01 +01:00
4Source
fcc243b4ee Apply version 1.3.1 2026-03-02 21:21:57 +00:00
4Source
b7d7227a7c
[FEAT] Enhance favorite list management with filtering and event listeners (#12)
* Enhance favorite list management by filtering out existing favorites

* Add event listeners for favorite list changes
2026-03-02 22:21:37 +01:00
4Source
a4182bf48a Apply version 1.3.0 2026-03-02 20:05:02 +00:00
4Source
8f706c9372
[FEAT] Add Commands (#11)
* Add commands for managing favorite lists

* Refactor favoritePlugins and favoriteThemes handling for improved clarity and performance

* Add community plugin/theme management commands and fetch functions

* Add note about using a separate key for favorite lists during development

* Fix plugin id

* Add backup and restore command for favorite lists

* Add detailed command descriptions for managing favorite lists in README
2026-03-02 21:04:43 +01:00
4Source
81a01388fa Apply version 1.2.0 2026-03-01 21:30:34 +00:00
4Source
40c4930ba1
[FEAT] Favorite/Unfavorite for installed plugins (#8)
* Fix favorite themes loading logic

* Validate environment variables for favorite plugins and themes on load

* Add community plugins settings tab favorite icon

* Enhance README with usage details and screenshots

Updated the README to clarify ease of use and added additional screenshots.

* Security Audit fix
2026-03-01 22:30:15 +01:00
4Source
7303174298 Apply version 1.1.2 2025-11-07 08:04:24 +00:00
4Source
7293d464c2
Fix requests (#6) 2025-11-07 09:04:06 +01:00
47 changed files with 1898 additions and 362 deletions

View file

@ -9,7 +9,6 @@
"allow": [
"warn",
"error",
"debug"
]
}
],

View file

@ -26,6 +26,8 @@ If not related issue exists, open a new one using the appropriate <a href="../..
#### 1. Fork the repository.
#### 2. Create a new vault
Just for testing the plugin so you **can't** exedentially destroy your real vault
> [!NOTE]
> When you working with ``npm run dev`` the plugin will use another key to identify the favorite lists. This **should** ensure you are not overwriting your real favorite lists. When you are working with ``npm run build`` the plugin will use the real key.
#### 3. Clone repository to your local machine.
```shell
cd path/to/vault

1
.gitignore vendored
View file

@ -17,6 +17,7 @@ main.js
# obsidian
data.json
backup
# Exclude macOS Finder (System Explorer) View States
.DS_Store

112
README.md
View file

@ -3,15 +3,16 @@
Stop re-searching the same plugins or themes in every vault. With one central list, your essential tools and styles are always easy to find.
# Features
- **Global Favorites for Plugins & Themes** - Mark your go to plugins and themes once and use them in all vaults
- **Ease of Use** - Favorite/unfavorite directly from the Obsidian's plugin and theme details browser
- **Global Favorites list for Plugins & Themes** - Mark your go-to plugins and themes once and access them across all vaults.
- **Global In Use list for Plugins & Themes** - Automatically maintained list of plugins and themes that are currently enabled in at least one vault.
- **Global Known list for Plugins & Themes** - Automatically maintained history of plugins and themes that have been installed at least once in any vault.
- **Ease of Use** - Favorite/unfavorite directly inside the Obsidian's
- Plugin browser
- Community plugins tab
- Theme browser
- via [Commands](#commands)
- **Vault Independence** - Favorites are stored globally on your device, not tied to a single vault
# Screenshots
<img width="1104" height="1004" alt="image" src="https://github.com/user-attachments/assets/61b41e7b-4569-4509-95f3-d5e8a667d653" />
<img width="1104" height="1004" alt="image" src="https://github.com/user-attachments/assets/960eac0d-fea2-48e1-8faf-e54a7504149e" />
# How It Works
1. Install and enable ***Favorites***.
2. Open Obsidian's plugin or theme browser.
@ -19,6 +20,103 @@ Stop re-searching the same plugins or themes in every vault. With one central li
4. Use the Favorite button.
5. Your favorites are saved globally and appear in every vault.
For plugins you can also favorite already installed plugins directly from the Community plugins tab.
# Screenshots
![FavoritesPluginBrowserDemo_1 4 0](https://github.com/user-attachments/assets/6b93f818-207a-4381-ab4d-f5aa57cad021)
![FavoritesThemeBrowserDemo_1 4 0](https://github.com/user-attachments/assets/3f3b802e-3a82-45fd-a174-b51d5dd4c0f0)
<img width="1100" height="1000" alt="image" src="https://github.com/user-attachments/assets/5c8537dd-f5dc-4393-8cf7-6b506806631c" />
<img width="1100" height="1000" alt="image" src="https://github.com/user-attachments/assets/a6aebde5-a93c-4a93-ba0e-ccb9ad1c6560" />
<img width="1100" height="1000" alt="image" src="https://github.com/user-attachments/assets/e34ec7f4-57f2-4dc1-a150-45e9ab152295" />
<img width="1100" height="1000" alt="image" src="https://github.com/user-attachments/assets/ec9dfdf9-d607-4ff5-8679-fc286cb5e43c" />
<img width="1100" height="1000" alt="image" src="https://github.com/user-attachments/assets/a0b35f98-b194-433f-bd9e-66eecb5dd111" />
# Commands
If you prefer managing your favorites list with commands here are the available commands
- [Search and add plugin to favorite list](#search-and-add-plugin-to-favorite-list)
- [Search and add theme to favorite list](#search-and-add-theme-to-favorite-list)
- [Search and remove plugin from favorite list](#search-and-remove-plugin-from-favorite-list)
- [Search and remove theme from favorite list](#search-and-remove-theme-from-favorite-list)
- [Clear the plugin favorites list](#clear-the-plugin-favorites-list)
- [Clear the plugin in use list](#clear-the-plugin-in-use-list)
- [Clear the plugin known list](#clear-the-plugin-known-list)
- [Clear the theme favorites list](#clear-the-theme-favorites-list)
- [Clear the theme in use list](#clear-the-theme-in-use-list)
- [Clear the theme known list](#clear-the-theme-known-list)
- [Manually load the lists](#manually-load-the-lists)
- [Manually save the lists](#manually-save-the-lists)
- [Export lists to file](#export-lists-to-file)
- [Import lists from file](#import-lists-from-file)
## Search and add plugin to favorite list
Search and select a community plugin to add it to your favorites list. The list is fetched from the community plugin registry.
## Search and add theme to favorite list
Search and select a community theme to add it to your favorites list. The list is fetched from the community theme registry.
## Search and remove plugin from favorite list
Search and select from your current favorite plugins to remove it from your favorites list.
## Search and remove theme from favorite list
Search and select from your current favorite themes to remove it from your favorites list.
## Clear the plugin favorites list
Permanently removes ***all*** plugins from your favorites list after confirmation.
> [!WARNING]
> This action cannot be undone.
## Clear the plugin in use list
Permanently removes ***all*** plugins from your in use list after confirmation.
> [!WARNING]
> This action cannot be undone.
## Clear the plugin known list
Permanently removes ***all*** plugins from your known list after confirmation.
> [!WARNING]
> This action cannot be undone.
## Clear the theme favorites list
Permanently removes ***all*** themes from your favorites list after confirmation.
> [!WARNING]
> This action cannot be undone.
## Clear the theme in use list
Permanently removes ***all*** themes from your in use list after confirmation.
> [!WARNING]
> This action cannot be undone.
## Clear the theme known list
Permanently removes ***all*** themes from your known list after confirmation.
> [!WARNING]
> This action cannot be undone.
## Manually load the lists
Reloads the saved lists for plugins and themes. Use this to restore your lists if they were changed externally or need to be refreshed.
> [!NOTE]
> This is normally not necessary because the plugin handles this internally.
## Manually save the lists
Forces the plugin to immediately save your current lists for plugins and themes. Use this if you want to ensure your lists are persistent.
> [!WARNING]
> This could overwrite changes from another running Obsidian instance if the changes from there are not loaded beforehand.
> [!NOTE]
> This is normally not necessary because the plugin handles this internally.
## Export lists to file
Creates a backup file containing your current lists for plugins and themes. The backup is saved as a timestamped JSON file inside the plugins backup folder within your vault configuration directory.
## Import lists from file
Restores lists for plugins and themes from a previously created backup file. You will be prompted to select a backup file from the plugins backup folder. After confirmation imported lists will replaces your current lists with the content of the selected backup.
> [!WARNING]
> This will overwrite current lists and can't be undone.
# Contribution
Feel free to contribute.

View file

@ -40,7 +40,11 @@ const context = await esbuild.context({
outfile: "main.js",
define: {
'process.env.FAVORITE_PLUGINS_KEY': JSON.stringify(prod ? 'favorite-plugins' : 'dev-favorite-plugins'),
'process.env.FAVORITE_THEMES_KEY': JSON.stringify(prod ? 'favorite-themes' : 'dev-favorite-themes')
'process.env.FAVORITE_THEMES_KEY': JSON.stringify(prod ? 'favorite-themes' : 'dev-favorite-themes'),
'process.env.INUSE_PLUGINS_KEY': JSON.stringify(prod ? 'in-use-plugins' : 'dev-in-use-plugins'),
'process.env.INUSE_THEMES_KEY': JSON.stringify(prod ? 'in-use-themes' : 'dev-in-use-themes'),
'process.env.KNOWN_PLUGINS_KEY': JSON.stringify(prod ? 'known-plugins' : 'dev-known-plugins'),
'process.env.KNOWN_THEMES_KEY': JSON.stringify(prod ? 'known-themes' : 'dev-known-themes'),
}
});

View file

@ -7,5 +7,5 @@
"authorUrl": "https://github.com/4Source",
"isDesktopOnly": false,
"fundingUrl": "https://www.buymeacoffee.com/4Source",
"version": "1.1.1"
"version": "1.4.0"
}

92
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "favorites",
"version": "1.1.0",
"version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "favorites",
"version": "1.1.0",
"version": "1.4.0",
"license": "MIT",
"devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
@ -1059,10 +1059,11 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -1105,10 +1106,11 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -1175,10 +1177,11 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@ -2207,10 +2210,11 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -3226,10 +3230,11 @@
}
},
"node_modules/eslint/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -3443,10 +3448,11 @@
}
},
"node_modules/filelist/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
@ -3497,10 +3503,11 @@
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
"dev": true,
"license": "ISC"
},
"node_modules/form-data": {
"version": "4.0.4",
@ -3667,10 +3674,11 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -4118,10 +4126,11 @@
}
},
"node_modules/jake/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@ -4730,10 +4739,11 @@
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
@ -5026,12 +5036,13 @@
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@ -5864,10 +5875,11 @@
}
},
"node_modules/test-exclude/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},

View file

@ -33,5 +33,5 @@
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"version": "1.1.1"
"version": "1.4.0"
}

View file

@ -0,0 +1,21 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'clear-plugin-favorites-list',
name: 'Clear the plugin favorites list',
callback: () => {
plugin.loadFavoritePlugins();
const numberClear = plugin.favoritePlugins.length;
new DialogModal(plugin.app, `Clear ${numberClear} plugin(s) permanently from favorite list?`, '', () => {
plugin.favoritePlugins = [];
plugin.saveFavoritePlugins();
new Notice(`Cleared ${numberClear} plugins from favorite list`);
}, () => {
new Notice('Canceled clear plugins from favorite list');
}, 'Clear', true, 'Cancel', false).open();
},
};
}

View file

@ -0,0 +1,21 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'clear-plugin-in-use-list',
name: 'Clear the plugin in use list',
callback: () => {
plugin.loadInUsePlugins();
const numberClear = Object.keys(plugin.inUsePlugins).length;
new DialogModal(plugin.app, `Clear ${numberClear} plugin(s) permanently from in use list?`, '', () => {
plugin.inUsePlugins = {};
plugin.saveInUsePlugins();
new Notice(`Cleared ${numberClear} plugins from in use list`);
}, () => {
new Notice('Canceled clear plugins from in use list');
}, 'Clear', true, 'Cancel', false).open();
},
};
}

View file

@ -0,0 +1,21 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'clear-plugin-known-list',
name: 'Clear the plugin known list',
callback: () => {
plugin.loadKnownPlugins();
const numberClear = plugin.knownPlugins.length;
new DialogModal(plugin.app, `Clear ${numberClear} plugin(s) permanently from known list?`, '', () => {
plugin.knownPlugins = [];
plugin.saveKnownPlugins();
new Notice(`Cleared ${numberClear} plugins from known list`);
}, () => {
new Notice('Canceled clear plugins from known list');
}, 'Clear', true, 'Cancel', false).open();
},
};
}

View file

@ -0,0 +1,21 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'clear-theme-favorites-list',
name: 'Clear the theme favorites list',
callback: () => {
plugin.loadFavoriteThemes();
const numberClear = plugin.favoriteThemes.length;
new DialogModal(plugin.app, `Clear ${numberClear} theme(s) permanently from favorite list?`, '', () => {
plugin.favoriteThemes = [];
plugin.saveFavoriteThemes();
new Notice(`Cleared ${numberClear} themes from favorite list`);
}, () => {
new Notice('Canceled clear themes from favorite list');
}, 'Clear', true, 'Cancel', false).open();
},
};
}

View file

@ -0,0 +1,21 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'clear-theme-in-use-list',
name: 'Clear the theme in use list',
callback: () => {
plugin.loadInUseThemes();
const numberClear = Object.keys(plugin.inUseThemes).length;
new DialogModal(plugin.app, `Clear ${numberClear} theme(s) permanently from in use list?`, '', () => {
plugin.inUseThemes = {};
plugin.saveInUseThemes();
new Notice(`Cleared ${numberClear} themes from in use list`);
}, () => {
new Notice('Canceled clear themes from in use list');
}, 'Clear', true, 'Cancel', false).open();
},
};
}

View file

@ -0,0 +1,21 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'clear-theme-known-list',
name: 'Clear the theme known list',
callback: () => {
plugin.loadKnownThemes();
const numberClear = plugin.knownThemes.length;
new DialogModal(plugin.app, `Clear ${numberClear} theme(s) permanently from known list?`, '', () => {
plugin.knownThemes = [];
plugin.saveKnownThemes();
new Notice(`Cleared ${numberClear} themes from known list`);
}, () => {
new Notice('Canceled clear themes from known list');
}, 'Clear', true, 'Cancel', false).open();
},
};
}

View file

@ -0,0 +1,37 @@
import { Command, normalizePath, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { PLUGIN_BACKUP_BASE_PATH } from 'src/constants';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'export-lists',
name: 'Export lists to file',
callback: async () => {
if (plugin.favoritePlugins.length <= 0 && plugin.favoriteThemes.length <= 0 && Object.keys(plugin.inUsePlugins).length <= 0 && Object.keys(plugin.inUseThemes).length <= 0 && plugin.knownPlugins.length <= 0 && plugin.knownThemes.length <= 0) {
new Notice('No lists to backup');
return;
}
// Ensure backup path does exist
const backupPath = normalizePath(`${plugin.app.vault.configDir}${PLUGIN_BACKUP_BASE_PATH}`);
await plugin.app.vault.adapter.mkdir(backupPath);
// Create the file name
const pad = (n: number) => n.toString().padStart(2, '0');
const date = new Date();
const dateString = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${pad(date.getHours())}-${pad(date.getMinutes())}`;
const backupFilePath = normalizePath(`${backupPath}/favorite-${dateString}.json`);
// Write backup file for lists
await plugin.app.vault.adapter.write(backupFilePath, JSON.stringify({
favoritePlugins: plugin.favoritePlugins,
favoriteThemes: plugin.favoriteThemes,
inUsePlugins: plugin.inUsePlugins,
inUseThemes: plugin.inUseThemes,
knownPlugins: plugin.knownPlugins,
knownThemes: plugin.knownThemes,
}));
new Notice(`Lists backup successful written to: ${backupFilePath}`);
},
};
}

View file

@ -0,0 +1,51 @@
import { Command, normalizePath, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { PLUGIN_BACKUP_BASE_PATH } from 'src/constants';
import { StringSuggestModal } from 'src/modals/StringSuggestModal';
import { DialogModal } from 'src/modals/DialogModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'import-lists',
name: 'Import lists from file',
callback: async () => {
const backupPath = normalizePath(`${plugin.app.vault.configDir}${PLUGIN_BACKUP_BASE_PATH}`);
if (!(await plugin.app.vault.adapter.exists(backupPath))) {
// Backup path does not exist
new Notice(`Backup path does not exist: ${backupPath}`);
return;
}
const backupFiles = (await plugin.app.vault.adapter.list(backupPath)).files;
if (backupFiles.length <= 0) {
// Backup path is empty
new Notice('Backup path is empty');
return;
}
new StringSuggestModal(plugin.app, 'Select backup file to load from', backupFiles, async (result) => {
const content = await plugin.app.vault.adapter.read(result);
const json = JSON.parse(content);
const loadedFavoritePlugins = json.favoritePlugins || [];
const loadedFavoriteThemes = json.favoriteThemes || [];
const loadedInUsePlugins = json.inUsePlugins || [];
const loadedInUseThemes = json.inUseThemes || [];
const loadedKnownPlugins = json.knownPlugins || [];
const loadedKnownThemes = json.knownThemes || [];
new DialogModal(plugin.app, 'Are you sure you want to overwrite current lists?', `The following lists will be overwritten: favorite plugins will change from ${plugin.favoritePlugins.length} items to ${loadedFavoritePlugins.length} items, favorite themes will change from ${plugin.favoriteThemes.length} items to ${loadedFavoriteThemes.length} items, in use plugins will change from ${plugin.inUsePlugins.length} items to ${loadedInUsePlugins.length} items, in use themes will change from ${plugin.inUseThemes.length} items to ${loadedInUseThemes.length} items, known plugins will change from ${plugin.knownPlugins.length} items to ${loadedKnownPlugins.length} items, and known themes will change from ${plugin.knownThemes.length} items to ${loadedKnownThemes.length} items.`, () => {
plugin.favoritePlugins = loadedFavoritePlugins;
plugin.favoriteThemes = loadedFavoriteThemes;
plugin.inUsePlugins = loadedInUsePlugins;
plugin.inUseThemes = loadedInUseThemes;
plugin.knownPlugins = loadedKnownPlugins;
plugin.knownThemes = loadedKnownThemes;
plugin.saveLists();
new Notice('Loaded backup successfully');
}, () => {
new Notice('Canceled loading backup by user');
}, 'Overwrite', true, 'Cancel', false).open();
}).open();
},
};
}

13
src/commands/LoadLists.ts Normal file
View file

@ -0,0 +1,13 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'load-lists',
name: 'Manually load the lists',
callback: () => {
plugin.loadLists();
new Notice('Loaded lists');
},
};
}

13
src/commands/SaveLists.ts Normal file
View file

@ -0,0 +1,13 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'save-lists',
name: 'Manually save the lists',
callback: () => {
plugin.saveLists();
new Notice('Saved lists');
},
};
}

View file

@ -0,0 +1,33 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { CommunitySuggestModal } from 'src/modals/CommunitySuggestModal';
import { CommunityPlugin, fetchCommunityPluginList } from 'src/util/GitHub';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'search-and-add-plugin-to-favorite-list',
name: 'Search and add plugin to favorite list',
callback: async () => {
let items = await fetchCommunityPluginList();
if (!items) {
new Notice('Failed to fetch community plugins. See console for more information.');
return;
}
// Filter out favorites
plugin.loadFavoritePlugins();
items = items.filter(value => !plugin.favoritePlugins.contains(value.id));
if (items.length <= 0) {
new Notice('No plugin left which is could be added to favorite list');
return;
}
new CommunitySuggestModal<CommunityPlugin>(plugin.app, 'Select plugin which should be added to favorites list...', items, (result) => {
plugin.favoritePlugins.push(result.id);
plugin.saveFavoritePlugins();
new Notice(`Added ${result.name} to favorite list`);
}).open();
},
};
}

View file

@ -0,0 +1,33 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { CommunitySuggestModal } from 'src/modals/CommunitySuggestModal';
import { CommunityTheme, fetchCommunityThemeList } from 'src/util/GitHub';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'search-and-add-theme-to-favorite-list',
name: 'Search and add theme to favorite list',
callback: async () => {
let items = await fetchCommunityThemeList();
if (!items) {
new Notice('Failed to fetch community themes. See console for more information.');
return;
}
// Filter out favorites
plugin.loadFavoriteThemes();
items = items.filter(value => !plugin.favoriteThemes.contains(value.name));
if (items.length <= 0) {
new Notice('No theme left which is could be added to favorite list');
return;
}
new CommunitySuggestModal<CommunityTheme>(plugin.app, 'Select theme which should be added to favorites list...', items, (result) => {
plugin.favoriteThemes.push(result.name);
plugin.saveFavoriteThemes();
new Notice(`Added ${result.name} to favorite list`);
}).open();
},
};
}

View file

@ -0,0 +1,29 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { CommunityPlugin, fetchCommunityPluginList } from 'src/util/GitHub';
import { CommunitySuggestModal } from 'src/modals/CommunitySuggestModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'search-and-remove-plugin-from-favorite-list',
name: 'Search and remove plugin from favorite list',
callback: async () => {
plugin.loadFavoritePlugins();
if (plugin.favoritePlugins.length <= 0) {
new Notice('Plugin favorite list is empty');
return;
}
const communityItems = await fetchCommunityPluginList();
const installedItems = Object.values(plugin.app.plugins.manifests);
const items = plugin.favoritePlugins.map((value) => {
return communityItems?.find((communityValue) => value === communityValue.id) || installedItems.find(installedValue => value === installedValue.id) as unknown as CommunityPlugin || { id: value, name: value, author: 'unknown' } as CommunityPlugin;
});
new CommunitySuggestModal<CommunityPlugin>(plugin.app, 'Select plugin which should be removed from the favorites list...', items, (result) => {
plugin.favoritePlugins.remove(result.id);
plugin.saveFavoritePlugins();
new Notice(`Removed ${result.name} from favorite list`);
}).open();
},
};
}

View file

@ -0,0 +1,29 @@
import { Command, Notice } from 'obsidian';
import CurrentPlugin from '../main';
import { CommunityTheme, fetchCommunityThemeList } from 'src/util/GitHub';
import { CommunitySuggestModal } from 'src/modals/CommunitySuggestModal';
export default function (plugin: CurrentPlugin): Command {
return {
id: 'search-and-remove-theme-from-favorite-list',
name: 'Search and remove theme from favorite list',
callback: async () => {
plugin.loadFavoriteThemes();
if (plugin.favoriteThemes.length <= 0) {
new Notice('Theme favorite list is empty');
return;
}
const communityItems = await fetchCommunityThemeList();
const installedItems = Object.values(plugin.app.customCss.themes);
const items = plugin.favoriteThemes.map((value) => {
return communityItems?.find((communityValue) => value === communityValue.name) || installedItems.find(installedValue => value === installedValue.name) as unknown as CommunityTheme || { name: value, author: 'unknown' } as CommunityTheme;
});
new CommunitySuggestModal<CommunityTheme>(plugin.app, 'Select theme which should be removed from the favorites list...', items, (result) => {
plugin.favoriteThemes.remove(result.name);
plugin.saveFavoriteThemes();
new Notice(`Removed ${result.name} from favorite list`);
}).open();
},
};
}

34
src/commands/index.ts Normal file
View file

@ -0,0 +1,34 @@
import CurrentPlugin from '../main';
import ClearPluginFavoritesList from './ClearPluginFavoritesList';
import ClearPluginInUseList from './ClearPluginInUseList';
import ClearPluginKnownList from './ClearPluginKnownList';
import ClearThemeFavoritesList from './ClearThemeFavoritesList';
import ClearThemeInUseList from './ClearThemeInUseList';
import ClearThemeKnownList from './ClearThemeKnownList';
import ExportLists from './ExportLists';
import ImportLists from './ImportLists';
import LoadLists from './LoadLists';
import SaveLists from './SaveLists';
import SearchAndAddPluginToFavoriteList from './SearchAndAddPluginToFavoriteList';
import SearchAndAddThemeToFavoriteList from './SearchAndAddThemeToFavoriteList';
import SearchAndRemovePluginFromFavoriteList from './SearchAndRemovePluginFromFavoriteList';
import SearchAndRemoveThemeFromFavoriteList from './SearchAndRemoveThemeFromFavoriteList';
export async function addAllCommands(plugin: CurrentPlugin) {
// TODO: Dynamically import all files in the commands folder during bundling time
plugin.addCommand(ClearPluginFavoritesList(plugin));
plugin.addCommand(ClearPluginInUseList(plugin));
plugin.addCommand(ClearPluginKnownList(plugin));
plugin.addCommand(ClearThemeFavoritesList(plugin));
plugin.addCommand(ClearThemeInUseList(plugin));
plugin.addCommand(ClearThemeKnownList(plugin));
plugin.addCommand(ExportLists(plugin));
plugin.addCommand(ImportLists(plugin));
plugin.addCommand(LoadLists(plugin));
plugin.addCommand(SaveLists(plugin));
plugin.addCommand(SearchAndAddPluginToFavoriteList(plugin));
plugin.addCommand(SearchAndAddThemeToFavoriteList(plugin));
plugin.addCommand(SearchAndRemovePluginFromFavoriteList(plugin));
plugin.addCommand(SearchAndRemoveThemeFromFavoriteList(plugin));
}

View file

@ -1,6 +1,2 @@
export const PLUGIN_ID = 'favorite';
export const MONKEY_KEY_MODAL_OPEN = `${PLUGIN_ID}-Modal-open`;
export const MONKEY_KEY_PLUGIN_BROWSER_MODAL_UPDATE_ITEMS = `${PLUGIN_ID}-CommunityPluginModal-updateItems`;
export const MONKEY_KEY_PLUGIN_BROWSER_MODAL_SHOW_ITEMS = `${PLUGIN_ID}-CommunityPluginModal-showItems`;
export const MONKEY_KEY_THEME_BROWSER_MODAL_UPDATE_ITEMS = `${PLUGIN_ID}-ThemePluginModal-updateItems`;
export const MONKEY_KEY_THEME_BROWSER_MODAL_SHOW_ITEMS = `${PLUGIN_ID}-ThemePluginModal-showItems`;
export const PLUGIN_ID = 'favorites';
export const PLUGIN_BACKUP_BASE_PATH = `/plugins/${PLUGIN_ID}/backup`;

View file

@ -1,312 +1,429 @@
import { CommunityItem, CommunityModal, Modal, Plugin } from 'obsidian';
import { dedupe, around } from 'monkey-around';
import { MONKEY_KEY_PLUGIN_BROWSER_MODAL_UPDATE_ITEMS, MONKEY_KEY_MODAL_OPEN, MONKEY_KEY_THEME_BROWSER_MODAL_UPDATE_ITEMS, MONKEY_KEY_THEME_BROWSER_MODAL_SHOW_ITEMS, MONKEY_KEY_PLUGIN_BROWSER_MODAL_SHOW_ITEMS } from './constants';
import { Plugin } from 'obsidian';
import { addAllCommands } from './commands';
import patchSettingsModal from './patches/patch.SettingsModal';
import patchModal from './patches/patch.Modal';
import patchPluginManager from './patches/patch.PluginManager';
import patchStyleManager from './patches/patch.StyleManager';
export default class FavoritesPlugin extends Plugin {
pluginsKey: string;
themesKey: string;
favoritePlugins?: string[];
favoriteThemes?: string[];
communityPluginModalPrototype?: CommunityModal;
communityThemesModalPrototype?: CommunityModal;
uninstallModalOpen?: () => void;
uninstallPluginBrowserModalUpdateItems?: () => void;
uninstallPluginBrowserModalShowItem?: () => void;
uninstallThemeBrowserModalUpdateItems?: () => void;
uninstallThemeBrowserModalShowItem?: () => void;
pluginFavoritesKey: string;
favoritePlugins: string[];
themeFavoritesKey: string;
favoriteThemes: string[];
pluginInUseKey: string;
inUsePlugins: Record<string, string[]>;
themeInUseKey: string;
inUseThemes: Record<string, string[]>;
pluginKnownKey: string;
knownPlugins: string[];
themeKnownKey: string;
knownThemes: string[];
uninstallCommunityPluginModalPatch?: () => void;
uninstallCommunityPluginsSettingTabPatch?: () => void;
uninstallCommunityThemeModalPatch?: () => void;
uninstallModalPatch?: () => void;
uninstallPluginManagerPatch?: () => void;
uninstallSettingsModalPatch?: () => void;
uninstallStyleManagerPatch?: () => void;
async onload() {
this.pluginsKey = process.env.FAVORITE_PLUGINS_KEY || '';
this.themesKey = process.env.FAVORITE_THEMES_KEY || '';
console.debug(`Plugins key: ${this.pluginsKey} Themes key: ${this.themesKey}`);
this.loadStorageKeys();
this.loadLists();
// eslint-disable-next-line @typescript-eslint/no-this-alias -- Is required because the this context wil change inside the 'monkey-around' functions but the plugin is required to be accessible
const plugin = this;
if (!this.uninstallModalOpen) {
console.debug('Patch Modal.open');
this.uninstallModalOpen = around(Modal.prototype, {
open(oldMethod) {
return dedupe(MONKEY_KEY_MODAL_OPEN, oldMethod, function (...args) {
console.debug('Call Modal.open');
const result = oldMethod && oldMethod.apply(this, args);
// Check the opened Modal is the Community plugin browse modal
if (this.containerEl?.querySelector('.mod-community-plugin')) {
// Patch updateItems method
if (!plugin.uninstallPluginBrowserModalUpdateItems) {
console.debug('Patch PluginBrowserModal.updateItems');
plugin.uninstallPluginBrowserModalUpdateItems = around(this.constructor.prototype, {
updateItems(oldMethod) {
return dedupe(MONKEY_KEY_PLUGIN_BROWSER_MODAL_UPDATE_ITEMS, oldMethod, function () {
console.debug('Call PluginBrowserModal.updateItems');
// Load the favorite plugins
plugin.loadFavoritePlugins;
const result = oldMethod && oldMethod.apply(this);
// Add to the favorite plugins a tag to visualize it for the user
if (plugin.favoritePlugins) {
plugin.favoritePlugins.forEach(id => {
if (this.items && this.items[id]?.nameEl) {
this.items[id].nameEl.createSpan({
cls: 'flair',
text: 'favorite',
});
}
});
}
return result;
});
},
});
}
else {
console.warn('PluginBrowserModal.updateItems already patched!');
}
// Patch showItem method
if (!plugin.uninstallPluginBrowserModalShowItem) {
console.debug('Patch PluginBrowserModal.showItem');
plugin.uninstallPluginBrowserModalShowItem = around(this.constructor.prototype, {
showItem(oldMethod) {
return dedupe(MONKEY_KEY_PLUGIN_BROWSER_MODAL_SHOW_ITEMS, oldMethod, function (...args: CommunityItem[]) {
console.debug('Call PluginBrowserModal.showItem');
return oldMethod && oldMethod.apply(this, args)
.then(async (r: void) => {
const infoEl = await this.detailsEl.getElementsByClassName('community-modal-info')[0];
const infoNameEl = await infoEl?.getElementsByClassName('community-modal-info-name')[0];
const buttonContainerEl = await infoEl?.getElementsByClassName('community-modal-button-container')[0];
// Load the favorite plugins
plugin.loadFavoritePlugins();
const isFavorite = plugin.favoritePlugins?.contains(this.selectedItemId);
// Add to the favorite plugins a tag to visualize it for the user
if (isFavorite) {
if (infoNameEl) {
infoNameEl.createSpan({
cls: 'flair',
text: 'favorite',
});
}
}
plugin.registerEvent(buttonContainerEl?.createEl('button', { text: isFavorite ? 'Unfavorite' : 'Favorite' }).addEventListener('click', () => {
if (isFavorite) {
plugin.favoritePlugins?.remove(this.selectedItemId);
}
else {
plugin.favoritePlugins?.push(this.selectedItemId);
}
plugin.saveFavorites();
// Redraw
infoEl.detach();
this.showItem(this.items[this.selectedItemId]);
this.update();
}));
return r;
});
});
},
});
}
else {
console.warn('PluginBrowserModal.showItem already patched!');
}
}
// Check the opened Modal is the Community themes browse modal
if (this.containerEl?.querySelector('.mod-community-theme')) {
// Patch updateItems method
if (!plugin.uninstallThemeBrowserModalUpdateItems) {
console.debug('Patch ThemeBrowserModal.updateItems');
plugin.uninstallThemeBrowserModalUpdateItems = around(this.constructor.prototype, {
updateItems(oldMethod) {
return dedupe(MONKEY_KEY_THEME_BROWSER_MODAL_UPDATE_ITEMS, oldMethod, function () {
console.debug('Call ThemeBrowserModal.updateItems');
// Load the favorite themes
plugin.loadFavoriteThemes();
const result = oldMethod && oldMethod.apply(this);
// Ignore the default theme
if (this.selectedItemId === '') {
return result;
}
// Add to the favorite themes a tag to visualize it for the user
if (plugin.favoriteThemes) {
plugin.favoriteThemes.forEach(id => {
if (this.items && this.items[id]?.nameEl) {
this.items[id].nameEl.createSpan({
cls: 'flair',
text: 'favorite',
});
}
});
}
return result;
});
},
});
}
else {
console.warn('ThemeBrowserModal.updateItems already patched!');
}
// Patch showItems method
if (!plugin.uninstallThemeBrowserModalShowItem) {
console.debug('Patch ThemeBrowserModal.showItem');
plugin.uninstallThemeBrowserModalShowItem = around(this.constructor.prototype, {
showItem(oldMethod) {
return dedupe(MONKEY_KEY_THEME_BROWSER_MODAL_SHOW_ITEMS, oldMethod, function (...args: CommunityItem[]) {
console.debug('Call ThemeBrowserModal.showItem');
return oldMethod && oldMethod.apply(this, args)
.then(async (r: void) => {
// Ignore the default theme
if (this.selectedItemId === '') {
return r;
}
const infoEl = await this.detailsEl.getElementsByClassName('community-modal-info')[0];
const infoNameEl = await infoEl?.getElementsByClassName('community-modal-info-name')[0];
const buttonContainerEl = await infoEl?.getElementsByClassName('community-modal-button-container')[0];
// Load the favorite themes
plugin.loadFavoriteThemes();
const isFavorite = plugin.favoriteThemes?.contains(this.selectedItemId);
// Add to the favorite themes a tag to visualize it for the user
if (isFavorite) {
if (infoNameEl) {
infoNameEl.createSpan({
cls: 'flair',
text: 'favorite',
});
}
}
plugin.registerEvent(buttonContainerEl?.createEl('button', { text: isFavorite ? 'Unfavorite' : 'Favorite' }).addEventListener('click', () => {
if (isFavorite) {
plugin.favoriteThemes?.remove(this.selectedItemId);
}
else {
plugin.favoriteThemes?.push(this.selectedItemId);
}
plugin.saveFavorites();
// Redraw
infoEl.detach();
this.showItem(this.items[this.selectedItemId]);
this.update();
}));
return r;
});
});
},
});
}
else {
console.warn('ThemeBrowserModal.showItem already patched!');
}
}
if (plugin.uninstallModalOpen && plugin.uninstallPluginBrowserModalUpdateItems && plugin.uninstallPluginBrowserModalShowItem && plugin.uninstallThemeBrowserModalUpdateItems && plugin.uninstallThemeBrowserModalShowItem) {
console.debug('Uninstall Modal.open');
plugin.uninstallModalOpen();
plugin.uninstallModalOpen = undefined;
}
return result;
});
},
});
}
else {
console.warn('Modal.open already patched!');
// Patch SettingsModal
if (!this.uninstallSettingsModalPatch) {
// console.debug('Patch SettingsModal');
this.uninstallSettingsModalPatch = patchSettingsModal(this);
}
// TODO: Redraw modals if local storage 'favorite-plugins' or 'favorite-themes' changed
// Patch Modal
if (!this.uninstallModalPatch) {
// console.debug('Patch Modal');
this.uninstallModalPatch = patchModal(this);
}
/*
* TODO: Add command to save and reload the favorites list
* TODO: Add command to remove all favorites from list
* TODO: Add command to search for plugin to add to favorites list
*/
// Patch PluginManager
if (!this.uninstallPluginManagerPatch) {
// console.debug('Patch PluginManager');
this.uninstallPluginManagerPatch = patchPluginManager(this);
}
// Patch StyleManager
if (!this.uninstallStyleManagerPatch) {
// console.debug('Patch StyleManager');
this.uninstallStyleManagerPatch = patchStyleManager(this);
}
// Check the state of the vault and update lists
this.updateListsForVault();
// Add all commands registered
addAllCommands(this);
}
onunload() {
async onunload() {
try {
// Restore the original functions
if (this.uninstallModalOpen) {
console.debug('Uninstall Modal.open');
this.uninstallModalOpen();
this.uninstallModalOpen = undefined;
if (this.uninstallCommunityPluginModalPatch) {
// console.debug('Uninstall CommunityPluginModal patch');
this.uninstallCommunityPluginModalPatch();
this.uninstallCommunityPluginModalPatch = undefined;
}
if (this.uninstallPluginBrowserModalUpdateItems) {
console.debug('Uninstall PluginBrowserModal.updateItem');
this.uninstallPluginBrowserModalUpdateItems();
this.uninstallPluginBrowserModalUpdateItems = undefined;
if (this.uninstallCommunityPluginsSettingTabPatch) {
// console.debug('Uninstall CommunityPluginsSettingTab patch');
this.uninstallCommunityPluginsSettingTabPatch();
this.uninstallCommunityPluginsSettingTabPatch = undefined;
}
if (this.uninstallPluginBrowserModalShowItem) {
console.debug('Uninstall PluginBrowserModal.showItem');
this.uninstallPluginBrowserModalShowItem();
this.uninstallPluginBrowserModalShowItem = undefined;
if (this.uninstallCommunityThemeModalPatch) {
// console.debug('Uninstall CommunityThemeModal patch');
this.uninstallCommunityThemeModalPatch();
this.uninstallCommunityThemeModalPatch = undefined;
}
if (this.uninstallThemeBrowserModalUpdateItems) {
console.debug('Uninstall ThemeBrowserModal.updateItem');
this.uninstallThemeBrowserModalUpdateItems();
this.uninstallThemeBrowserModalUpdateItems = undefined;
if (this.uninstallModalPatch) {
// console.debug('Uninstall Modal patch');
this.uninstallModalPatch();
this.uninstallModalPatch = undefined;
}
if (this.uninstallThemeBrowserModalShowItem) {
console.debug('Uninstall ThemeBrowserModal.showItem');
this.uninstallThemeBrowserModalShowItem();
this.uninstallThemeBrowserModalShowItem = undefined;
if (this.uninstallPluginManagerPatch) {
// console.debug('Uninstall PluginManager patch');
this.uninstallPluginManagerPatch();
this.uninstallPluginManagerPatch = undefined;
}
if (this.uninstallSettingsModalPatch) {
// console.debug('Uninstall SettingsModal patch');
this.uninstallSettingsModalPatch();
this.uninstallSettingsModalPatch = undefined;
}
if (this.uninstallStyleManagerPatch) {
// console.debug('Uninstall StyleManager patch');
this.uninstallStyleManagerPatch();
this.uninstallStyleManagerPatch = undefined;
}
}
catch (e) {
console.error('Failed uninstalling patch functions!');
console.error(e);
}
}
loadStorageKeys() {
// favorite keys
if (process.env.FAVORITE_PLUGINS_KEY) {
this.pluginFavoritesKey = process.env.FAVORITE_PLUGINS_KEY;
}
else {
throw Error('Missing environment variable \'FAVORITE_PLUGINS_KEY\'');
}
if (process.env.FAVORITE_THEMES_KEY) {
this.themeFavoritesKey = process.env.FAVORITE_THEMES_KEY;
}
else {
throw Error('Missing environment variable \'FAVORITE_THEMES_KEY\'');
}
// console.debug(`Plugins favorite key: ${this.pluginFavoritesKey} Themes favorite key: ${this.themeFavoritesKey}`);
// In use keys
if (process.env.INUSE_PLUGINS_KEY) {
this.pluginInUseKey = process.env.INUSE_PLUGINS_KEY;
}
else {
throw Error('Missing environment variable \'INUSE_PLUGINS_KEY\'');
}
if (process.env.INUSE_THEMES_KEY) {
this.themeInUseKey = process.env.INUSE_THEMES_KEY;
}
else {
throw Error('Missing environment variable \'INUSE_THEMES_KEY\'');
}
// console.debug(`Plugins in use key: ${this.pluginInUseKey} Themes in use key: ${this.themeInUseKey}`);
// Known keys
if (process.env.KNOWN_PLUGINS_KEY) {
this.pluginKnownKey = process.env.KNOWN_PLUGINS_KEY;
}
else {
throw Error('Missing environment variable \'KNOWN_PLUGINS_KEY\'');
}
if (process.env.KNOWN_THEMES_KEY) {
this.themeKnownKey = process.env.KNOWN_THEMES_KEY;
}
else {
throw Error('Missing environment variable \'KNOWN_THEMES_KEY\'');
}
// console.debug(`Plugins known key: ${this.pluginKnownKey} Themes known key: ${this.themeKnownKey}`);
// Register for storage key changes
this.registerDomEvent(window, 'storage', (event) => {
if (event.key === this.pluginFavoritesKey) {
// console.debug('Plugins favorites key content changed', event);
this.onFavoritePluginsChanged(event.newValue);
}
if (event.key === this.themeFavoritesKey) {
// console.debug('Themes favorites key content changed', event);
this.onFavoriteThemesChanged(event.newValue);
}
if (event.key === this.pluginInUseKey) {
// console.debug('Plugins in use key content changed', event);
this.onInUsePluginsChanged(event.newValue);
}
if (event.key === this.themeInUseKey) {
// console.debug('Themes in use key content changed', event);
this.onInUseThemesChanged(event.newValue);
}
if (event.key === this.pluginKnownKey) {
// console.debug('Plugins known key content changed', event);
this.onKnownPluginsChanged(event.newValue);
}
if (event.key === this.themeKnownKey) {
// console.debug('Themes known key content changed', event);
this.onKnownThemesChanged(event.newValue);
}
});
}
onFavoritePluginsChanged(newValue: string | null) {
// console.debug(`onFavoritePluginsChanged: ${newValue}`);
if (newValue) {
this.favoritePlugins = JSON.parse(newValue);
}
else {
this.favoritePlugins = [];
}
}
onFavoriteThemesChanged(newValue: string | null) {
// console.debug(`onFavoriteThemesChanged: ${newValue}`);
if (newValue) {
this.favoriteThemes = JSON.parse(newValue);
}
else {
this.favoriteThemes = [];
}
}
onInUsePluginsChanged(newValue: string | null) {
// console.debug(`onInUsePluginsChanged: ${newValue}`);
if (newValue) {
this.inUsePlugins = JSON.parse(newValue);
}
else {
this.inUsePlugins = {};
}
}
onInUseThemesChanged(newValue: string | null) {
// console.debug(`onInUseThemesChanged: ${newValue}`);
if (newValue) {
this.inUseThemes = JSON.parse(newValue);
}
else {
this.inUseThemes = {};
}
}
onKnownPluginsChanged(newValue: string | null) {
// console.debug(`onKnownPluginsChanged: ${newValue}`);
if (newValue) {
this.knownPlugins = JSON.parse(newValue);
}
else {
this.knownPlugins = [];
}
}
onKnownThemesChanged(newValue: string | null) {
// console.debug(`onKnownThemesChanged: ${newValue}`);
if (newValue) {
this.knownThemes = JSON.parse(newValue);
}
else {
this.knownThemes = [];
}
}
loadFavoritePlugins() {
// Load the favorite plugins
this.favoritePlugins = JSON.parse(localStorage.getItem(this.pluginsKey) || '[]');
this.favoritePlugins = JSON.parse(localStorage.getItem(this.pluginFavoritesKey) || '[]');
}
loadFavoriteThemes() {
// Load the favorite plugins
this.favoritePlugins = JSON.parse(localStorage.getItem(this.themesKey) || '[]');
this.favoriteThemes = JSON.parse(localStorage.getItem(this.themeFavoritesKey) || '[]');
}
saveFavorites() {
if (this.favoritePlugins) {
if (this.favoritePlugins.length > 0) {
localStorage.setItem(this.pluginsKey, JSON.stringify(this.favoritePlugins));
}
else {
localStorage.removeItem(this.pluginsKey);
loadInUsePlugins() {
// Load the in use plugins
this.inUsePlugins = JSON.parse(localStorage.getItem(this.pluginInUseKey) || '{}');
}
loadInUseThemes() {
// Load the in use plugins
this.inUseThemes = JSON.parse(localStorage.getItem(this.themeInUseKey) || '{}');
}
loadKnownPlugins() {
// Load the known plugins
this.knownPlugins = JSON.parse(localStorage.getItem(this.pluginKnownKey) || '[]');
}
loadKnownThemes() {
// Load the known plugins
this.knownThemes = JSON.parse(localStorage.getItem(this.themeKnownKey) || '[]');
}
loadLists() {
this.loadFavoritePlugins();
this.loadFavoriteThemes();
this.loadInUsePlugins();
this.loadInUseThemes();
this.loadKnownPlugins();
this.loadKnownThemes();
}
saveFavoritePlugins() {
if (this.favoritePlugins.length > 0) {
localStorage.setItem(this.pluginFavoritesKey, JSON.stringify(this.favoritePlugins));
}
else {
localStorage.removeItem(this.pluginFavoritesKey);
}
}
saveFavoriteThemes() {
if (this.favoriteThemes.length > 0) {
localStorage.setItem(this.themeFavoritesKey, JSON.stringify(this.favoriteThemes));
}
else {
localStorage.removeItem(this.themeFavoritesKey);
}
}
saveInUsePlugins() {
if (Object.keys(this.inUsePlugins).length > 0) {
localStorage.setItem(this.pluginInUseKey, JSON.stringify(this.inUsePlugins));
}
else {
localStorage.removeItem(this.pluginInUseKey);
}
}
saveInUseThemes() {
if (Object.keys(this.inUseThemes).length > 0) {
localStorage.setItem(this.themeInUseKey, JSON.stringify(this.inUseThemes));
}
else {
localStorage.removeItem(this.themeInUseKey);
}
}
saveKnownPlugins() {
if (this.knownPlugins.length > 0) {
localStorage.setItem(this.pluginKnownKey, JSON.stringify(this.knownPlugins));
}
else {
localStorage.removeItem(this.pluginKnownKey);
}
}
saveKnownThemes() {
if (this.knownThemes.length > 0) {
localStorage.setItem(this.themeKnownKey, JSON.stringify(this.knownThemes));
}
else {
localStorage.removeItem(this.themeKnownKey);
}
}
saveLists() {
this.saveFavoritePlugins();
this.saveFavoriteThemes();
this.saveInUsePlugins();
this.saveInUseThemes();
this.saveKnownPlugins();
this.saveKnownThemes();
}
updateListsForVault() {
const currentVault = this.app.appId;
// Check the installed plugins and update known plugins list
for (const id of Object.keys(this.app.plugins.manifests)) {
// Add Plugin to known list if not already
if (!this.knownPlugins.contains(id)) {
this.knownPlugins.push(id);
}
}
if (this.favoriteThemes) {
if (this.favoriteThemes.length > 0) {
localStorage.setItem(this.themesKey, JSON.stringify(this.favoriteThemes));
// Check the enabled plugins and update in use plugins list
for (const id of this.app.plugins.enabledPlugins) {
// Add the plugin to the in use list with this vault as user
if (!this.inUsePlugins[id] || !Array.isArray(this.inUsePlugins[id])) {
this.inUsePlugins[id] = [];
}
else {
localStorage.removeItem(this.themesKey);
if (!this.inUsePlugins[id].contains(currentVault)) {
this.inUsePlugins[id].push(currentVault);
}
}
// Check the in use plugins list does not contain old entries of this vault
for (const id of Object.keys(this.inUsePlugins)) {
if (Array.isArray(this.inUsePlugins[id]) && this.inUsePlugins[id].contains(currentVault)) {
if (!this.app.plugins.enabledPlugins.has(id)) {
this.inUsePlugins[id].remove(currentVault);
// Cleanup in use if last vault stops using it
if (this.inUsePlugins[id].length <= 0) {
delete this.inUsePlugins[id];
}
}
}
}
// Check the installed themes and update known themes list
for (const name of Object.keys(this.app.customCss.themes)) {
// Add theme to known list if not already
if (!this.knownThemes.contains(name)) {
this.knownThemes.push(name);
}
}
// Check the enabled theme and update in use themes list
const name = this.app.customCss.theme;
// Ignore the default theme
if (name !== '') {
// Add the theme to the in use list with this vault as user
if (!this.inUseThemes[name] || !Array.isArray(this.inUseThemes[name])) {
this.inUseThemes[name] = [];
}
if (!this.inUseThemes[name].contains(currentVault)) {
this.inUseThemes[name].push(currentVault);
}
}
// Check the in use themes list does not contain old entries of this vault
for (const name of Object.keys(this.inUseThemes)) {
if (Array.isArray(this.inUseThemes[name]) && this.inUseThemes[name].contains(currentVault)) {
if (this.app.customCss.theme !== name) {
this.inUseThemes[name].remove(currentVault);
// Cleanup in use if last vault stops using it
if (this.inUseThemes[name].length <= 0) {
delete this.inUseThemes[name];
}
}
}
}
this.saveLists();
}
}

View file

@ -0,0 +1,35 @@
import { App, FuzzyMatch, FuzzySuggestModal, renderResults } from 'obsidian';
export class CommunitySuggestModal<T extends { name: string, author: string }> extends FuzzySuggestModal<T> {
private items: T[] = [];
private onSubmit: (result: T) => void = () => { };
constructor(app: App, title: string, items: T[], onSubmit: (result: T) => void) {
super(app);
this.setPlaceholder(title);
this.items = items;
this.onSubmit = onSubmit;
}
getItemText(item: T): string {
return `${item.name} ${item.author}`;
}
getItems(): T[] {
return this.items;
}
onChooseItem(item: T): void {
this.onSubmit(item);
}
renderSuggestion(match: FuzzyMatch<T>, el: HTMLElement) {
const titleEl = el.createDiv();
renderResults(titleEl, match.item.name, match.match);
// Only render the matches in the author name.
const authorEl = el.createEl('small');
const offset = -(match.item.name.length + 1);
renderResults(authorEl, match.item.author, match.match, offset);
}
}

77
src/modals/DialogModal.ts Normal file
View file

@ -0,0 +1,77 @@
import { App, Modal, Setting } from 'obsidian';
export class DialogModal extends Modal {
message: string;
submit: string;
submitWarning: boolean;
deny: string;
denyWarning: boolean;
onSubmit: () => void;
onDeny: () => void;
constructor(app: App, title: string, message: string, onSubmit: () => void, onDeny: () => void, submit = 'Agree', submitWarning = false, deny = 'Cancel', denyWarning = true) {
super(app);
this.titleEl.setText(title);
this.message = message;
this.onSubmit = onSubmit;
this.onDeny = onDeny;
this.submit = submit;
this.submitWarning = submitWarning;
this.deny = deny;
this.denyWarning = denyWarning;
}
onOpen(): void {
const { contentEl } = this;
if (this.message && this.message !== '') {
contentEl.createEl('span', { text: this.message });
}
const setting = new Setting(contentEl);
if (this.submitWarning) {
setting.addButton(button => button
.setButtonText(this.submit)
.setWarning()
.onClick(() => {
this.close();
this.onSubmit();
}));
}
else {
setting.addButton(button => button
.setButtonText(this.submit)
.onClick(() => {
this.close();
this.onSubmit();
}));
}
if (this.denyWarning) {
setting.addButton(button => button
.setButtonText(this.deny)
.setWarning()
.onClick(() => {
this.close();
this.onDeny();
}));
}
else {
setting.addButton(button => button
.setButtonText(this.deny)
.onClick(() => {
this.close();
this.onDeny();
}));
}
setting.setClass('modal-buttons');
}
onClose(): void {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -0,0 +1,23 @@
import { App, FuzzySuggestModal } from 'obsidian';
export class StringSuggestModal extends FuzzySuggestModal<string> {
private items: string[] = [];
onSubmit: (result: string) => void = () => { };
constructor(app: App, title: string, items: string[], onSubmit: (result: string) => void) {
super(app);
this.setPlaceholder(title);
this.items = items;
this.onSubmit = onSubmit;
}
getItems(): string[] {
return this.items;
}
getItemText(item: string): string {
return item;
}
onChooseItem(item: string): void {
this.onSubmit(item);
}
}

View file

@ -0,0 +1,100 @@
import { around, dedupe } from 'monkey-around';
import MyPlugin from '../main';
import { PLUGIN_ID } from 'src/constants';
import { CommunityItem, CommunityPluginModal } from 'obsidian';
import FavoriteFlair from 'src/ui-components/FavoriteFlair';
import InUseFlair from 'src/ui-components/InUseFlair';
import KnownFlair from 'src/ui-components/KnownFlair';
import InstalledFlair from 'src/ui-components/InstalledFlair';
const MONKEY_KEY = `${PLUGIN_ID}-CommunityPluginModal-`;
export default (context: CommunityPluginModal, plugin: MyPlugin) => around(context.constructor.prototype, {
showItem(oldMethod) {
return dedupe(`${MONKEY_KEY}showItem`, oldMethod, async function (item: CommunityItem) {
// console.debug('Call CommunityPluginModal.showItem');
const result = oldMethod && await oldMethod.apply(this, [item]);
const infoEl = await this.detailsEl.getElementsByClassName('community-modal-info')[0];
const infoNameEl = await infoEl?.getElementsByClassName('community-modal-info-name')[0];
const buttonContainerEl = await infoEl?.getElementsByClassName('community-modal-button-container')[0];
// Load the plugin lists
plugin.loadFavoritePlugins();
plugin.loadInUsePlugins();
plugin.loadKnownPlugins();
// Add aria label to installed flair
new InstalledFlair(infoNameEl);
// Add to the favorite plugins a flair to visualize it for the user
const isFavorite = plugin.favoritePlugins.contains(this.selectedItemId);
if (isFavorite) {
new FavoriteFlair(infoNameEl);
}
// Add to the in use plugins a flair to visualize it for the user
if (Object.keys(plugin.inUsePlugins).contains(this.selectedItemId)) {
new InUseFlair(infoNameEl);
}
// Add to the known plugins a flair to visualize it for the user
if (plugin.knownPlugins.contains(this.selectedItemId)) {
new KnownFlair(infoNameEl);
}
plugin.registerDomEvent(buttonContainerEl?.createEl('button', { text: isFavorite ? 'Unfavorite' : 'Favorite' }), 'click', async () => {
if (isFavorite) {
plugin.favoritePlugins.remove(this.selectedItemId);
}
else {
plugin.favoritePlugins.push(this.selectedItemId);
}
plugin.saveFavoritePlugins();
// Redraw
infoEl.detach();
await this.showItem(this.items[this.selectedItemId]);
this.update();
});
return result;
});
},
updateItems(oldMethod) {
return dedupe(`${MONKEY_KEY}updateItems`, oldMethod, function () {
// console.debug('Call PluginBrowserModal.updateItems');
// Load the plugin lists
plugin.loadFavoritePlugins();
plugin.loadInUsePlugins();
plugin.loadKnownPlugins();
const result = oldMethod && oldMethod.apply(this);
// Add aria label to installed flair
Object.keys(plugin.app.plugins.manifests).forEach(id => {
new InstalledFlair(this.items[id]?.nameEl);
});
// Add to the favorite plugins a flair to visualize it for the user
plugin.favoritePlugins.forEach(id => {
new FavoriteFlair(this.items[id]?.nameEl);
});
// Add to the in use plugins a flair to visualize it for the user
Object.keys(plugin.inUsePlugins).forEach(id => {
new InUseFlair(this.items[id]?.nameEl);
});
// Add to the known plugins a flair to visualize it for the user
plugin.knownPlugins.forEach(id => {
new KnownFlair(this.items[id]?.nameEl);
});
return result;
});
},
});

View file

@ -0,0 +1,50 @@
import { CommunityItem, CommunityPluginsSettingTab, setIcon, setTooltip } from 'obsidian';
import MyPlugin from '../main';
import { around, dedupe } from 'monkey-around';
import { PLUGIN_ID } from 'src/constants';
const MONKEY_KEY = `${PLUGIN_ID}-CommunityPluginsSettingTab-`;
export default (context: CommunityPluginsSettingTab, plugin: MyPlugin) => around(context, {
renderInstalledPlugin(oldMethod) {
return dedupe(`${MONKEY_KEY}renderInstalledPlugin`, oldMethod, function (item: CommunityItem, containerEl: HTMLElement, n: unknown, i: unknown, r: unknown) {
// console.debug('Call CommunityPluginsSettingTab.renderInstalledPlugin');
oldMethod && oldMethod.apply(this, [item, containerEl, n, i, r]);
// Load the favorite plugins
plugin.loadFavoritePlugins();
const selectedPluginID = item.id;
const isFavorite = plugin.favoritePlugins.contains(selectedPluginID);
const favEl = createDiv('clickable-icon extra-setting-button');
plugin.registerDomEvent(favEl, 'click', () => {
if (isFavorite) {
plugin.favoritePlugins.remove(selectedPluginID);
}
else {
plugin.favoritePlugins.push(selectedPluginID);
}
plugin.saveFavoritePlugins();
// Redraw
this.render(false);
});
if (isFavorite) {
setIcon(favEl, 'star-off');
setTooltip(favEl, 'Unfavorite');
}
else {
setIcon(favEl, 'star');
setTooltip(favEl, 'Favorite');
}
// Add Favorite Icon to plugin control
const controlEl: HTMLElement = this.installedPlugins.listEl.lastChild?.getElementsByClassName('setting-item-control')[0];
controlEl.insertBefore(favEl, controlEl.children[0]);
});
},
});

View file

@ -0,0 +1,114 @@
import { CommunityItem, CommunityThemeModal } from 'obsidian';
import MyPlugin from '../main';
import { around, dedupe } from 'monkey-around';
import { PLUGIN_ID } from 'src/constants';
import InstalledFlair from 'src/ui-components/InstalledFlair';
import FavoriteFlair from 'src/ui-components/FavoriteFlair';
import InUseFlair from 'src/ui-components/InUseFlair';
import KnownFlair from 'src/ui-components/KnownFlair';
import ActiveFlair from 'src/ui-components/ActiveFlair';
const MONKEY_KEY = `${PLUGIN_ID}-ThemePluginModal-`;
export default (context: CommunityThemeModal, plugin: MyPlugin) => around(context.constructor.prototype, {
showItem(oldMethod) {
return dedupe(`${MONKEY_KEY}showItem`, oldMethod, async function (item: CommunityItem) {
// console.debug('Call CommunityThemeModal.showItem');
const result = oldMethod && await oldMethod.apply(this, [item]);
const infoEl = await this.detailsEl.getElementsByClassName('community-modal-info')[0];
const infoNameEl = await infoEl?.getElementsByClassName('community-modal-info-name')[0];
// Add aria label to installed flair
new InstalledFlair(infoNameEl);
// Ignore the default theme
if (this.selectedItemId === '') {
return result;
}
const buttonContainerEl = await infoEl?.getElementsByClassName('community-modal-button-container')[0];
// Load the theme lists
plugin.loadFavoriteThemes();
plugin.loadInUseThemes();
plugin.loadKnownThemes();
// Add to the favorite themes a flair to visualize it for the user
const isFavorite = plugin.favoriteThemes.contains(this.selectedItemId);
if (isFavorite) {
new FavoriteFlair(infoNameEl);
}
// Add to the in use themes a flair to visualize it for the user
if (Object.keys(plugin.inUseThemes).contains(this.selectedItemId)) {
new InUseFlair(infoNameEl);
}
// Add to the known themes a flair to visualize it for the user
if (plugin.knownThemes.contains(this.selectedItemId)) {
new KnownFlair(infoNameEl);
}
plugin.registerDomEvent(buttonContainerEl?.createEl('button', { text: isFavorite ? 'Unfavorite' : 'Favorite' }), 'click', async () => {
if (isFavorite) {
plugin.favoriteThemes.remove(this.selectedItemId);
}
else {
plugin.favoriteThemes.push(this.selectedItemId);
}
plugin.saveFavoriteThemes();
// Redraw
infoEl.detach();
await this.showItem(this.items[this.selectedItemId]);
this.update();
});
return result;
});
},
updateItems(oldMethod) {
return dedupe(`${MONKEY_KEY}updateItems`, oldMethod, function () {
// console.debug('Call CommunityThemeModal.updateItems');
// Load the theme lists
plugin.loadFavoriteThemes();
plugin.loadInUseThemes();
plugin.loadKnownThemes();
const result = oldMethod && oldMethod.apply(this);
// Add aria label to installed flair
Object.keys(plugin.app.customCss.themes).forEach(id => {
new InstalledFlair(this.items[id]?.nameEl);
});
// Add aria label to active flair
new ActiveFlair(this.items[plugin.app.customCss.theme]?.nameEl);
// Ignore the default theme
if (this.selectedItemId === '') {
return result;
}
// Add to the favorite themes a flair to visualize it for the user
plugin.favoriteThemes.forEach(id => {
new FavoriteFlair(this.items[id]?.nameEl);
});
// Add to the in use themes a flair to visualize it for the user
Object.keys(plugin.inUseThemes).forEach(id => {
new InUseFlair(this.items[id]?.nameEl);
});
// Add to the known themes a flair to visualize it for the user
plugin.knownThemes.forEach(id => {
new KnownFlair(this.items[id]?.nameEl);
});
return result;
});
},
});

View file

@ -0,0 +1,43 @@
import { Modal } from 'obsidian';
import MyPlugin from '../main';
import { around, dedupe } from 'monkey-around';
import { PLUGIN_ID } from 'src/constants';
import patchCommunityPluginModal from './patch.CommunityPluginModal';
import patchCommunityThemeModal from './patch.CommunityThemeModal';
const MONKEY_KEY = `${PLUGIN_ID}-Modal-`;
export default (plugin: MyPlugin) => around(Modal.prototype, {
open(oldMethod) {
return dedupe(`${MONKEY_KEY}open`, oldMethod, function () {
// console.debug('Call Modal.open');
const result = oldMethod && oldMethod.apply(this);
// Check the opened Modal is the Community plugin browse modal
if (this.containerEl?.querySelector('.mod-community-plugin')) {
// Patch CommunityPluginModal
if (!plugin.uninstallCommunityPluginModalPatch) {
// console.debug('Patch CommunityPluginModal');
plugin.uninstallCommunityPluginModalPatch = patchCommunityPluginModal(this, plugin);
}
}
// Check the opened Modal is the Community themes browse modal
if (this.containerEl?.querySelector('.mod-community-theme')) {
// Patch CommunityThemeModal
if (!plugin.uninstallCommunityThemeModalPatch) {
// console.debug('Patch CommunityThemeModal');
plugin.uninstallCommunityThemeModalPatch = patchCommunityThemeModal(this, plugin);
}
}
if (plugin.uninstallModalPatch && plugin.uninstallCommunityPluginModalPatch && plugin.uninstallCommunityThemeModalPatch) {
// console.debug('Uninstall Modal patch');
plugin.uninstallModalPatch();
plugin.uninstallModalPatch = undefined;
}
return result;
});
},
});

View file

@ -0,0 +1,74 @@
import MyPlugin from '../main';
import { around, dedupe } from 'monkey-around';
import { PluginManifest } from 'obsidian';
import { PLUGIN_ID } from 'src/constants';
const MONKEY_KEY = `${PLUGIN_ID}-PluginManager-`;
export default (plugin: MyPlugin) => around(plugin.app.plugins, {
installPlugin(oldMethod) {
return dedupe(`${MONKEY_KEY}installPlugin`, oldMethod, async function (repo: string, version: string, manifest: PluginManifest) {
// console.debug('Call PluginManager.installPlugin');
const result = oldMethod && await oldMethod.apply(this, [repo, version, manifest]);
// Load the plugin lists
plugin.loadKnownPlugins();
// Add Plugin to known list if not already
if (!plugin.knownPlugins.contains(manifest.id)) {
plugin.knownPlugins.push(manifest.id);
}
plugin.saveKnownPlugins();
return result;
});
},
loadPlugin(oldMethod) {
return dedupe(`${MONKEY_KEY}loadPlugin`, oldMethod, async function (id: string) {
// console.debug('Call PluginManager.loadPlugin');
const currentVault = plugin.app.appId;
// Load the plugin lists
plugin.loadInUsePlugins();
// Add the plugin to the in use list with this vault as user
if (!plugin.inUsePlugins[id] || !Array.isArray(plugin.inUsePlugins[id])) {
plugin.inUsePlugins[id] = [];
}
if (!plugin.inUsePlugins[id].contains(currentVault)) {
plugin.inUsePlugins[id].push(currentVault);
}
plugin.saveInUsePlugins();
return oldMethod && oldMethod.apply(this, [id]);
});
},
unloadPlugin(oldMethod) {
return dedupe(`${MONKEY_KEY}unloadPlugin`, oldMethod, async function (id: string) {
// console.debug('Call PluginManager.unloadPlugin');
const currentVault = plugin.app.appId;
// Load the plugin lists
plugin.loadInUsePlugins();
// Remove current vault from unloaded plugin
if (plugin.inUsePlugins[id] && Array.isArray(plugin.inUsePlugins[id]) && plugin.inUsePlugins[id].contains(currentVault)) {
plugin.inUsePlugins[id].remove(currentVault);
// Cleanup in use if last vault stops using it
if (plugin.inUsePlugins[id].length <= 0) {
delete plugin.inUsePlugins[id];
}
}
plugin.saveInUsePlugins();
return oldMethod && oldMethod.apply(this, [id]);
});
},
});

View file

@ -0,0 +1,30 @@
import { CommunityPluginsSettingTab, SettingsModal, SettingTab } from 'obsidian';
import MyPlugin from '../main';
import { around, dedupe } from 'monkey-around';
import { PLUGIN_ID } from 'src/constants';
import patchCommunityPluginsSettingTab from './patch.CommunityPluginsSettingTab';
const MONKEY_KEY = `${PLUGIN_ID}-SettingsModal-`;
export default (plugin: MyPlugin) => around(plugin.app.setting as SettingsModal, {
openTab(oldMethod) {
return dedupe(`${MONKEY_KEY}openTab`, oldMethod, function (tab: SettingTab) {
// console.debug('Call SettingsModal.openTab');
if (tab.id === 'community-plugins') {
if (!plugin.uninstallCommunityPluginsSettingTabPatch) {
// console.debug('Patch CommunityPluginsSettingTab');
plugin.uninstallCommunityPluginsSettingTabPatch = patchCommunityPluginsSettingTab(tab as CommunityPluginsSettingTab, plugin);
}
}
if (plugin.uninstallSettingsModalPatch && plugin.uninstallCommunityPluginsSettingTabPatch) {
// console.debug('Uninstall SettingsModal patch');
plugin.uninstallSettingsModalPatch();
plugin.uninstallSettingsModalPatch = undefined;
}
return oldMethod && oldMethod.apply(this, [tab]);
});
},
});

View file

@ -0,0 +1,69 @@
import MyPlugin from '../main';
import { around, dedupe } from 'monkey-around';
import { ThemeManifest } from 'obsidian';
import { PLUGIN_ID } from 'src/constants';
const MONKEY_KEY = `${PLUGIN_ID}-StyleManager-installTheme`;
export default (plugin: MyPlugin) => around(plugin.app.customCss, {
installTheme(oldMethod) {
return dedupe(`${MONKEY_KEY}installTheme`, oldMethod, async function (manifest: ThemeManifest, version: string) {
// console.debug('Call StyleManager.installTheme');
const result = oldMethod && await oldMethod.apply(this, [manifest, version]);
// Load the theme lists
plugin.loadKnownThemes();
// Add Theme to known list if not already
if (!plugin.knownThemes.contains(manifest.name)) {
plugin.knownThemes.push(manifest.name);
}
plugin.saveKnownThemes();
return result;
});
},
setTheme(oldMethod) {
return dedupe(`${MONKEY_KEY}setTheme`, oldMethod, function (name: string) {
// console.debug('Call StyleManager.setTheme', this);
const currentVault = plugin.app.appId;
// Load the theme lists
plugin.loadInUseThemes();
// Remove current vault from replaced theme
const oldTheme = this.theme;
if (plugin.inUseThemes[oldTheme] && Array.isArray(plugin.inUseThemes[oldTheme]) && plugin.inUseThemes[oldTheme].contains(currentVault)) {
plugin.inUseThemes[oldTheme].remove(currentVault);
// Cleanup in use if last vault stops using it
if (plugin.inUseThemes[oldTheme].length <= 0) {
delete plugin.inUseThemes[oldTheme];
}
}
const result = oldMethod && oldMethod.apply(this, [name]);
// Ignore the default theme
if (name === '') {
plugin.saveInUseThemes();
return result;
}
// Add the theme to the in use list with this vault as user
if (!plugin.inUseThemes[name] || !Array.isArray(plugin.inUseThemes[name])) {
plugin.inUseThemes[name] = [];
}
if (!plugin.inUseThemes[name].contains(currentVault)) {
plugin.inUseThemes[name].push(currentVault);
}
plugin.saveInUseThemes();
return result;
});
},
});

205
src/types.d.ts vendored
View file

@ -1,37 +1,200 @@
import { } from 'obsidian';
declare module 'obsidian' {
interface App {
appId: string;
setting: SettingsModal;
plugins: PluginManager;
customCss: StyleManager;
}
interface ThemeManifest {
author: string;
authorUrl: string;
dir: string;
minAppVersion: string;
name: string;
version: string;
}
interface CommunityItem {
id: string;
// ...
}
interface CommunityModal {
interface CommunityModal extends Modal {
sortOrder: string;
items: unknown;
itemsVisible: unknown;
selectedItemId: string | null;
dimBackground: unknown;
onCloseCallback: () => void;
update(): void;
returnToGridView(): void;
selectItem(id: string | null): unknown;
showSortMenu(event: unknown): void;
sortItems(items: unknown): unknown;
scrollIntoView(): unknown;
addCustomControls(): unknown;
updateItems(): CommunityItem[];
showItem(item: CommunityItem): void;
// ...
// Additional from this plugin
/**
* This is the backup of the original ``updateItems()`` function. Do not use otherwise than restoring!
*/
originalUpdateItems?(): CommunityItem[];
/**
* This is the backup of the original ``showItem()`` function. Do not use otherwise than restoring!
*/
originalShowItem?(item: CommunityItem): void;
showItem(item: CommunityItem): Promise<void>;
}
interface Modal {
interface CommunityThemeModal extends CommunityModal {
}
interface CommunityPluginModal extends CommunityModal {
setAutoOpen(id: string): CommunityPluginModal;
loadItems(): unknown;
}
interface SettingsModal extends Modal {
openTab(settingTab: SettingTab): void;
}
interface SettingTab {
// Additional form this plugin
/**
* This is the backup of the original ``open()`` function. Do not use otherwise than restoring!
* Used to identify the tab
*
* @private reverse engineered
*/
originalOpen?(): void;
id: string;
setting: unknown;
navEl: HTMLElement;
}
interface CommunityPluginsSettingTab extends SettingTab {
id: 'community-plugins';
installedPlugins: {
listEl: HTMLElement
};
render(e: boolean): void;
renderInstalledPlugin(plugin: CommunityItem, containerEl: HTMLElement, n: unknown, i: unknown, r: unknown): void;
updateSearch(e: unknown): void
}
interface HotkeysSettingTab extends SettingTab {
name: string;
id: 'hotkeys';
setHotkeyScope: unknown;
commandBeingCustomized: unknown;
filterHotkeyScope: unknown;
filterHotkeyEl: unknown;
filterHotkeyActiveEl: unknown;
hotkeyListContainerEl: unknown;
hotkeyFilter: unknown;
}
interface StyleManager extends Component {
app: App;
styleEl: HTMLStyleElement;
extraStyleEls: HTMLStyleElement[];
boundRaw: () => unknown;
oldThemes: unknown[];
/**
* Holds the manifest of the installed themes in this vault
*
* @private reverse engineered
*/
themes: Record<string, ThemeManifest>;
updates: Record<string, { themeInfo: unknown, version: string }>;
snippets: string[];
enabledSnippets: Set<string>;
csscache: Map<number, unknown>;
/**
* Holds the id of the enabled theme
*
* @private reverse engineered
*/
theme: string;
requestLoadTheme: () => unknown;
requestLoadSnippets: () => unknown;
requestReadThemes: () => unknown;
queue: unknown;
onload(): Promise<unknown>;
loadData(): void;
readThemes(e: unknown): unknown;
readSnippets(e: unknown): unknown;
onRaw(e: unknown): void;
loadTheme(e: undefined): unknown;
loadSnippets(): unknown;
loadCss(path: string): Promise<unknown>;
setCssEnabledStatus(e: unknown, t: unknown): void;
setTheme(name: string): void;
setTranslucency(e: unknown): void;
enableTranslucency(): void;
disableTranslucency(): void;
getThemeFolder(): string;
getSnippetsFolder(): string;
getThemePath(e: unknown): string;
getSnippetPath(e: unknown): string;
removeTheme(e: unknown): Promise<unknown>;
downloadLegacyTheme(e: unknown): Promise<unknown>;
isThemeInstalled(e: unknown): unknown;
hasUpdates(): unknown;
checkForUpdate(e: unknown): Promise<unknown>;
checkForUpdates(): Promise<unknown>;
installLegacyTheme(e: unknown): Promise<unknown>;
installTheme(manifest: ThemeManifest, version: string): Promise<unknown>;
getManifest(e: unknown): Promise<unknown>;
}
interface PluginManager {
/**
* Holds the manifest of the installed plugins in this vault
*
* @private reverse engineered
*/
manifests: Record<string, PluginManifest>;
/**
* Holds references to the loaded plugins identified by there id
*
* @private reverse engineered
*/
plugins: Record<string, Plugin>;
/**
* Holds a the ids of the enabled plugins in this vault
*
* @private reverse engineered
*/
enabledPlugins: Set<string>;
/**
* Holds the available updates identified by there id with information about the update
*
* @private reverse engineered
*/
updates: Record<string, { repo: string, version: string, manifest: PluginManifest }>;
requestSaveConfig: () => unknown;
app: App;
onRaw(e: unknown): void;
loadManifests(): Promise<unknown>;
loadManifest(path: string): Promise<unknown>;
loadPlugin(id: string): Promise<unknown>;
unloadPlugin(id: string): Promise<unknown>;
initialize(): Promise<unknown>;
getPluginFolder(): string;
enablePlugin(id: string): Promise<unknown>;
disablePlugin(id: string): Promise<unknown>;
enablePluginAndSave(id: string): Promise<unknown>;
disablePluginAndSave(id: string): Promise<unknown>;
uninstallPlugin(id: string): Promise<unknown>;
getPlugin(id: string): unknown | null;
saveConfig(): Promise<unknown>;
isEnabled(): boolean;
setEnable(e: unknown): Promise<unknown>;
checkForDeprecations(): Promise<unknown>;
isDeprecated(e: unknown): unknown;
installPlugin(repo: string, version: string, manifest: PluginManifest): Promise<unknown>;
checkForUpdates(): Promise<unknown>;
}
}

View file

@ -0,0 +1,11 @@
export default class ActiveFlair {
constructor(parentEl: HTMLElement) {
if (parentEl) {
const flair = Array.from(parentEl.getElementsByClassName('flair')).find(el => el.textContent?.trim().toLowerCase() === 'active');
if (flair) {
flair.setAttribute('aria-label', 'Active theme in this vault');
}
}
}
}

View file

@ -0,0 +1,7 @@
import Flair from './Flair';
export default class FavoriteFlair extends Flair {
constructor(parentEl: HTMLElement) {
super(parentEl, 'Favorite', 'Marked as favorite');
}
}

View file

@ -0,0 +1,13 @@
export default class Flair {
constructor(parentEl: HTMLElement, text: string, ariaLabel: string | null = null) {
if (parentEl) {
const flair = parentEl.createSpan({
cls: 'flair',
text: text,
});
if (ariaLabel) {
flair.setAttribute('aria-label', ariaLabel);
}
}
}
}

View file

@ -0,0 +1,7 @@
import Flair from './Flair';
export default class InUseFlair extends Flair {
constructor(parentEl: HTMLElement) {
super(parentEl, 'In Use', 'Enabled in at least one vault');
}
}

View file

@ -0,0 +1,11 @@
export default class InstalledFlair {
constructor(parentEl: HTMLElement) {
if (parentEl) {
const flair = Array.from(parentEl.getElementsByClassName('flair')).find(el => el.textContent?.trim().toLowerCase() === 'installed');
if (flair) {
flair.setAttribute('aria-label', 'Installed in this vault');
}
}
}
}

View file

@ -0,0 +1,7 @@
import Flair from './Flair';
export default class KnownFlair extends Flair {
constructor(parentEl: HTMLElement) {
super(parentEl, 'Known', 'Installed at least once in a vault');
}
}

60
src/util/GitHub.ts Normal file
View file

@ -0,0 +1,60 @@
/**
* Credits: https://github.com/TfTHacker/obsidian42-brat
*/
import { request } from 'obsidian';
export interface CommunityEntry {
name: string;
author: string;
repo: string;
}
export interface CommunityPlugin extends CommunityEntry {
id: string;
description: string;
}
export interface CommunityTheme extends CommunityEntry {
screenshot: string;
modes: string[];
legacy?: boolean;
}
/**
* Fetch all community plugin entries.
* @returns A list of community plugins
*/
export async function fetchCommunityPluginList(): Promise<CommunityPlugin[] | undefined> {
const URL = 'https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json';
try {
// Do a request to the url
const response = await request({ url: URL });
// Process the response
return (await JSON.parse(response)) as CommunityPlugin[];
}
catch (e) {
(e as Error).message = 'Failed to fetch community plugin list! ' + (e as Error).message;
console.error(e);
}
}
/**
* Fetch all community theme entries.
* @returns A list of community themes
*/
export async function fetchCommunityThemeList(): Promise<CommunityTheme[] | undefined> {
const URL = 'https://raw.githubusercontent.com/obsidianmd/obsidian-releases/refs/heads/master/community-css-themes.json';
try {
// Do a request to the url
const response = await request({ url: URL });
// Process the response
return (await JSON.parse(response)) as CommunityTheme[];
}
catch (e) {
(e as Error).message = 'Failed to fetch community plugin list! ' + (e as Error).message;
console.error(e);
}
}

5
styles.css Normal file
View file

@ -0,0 +1,5 @@
.community-item-name {
display: flex;
flex-wrap: wrap;
align-items: baseline;
}

View file

@ -1,9 +1,11 @@
import { version as manifestVersion, minAppVersion as manifestMinAppVersion } from '../manifest.json';
import { version as packageVersion } from '../package.json';
import { version as packageLockVersion } from '../package.json';
import versions from '../versions.json';
test('Versions matching', () => {
expect(manifestVersion).toBe(packageVersion);
expect(manifestVersion).toBe(packageLockVersion);
expect(Object.keys(versions).find(value => value === manifestVersion)).toBe(manifestVersion);
expect(Object.entries(versions).find(value => value[0] === manifestVersion)?.[1]).toBe(manifestMinAppVersion);
});

View file

@ -1,5 +1,6 @@
import { readFileSync, writeFileSync } from "fs";
import { valid } from "semver";
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
import { valid } from 'semver';
const newVersion = process.argv.find(value => value.startsWith('--new_version'));
if (!newVersion) {
@ -14,23 +15,30 @@ if (!valid(targetVersion)) {
}
// read minAppVersion from manifest.json
let manifestFile = JSON.parse(readFileSync("manifest.json", "utf8"));
let manifestFile = JSON.parse(readFileSync('manifest.json', 'utf8'));
const { minAppVersion } = manifestFile;
if (!minAppVersion || minAppVersion === "") {
throw Error('Missing minAppVersion in "manifest.json"');
if (!minAppVersion || minAppVersion === '') {
throw Error(`Missing minAppVersion in 'manifest.json'`);
}
// update version to target version
manifestFile.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifestFile, null, "\t"));
writeFileSync('manifest.json', JSON.stringify(manifestFile, null, '\t'));
// update version in package
let packageFile = JSON.parse(readFileSync("package.json", "utf8"));
let packageFile = JSON.parse(readFileSync('package.json', 'utf8'));
packageFile.version = targetVersion;
writeFileSync("package.json", JSON.stringify(packageFile, null, "\t"));
writeFileSync('package.json', JSON.stringify(packageFile, null, '\t'));
// update version in package-lock
try {
execSync('npm install', { stdio: 'inherit' });
} catch (error) {
throw Error('npm installed failed: ' + error);
}
// read versions file
let versionsFile = JSON.parse(readFileSync("versions.json", "utf8"));
let versionsFile = JSON.parse(readFileSync('versions.json', 'utf8'));
let keys = Object.keys(versionsFile);
// remove existing versions with same minAppVersion
@ -42,4 +50,4 @@ keys.forEach(key => {
// update versions.json with target version and minAppVersion from manifest.json
versionsFile[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versionsFile, null, "\t"));
writeFileSync('versions.json', JSON.stringify(versionsFile, null, '\t'));

View file

@ -1,3 +1,3 @@
{
"1.1.1": "1.8.10"
"1.4.0": "1.8.10"
}