From 8f706c9372050ebe698c5baa94b680b17f2fc444 Mon Sep 17 00:00:00 2001
From: 4Source <38220764+4Source@users.noreply.github.com>
Date: Mon, 2 Mar 2026 21:04:43 +0100
Subject: [PATCH] [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
---
.github/CONTRIBUTING.md | 2 +
.gitignore | 1 +
README.md | 57 +++++
src/constants.ts | 3 +-
src/main.ts | 318 ++++++++++++++++++++++------
src/modals/CommunitySuggestModal.ts | 35 +++
src/modals/DialogModal.ts | 77 +++++++
src/modals/StringSuggestModal.ts | 23 ++
src/types.d.ts | 17 +-
src/util/GitHub.ts | 60 ++++++
10 files changed, 531 insertions(+), 62 deletions(-)
create mode 100644 src/modals/CommunitySuggestModal.ts
create mode 100644 src/modals/DialogModal.ts
create mode 100644 src/modals/StringSuggestModal.ts
create mode 100644 src/util/GitHub.ts
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 452faeb..a0c3550 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -26,6 +26,8 @@ If not related issue exists, open a new one using the appropriate
+# Commands
+If you prefer managing your favorites list with commands here are the available commands
+- [Add plugin to favorite list](#add-plugin-to-favorite-list)
+- [Add theme to favorite list](#add-theme-to-favorite-list)
+- [Remove plugin from favorite list](#remove-plugin-from-favorite-list)
+- [Remove theme from favorite list](#remove-theme-from-favorite-list)
+- [Clear the plugin favorites lists](#clear-the-plugin-favorites-lists)
+- [Clear the theme favorites lists](#clear-the-theme-favorites-lists)
+- [Manually load the favorites lists](#manually-load-the-favorites-lists)
+- [Manually save the favorites lists](#manually-save-the-favorites-lists)
+- [Export favorite lists to file](#export-favorite-lists-to-file)
+- [Import favorite lists from file](#import-favorite-lists-from-file)
+
+## 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.
+
+## 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.
+
+## Remove plugin from favorite list
+Search and select from your current favorite plugins to remove it from your favorites list.
+
+## Remove theme from favorite list
+Search and select from your current favorite themes to remove it from your favorites list.
+
+## Clear the plugin favorites lists
+Permanently removes ***all*** plugins from your favorites list after confirmation.
+> [!WARNING]
+> This action cannot be undone.
+
+## Clear the theme favorites lists
+Permanently removes ***all*** themes from your favorites list after confirmation.
+> [!WARNING]
+> This action cannot be undone.
+
+## Manually load the favorites lists
+Reloads the saved favorite plugins and themes. Use this to restore your favorites 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 favorites lists
+Forces the plugin to immediately save your current favorite plugins and themes. Use this if you want to ensure your favorites 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 favorite lists to file
+Creates a backup file containing your current favorite plugins and themes. The backup is saved as a timestamped JSON file inside the plugin’s backup folder within your vault configuration directory.
+
+## Import favorite lists from file
+Restores favorite plugins and themes from a previously created backup file. You will be prompted to select a backup file from the plugin’s backup folder. After confirmation imported favorites will replaces your current favorites with the contents of the selected backup.
+> [!WARNING]
+> This will overwrite current favorite lists and can't be undone.
+
# Contribution
Feel free to contribute.
diff --git a/src/constants.ts b/src/constants.ts
index 20ab87b..ed740f7 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,4 +1,5 @@
-export const PLUGIN_ID = 'favorite';
+export const PLUGIN_ID = 'favorites';
+export const PLUGIN_BACKUP_BASE_PATH = `/plugins/${PLUGIN_ID}/backup`;
export const MONKEY_KEY_MODAL_OPEN = `${PLUGIN_ID}-Modal-open`;
export const MONKEY_KEY_SETTINGS_MODAL_OPEN_TAB = `${PLUGIN_ID}-SettingsModal-openTab`;
export const MONKEY_KEY_PLUGIN_BROWSER_MODAL_UPDATE_ITEMS = `${PLUGIN_ID}-CommunityPluginModal-updateItems`;
diff --git a/src/main.ts b/src/main.ts
index 251dbca..0c6b694 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,14 +1,16 @@
-import { CommunityItem, CommunityModal, CommunityPluginsSettingTab, Modal, Plugin, setIcon, SettingsModal, SettingTab, setTooltip } from 'obsidian';
+import { CommunityItem, CommunityPluginsSettingTab, Modal, normalizePath, Notice, Plugin, setIcon, SettingsModal, SettingTab, setTooltip } 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, MONKEY_KEY_SETTINGS_MODAL_OPEN_TAB, MONKEY_KEY_COMMUNITY_PLUGIN_SETTINGS_TAB_RENDER_INSTALLED_PLUGIN } from './constants';
+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, MONKEY_KEY_SETTINGS_MODAL_OPEN_TAB, MONKEY_KEY_COMMUNITY_PLUGIN_SETTINGS_TAB_RENDER_INSTALLED_PLUGIN, PLUGIN_BACKUP_BASE_PATH } from './constants';
+import { DialogModal } from './modals/DialogModal';
+import { CommunitySuggestModal } from './modals/CommunitySuggestModal';
+import { CommunityPlugin, CommunityTheme, fetchCommunityPluginList, fetchCommunityThemeList } from './util/GitHub';
+import { StringSuggestModal } from './modals/StringSuggestModal';
export default class FavoritesPlugin extends Plugin {
pluginsKey: string;
themesKey: string;
- favoritePlugins?: string[];
- favoriteThemes?: string[];
- communityPluginModalPrototype?: CommunityModal;
- communityThemesModalPrototype?: CommunityModal;
+ favoritePlugins: string[];
+ favoriteThemes: string[];
uninstallModalOpen?: () => void;
uninstallSettingsModalOpenTab?: () => void;
uninstallPluginBrowserModalUpdateItems?: () => void;
@@ -31,6 +33,7 @@ export default class FavoritesPlugin extends Plugin {
throw Error('Missing environment variable \'FAVORITE_THEMES_KEY\'');
}
console.debug(`Plugins key: ${this.pluginsKey} Themes key: ${this.themesKey}`);
+ this.loadFavorites();
// 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;
@@ -57,19 +60,19 @@ export default class FavoritesPlugin extends Plugin {
plugin.loadFavoritePlugins();
const selectedPluginID = args[0].id;
- const isFavorite = plugin.favoritePlugins?.contains(selectedPluginID);
+ const isFavorite = plugin.favoritePlugins.contains(selectedPluginID);
const favEl = createDiv('clickable-icon extra-setting-button');
plugin.registerDomEvent(favEl, 'click', () => {
if (isFavorite) {
- plugin.favoritePlugins?.remove(selectedPluginID);
+ plugin.favoritePlugins.remove(selectedPluginID);
}
else {
- plugin.favoritePlugins?.push(selectedPluginID);
+ plugin.favoritePlugins.push(selectedPluginID);
}
- plugin.saveFavorites();
+ plugin.saveFavoritesPlugins();
// Redraw
(tab as CommunityPluginsSettingTab).render(false);
@@ -131,16 +134,14 @@ export default class FavoritesPlugin extends Plugin {
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',
- });
- }
- });
- }
+ plugin.favoritePlugins.forEach(id => {
+ if (this.items && this.items[id]?.nameEl) {
+ this.items[id].nameEl.createSpan({
+ cls: 'flair',
+ text: 'favorite',
+ });
+ }
+ });
return result;
});
@@ -167,7 +168,7 @@ export default class FavoritesPlugin extends Plugin {
// Load the favorite plugins
plugin.loadFavoritePlugins();
- const isFavorite = plugin.favoritePlugins?.contains(this.selectedItemId);
+ const isFavorite = plugin.favoritePlugins.contains(this.selectedItemId);
// Add to the favorite plugins a tag to visualize it for the user
if (isFavorite) {
@@ -181,13 +182,13 @@ export default class FavoritesPlugin extends Plugin {
plugin.registerEvent(buttonContainerEl?.createEl('button', { text: isFavorite ? 'Unfavorite' : 'Favorite' }).addEventListener('click', () => {
if (isFavorite) {
- plugin.favoritePlugins?.remove(this.selectedItemId);
+ plugin.favoritePlugins.remove(this.selectedItemId);
}
else {
- plugin.favoritePlugins?.push(this.selectedItemId);
+ plugin.favoritePlugins.push(this.selectedItemId);
}
- plugin.saveFavorites();
+ plugin.saveFavoritesPlugins();
// Redraw
infoEl.detach();
@@ -227,16 +228,14 @@ export default class FavoritesPlugin extends Plugin {
}
// 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',
- });
- }
- });
- }
+ plugin.favoriteThemes.forEach(id => {
+ if (this.items && this.items[id]?.nameEl) {
+ this.items[id].nameEl.createSpan({
+ cls: 'flair',
+ text: 'favorite',
+ });
+ }
+ });
return result;
});
@@ -268,7 +267,7 @@ export default class FavoritesPlugin extends Plugin {
// Load the favorite themes
plugin.loadFavoriteThemes();
- const isFavorite = plugin.favoriteThemes?.contains(this.selectedItemId);
+ const isFavorite = plugin.favoriteThemes.contains(this.selectedItemId);
// Add to the favorite themes a tag to visualize it for the user
if (isFavorite) {
@@ -282,13 +281,13 @@ export default class FavoritesPlugin extends Plugin {
plugin.registerEvent(buttonContainerEl?.createEl('button', { text: isFavorite ? 'Unfavorite' : 'Favorite' }).addEventListener('click', () => {
if (isFavorite) {
- plugin.favoriteThemes?.remove(this.selectedItemId);
+ plugin.favoriteThemes.remove(this.selectedItemId);
}
else {
- plugin.favoriteThemes?.push(this.selectedItemId);
+ plugin.favoriteThemes.push(this.selectedItemId);
}
- plugin.saveFavorites();
+ plugin.saveFavoritesThemes();
// Redraw
infoEl.detach();
@@ -321,13 +320,151 @@ export default class FavoritesPlugin extends Plugin {
console.warn('Modal.open already patched!');
}
- // TODO: Redraw modals if local storage 'favorite-plugins' or 'favorite-themes' changed
+ this.addCommand({
+ id: 'save-favorites-lists',
+ name: 'Manually save the favorites lists',
+ callback: () => {
+ this.saveFavorites();
+ new Notice('Saved favorite lists');
+ },
+ });
- /*
- * 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
- */
+ this.addCommand({
+ id: 'load-favorites-lists',
+ name: 'Manually load the favorites lists',
+ callback: () => {
+ this.loadFavorites();
+ new Notice('Loaded favorite lists');
+ },
+ });
+
+ this.addCommand({
+ id: 'clear-plugin-favorites-lists',
+ name: 'Clear the plugin favorites lists',
+ callback: () => {
+ this.loadFavoritePlugins();
+ const numberClear = this.favoritePlugins.length;
+ new DialogModal(this.app, `Clear ${numberClear} plugin(s) permanently from favorite list?`, '', () => {
+ this.favoritePlugins = [];
+ this.saveFavoritesPlugins();
+ new Notice(`Cleared ${numberClear} plugins from favorite list`);
+ }, () => {
+ new Notice('Canceled clear plugins from favorite list');
+ }, 'Clear', true, 'Cancel', false).open();
+ },
+ });
+
+ this.addCommand({
+ id: 'clear-theme-favorites-lists',
+ name: 'Clear the theme favorites lists',
+ callback: () => {
+ this.loadFavoriteThemes();
+ const numberClear = this.favoriteThemes.length;
+ new DialogModal(this.app, `Clear ${numberClear} theme(s) permanently from favorite list?`, '', () => {
+ this.favoriteThemes = [];
+ this.saveFavoritesThemes();
+ new Notice(`Cleared ${numberClear} themes from favorite list`);
+ }, () => {
+ new Notice('Canceled clear themes from favorite list');
+ }, 'Clear', true, 'Cancel', false).open();
+ },
+ });
+
+ this.addCommand({
+ id: 'search-and-add-plugin-to-favorite-list',
+ name: 'Add plugin to favorite list',
+ callback: async () => {
+ const items = await fetchCommunityPluginList();
+ if (!items) {
+ new Notice('Failed to fetch community plugins. See console for more information.');
+ return;
+ }
+
+ new CommunitySuggestModal(this.app, 'Select plugin which should be added to favorites list...', items, (result) => {
+ this.loadFavoritePlugins();
+ this.favoritePlugins.push(result.id);
+ this.saveFavoritesPlugins();
+ new Notice(`Added ${result.name} to favorite list`);
+ }).open();
+ },
+ });
+
+ this.addCommand({
+ id: 'search-and-add-theme-to-favorite-list',
+ name: 'Add theme to favorite list',
+ callback: async () => {
+ const items = await fetchCommunityThemeList();
+ if (!items) {
+ new Notice('Failed to fetch community themes. See console for more information.');
+ return;
+ }
+
+ new CommunitySuggestModal(this.app, 'Select theme which should be added to favorites list...', items, (result) => {
+ this.loadFavoriteThemes();
+ this.favoriteThemes.push(result.name);
+ this.saveFavoritesThemes();
+ new Notice(`Added ${result.name} to favorite list`);
+ }).open();
+ },
+ });
+
+ this.addCommand({
+ id: 'search-and-remove-plugin-to-favorite-list',
+ name: 'Remove plugin from favorite list',
+ callback: async () => {
+ this.loadFavoritePlugins();
+ if (this.favoritePlugins.length <= 0) {
+ new Notice('Plugin favorite list is empty');
+ return;
+ }
+ const communityItems = await fetchCommunityPluginList();
+ const installedItems = Object.values(this.app.plugins.manifests);
+ const items = this.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(this.app, 'Select plugin which should be removed from the favorites list...', items, (result) => {
+ this.favoritePlugins.remove(result.id);
+ this.saveFavoritesPlugins();
+ new Notice(`Removed ${result.name} from favorite list`);
+ }).open();
+ },
+ });
+
+ this.addCommand({
+ id: 'search-and-remove-theme-to-favorite-list',
+ name: 'Remove theme from favorite list',
+ callback: async () => {
+ this.loadFavoriteThemes();
+ if (this.favoriteThemes.length <= 0) {
+ new Notice('Theme favorite list is empty');
+ return;
+ }
+ const communityItems = await fetchCommunityThemeList();
+ const installedItems = Object.values(this.app.customCss.themes);
+ const items = this.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(this.app, 'Select theme which should be removed from the favorites list...', items, (result) => {
+ this.favoriteThemes.remove(result.name);
+ this.saveFavoritesThemes();
+ new Notice(`Removed ${result.name} from favorite list`);
+ }).open();
+ },
+ });
+
+ this.addCommand({
+ id: 'export-favorite-lists',
+ name: 'Export favorite lists to file',
+ callback: () => { this.exportFavorites(); },
+ });
+
+ this.addCommand({
+ id: 'import-favorite-lists',
+ name: 'Import favorite lists from file',
+ callback: () => { this.importFavorites(); },
+ });
}
onunload() {
@@ -384,23 +521,84 @@ export default class FavoritesPlugin extends Plugin {
this.favoriteThemes = JSON.parse(localStorage.getItem(this.themesKey) || '[]');
}
- saveFavorites() {
- if (this.favoritePlugins) {
- if (this.favoritePlugins.length > 0) {
- localStorage.setItem(this.pluginsKey, JSON.stringify(this.favoritePlugins));
- }
- else {
- localStorage.removeItem(this.pluginsKey);
- }
- }
+ loadFavorites() {
+ this.loadFavoritePlugins();
+ this.loadFavoriteThemes();
+ }
- if (this.favoriteThemes) {
- if (this.favoriteThemes.length > 0) {
- localStorage.setItem(this.themesKey, JSON.stringify(this.favoriteThemes));
- }
- else {
- localStorage.removeItem(this.themesKey);
- }
+ saveFavoritesPlugins() {
+ if (this.favoritePlugins.length > 0) {
+ localStorage.setItem(this.pluginsKey, JSON.stringify(this.favoritePlugins));
+ }
+ else {
+ localStorage.removeItem(this.pluginsKey);
}
}
+
+ saveFavoritesThemes() {
+ if (this.favoriteThemes.length > 0) {
+ localStorage.setItem(this.themesKey, JSON.stringify(this.favoriteThemes));
+ }
+ else {
+ localStorage.removeItem(this.themesKey);
+ }
+ }
+
+ saveFavorites() {
+ this.saveFavoritesPlugins();
+ this.saveFavoritesThemes();
+ }
+
+ async exportFavorites() {
+ if (this.favoritePlugins.length <= 0 && this.favoriteThemes.length <= 0) {
+ new Notice('No favorite lists to backup');
+ return;
+ }
+
+ // Ensure backup path does exist
+ const backupPath = normalizePath(`${this.app.vault.configDir}${PLUGIN_BACKUP_BASE_PATH}`);
+ await this.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 this.app.vault.adapter.write(backupFilePath, JSON.stringify({ plugins: this.favoritePlugins, themes: this.favoriteThemes }));
+ new Notice(`Favorites list backup successful written to: ${backupFilePath}`);
+ }
+
+ async importFavorites() {
+ const backupPath = normalizePath(`${this.app.vault.configDir}${PLUGIN_BACKUP_BASE_PATH}`);
+ if (!(await this.app.vault.adapter.exists(backupPath))) {
+ // Backup path does not exist
+ new Notice(`Backup path does not exist: ${backupPath}`);
+ return;
+ }
+
+ const backupFiles = (await this.app.vault.adapter.list(backupPath)).files;
+ if (backupFiles.length <= 0) {
+ // Backup path is empty
+ new Notice('Backup path is empty');
+ return;
+ }
+
+ new StringSuggestModal(this.app, 'Select backup file to load from', backupFiles, async (result) => {
+ const content = await this.app.vault.adapter.read(result);
+ const json = JSON.parse(content);
+ const loadedPlugins = json.plugins || [];
+ const loadedThemes = json.themes || [];
+ new DialogModal(this.app, 'Are you sure you want to overwrite current favorite lists?', `The list of favorite plugins will change from ${this.favoritePlugins.length} items to ${loadedPlugins.length} items, and the list of favorite themes will change from ${this.favoriteThemes.length} items to ${loadedThemes.length} items.`, () => {
+ this.favoritePlugins = loadedPlugins;
+ this.favoriteThemes = loadedThemes;
+ this.saveFavorites();
+
+ new Notice('Loaded favorites backup successfully');
+ }, () => {
+ new Notice('Canceled loading favorites backup by user');
+ }, 'Overwrite', true, 'Cancel', false).open();
+ }).open();
+ }
}
diff --git a/src/modals/CommunitySuggestModal.ts b/src/modals/CommunitySuggestModal.ts
new file mode 100644
index 0000000..c740100
--- /dev/null
+++ b/src/modals/CommunitySuggestModal.ts
@@ -0,0 +1,35 @@
+import { App, FuzzyMatch, FuzzySuggestModal, renderResults } from 'obsidian';
+
+export class CommunitySuggestModal extends FuzzySuggestModal {
+ 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, 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);
+ }
+}
\ No newline at end of file
diff --git a/src/modals/DialogModal.ts b/src/modals/DialogModal.ts
new file mode 100644
index 0000000..fdbc2b1
--- /dev/null
+++ b/src/modals/DialogModal.ts
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/modals/StringSuggestModal.ts b/src/modals/StringSuggestModal.ts
new file mode 100644
index 0000000..f503187
--- /dev/null
+++ b/src/modals/StringSuggestModal.ts
@@ -0,0 +1,23 @@
+import { App, FuzzySuggestModal } from 'obsidian';
+
+export class StringSuggestModal extends FuzzySuggestModal {
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/types.d.ts b/src/types.d.ts
index fa845a4..29161f0 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -2,7 +2,22 @@ import { } from 'obsidian';
declare module 'obsidian' {
interface App {
- setting: SettingsModal
+ setting: SettingsModal;
+ plugins: {
+ manifests: Record;
+ };
+ customCss: {
+ themes: Record;
+ }
+ }
+
+ interface ThemeManifest {
+ author: string;
+ authorUrl: string;
+ dir: string;
+ minAppVersion: string;
+ name: string;
+ version: string;
}
interface CommunityItem {
diff --git a/src/util/GitHub.ts b/src/util/GitHub.ts
new file mode 100644
index 0000000..2f38d4d
--- /dev/null
+++ b/src/util/GitHub.ts
@@ -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 {
+ 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 {
+ 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);
+ }
+}