From ebfd81563081f4ce881435aaef356abfec63ae23 Mon Sep 17 00:00:00 2001 From: DecafDev <40307803+decaf-dev@users.noreply.github.com> Date: Sat, 6 Jul 2024 14:57:47 -0600 Subject: [PATCH] Add file icons (#155) * feat: add file icons setting * feat: render file icons --- FEATURES.md | 2 + manifest.json | 2 +- package.json | 2 +- src/constants.ts | 17 +- src/event/types.ts | 20 +- src/main.ts | 148 ++++-- src/migrations/index.ts | 13 +- src/migrations/migrate_1_17_0.ts | 30 +- src/migrations/migrate_1_21_0.ts | 14 + src/obsidian/vault-explorer-settings-tab.ts | 491 +++++++++++------- src/svelte/app/components/feed-card.svelte | 32 +- src/svelte/app/components/feed-view.svelte | 9 +- src/svelte/app/components/grid-card.svelte | 32 +- src/svelte/app/components/grid-view.svelte | 4 +- src/svelte/app/components/list-item.svelte | 35 +- src/svelte/app/components/list-view.svelte | 8 +- src/svelte/app/index.svelte | 8 +- src/svelte/app/services/file-content-store.ts | 7 +- .../app/services/filters/search-filter.ts | 6 +- .../app/services/utils/file-icon-utils.ts | 63 +++ src/svelte/app/services/utils/render-utils.ts | 4 +- src/svelte/app/types.ts | 4 +- .../shared/components/icon-button.svelte | 13 +- src/svelte/shared/components/icon.svelte | 29 +- src/svelte/shared/services/get-svg-data.ts | 10 + src/types/index.guard.ts | 4 +- src/types/index.ts | 1 + src/types/types-1.20.0.ts | 298 +++++++++++ versions.json | 3 +- 29 files changed, 1009 insertions(+), 300 deletions(-) create mode 100644 src/migrations/migrate_1_21_0.ts create mode 100644 src/svelte/app/services/utils/file-icon-utils.ts create mode 100644 src/svelte/shared/services/get-svg-data.ts create mode 100644 src/types/types-1.20.0.ts diff --git a/FEATURES.md b/FEATURES.md index 1a66566..d8f0074 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -51,6 +51,8 @@ Features are what the software should do. | Left/right buttons to allow a user to scroll between groups | #accessibility | | Duplicate a filter rule | #filter | | Random sort | #filter | +| Add file icons | #filter | +| Add file icon setting | #setting | ## Event callbacks diff --git a/manifest.json b/manifest.json index 6b2e600..35e168b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "vault-explorer", "name": "Vault Explorer", - "version": "1.20.0", + "version": "1.21.0", "minAppVersion": "1.4.13", "description": "Explore your vault in visual format", "author": "DecafDev", diff --git a/package.json b/package.json index 5f911a0..5493c91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-vault-explorer", - "version": "1.20.0", + "version": "1.21.0", "description": "Explore your vault in visual format", "main": "main.js", "scripts": { diff --git a/src/constants.ts b/src/constants.ts index 00a7f04..93e4ea0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,15 +19,15 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = { filters: { search: { isEnabled: true, - value: "" + value: "", }, favorites: { isEnabled: true, - value: false + value: false, }, timestamp: { isEnabled: true, - value: "all" + value: "all", }, sort: { isEnabled: true, @@ -36,8 +36,8 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = { custom: { isEnabled: true, selectedGroupId: "", - groups: [] - } + groups: [], + }, }, views: { dashboard: { @@ -60,13 +60,14 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = { }, related: { isEnabled: false, - } + }, }, currentView: TExplorerView.GRID, titleWrapping: "normal", enableClockUpdates: true, + enableFileIcons: false, enableScrollButtons: true, pageSize: 50, viewOrder: [TExplorerView.GRID, TExplorerView.LIST, TExplorerView.FEED], - pluginVersion: null -} + pluginVersion: null, +}; diff --git a/src/event/types.ts b/src/event/types.ts index ab10517..2e49348 100644 --- a/src/event/types.ts +++ b/src/event/types.ts @@ -1,3 +1,21 @@ -export type PluginEvent = "file-rename" | "file-create" | "file-delete" | "file-modify" | "metadata-change" | "properties-filter-update" | "folder-rename" | "folder-delete" | "folder-create" | "page-size-setting-change" | "title-wrapping-setting-change" | "property-setting-change" | "device-registration-change" | "clock-updates-setting-change" | "filter-toggle-setting-change" | "scroll-buttons-setting-change" | "view-toggle-setting-change"; +export type PluginEvent = + | "file-rename" + | "file-create" + | "file-delete" + | "file-modify" + | "metadata-change" + | "properties-filter-update" + | "folder-rename" + | "folder-delete" + | "folder-create" + | "page-size-setting-change" + | "title-wrapping-setting-change" + | "property-setting-change" + | "device-registration-change" + | "clock-updates-setting-change" + | "filter-toggle-setting-change" + | "scroll-buttons-setting-change" + | "view-toggle-setting-change" + | "file-icons-setting-change"; export type EventCallback = (...data: unknown[]) => void; diff --git a/src/main.ts b/src/main.ts index ece27d7..2656540 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,18 +1,22 @@ -import { Plugin, TAbstractFile, TFile, TFolder } from 'obsidian'; +import { Plugin, TAbstractFile, TFile, TFolder } from "obsidian"; -import VaultExplorerView from './obsidian/vault-explorer-view'; -import VaultExplorerSettingsTab from './obsidian/vault-explorer-settings-tab'; +import VaultExplorerView from "./obsidian/vault-explorer-view"; +import VaultExplorerSettingsTab from "./obsidian/vault-explorer-settings-tab"; -import { VaultExplorerPluginSettings } from './types'; -import { DEFAULT_SETTINGS, HOVER_LINK_SOURCE_ID, VAULT_EXPLORER_VIEW } from './constants'; -import _ from 'lodash'; -import EventManager from './event/event-manager'; -import { preformMigrations } from './migrations'; -import Logger from 'js-logger'; -import { formatMessageForLogger, stringToLogLevel } from './logger'; -import { moveFocus } from './focus-utils'; -import { loadDeviceId } from './svelte/shared/services/device-id-utils'; -import License from './svelte/shared/services/license'; +import { VaultExplorerPluginSettings } from "./types"; +import { + DEFAULT_SETTINGS, + HOVER_LINK_SOURCE_ID, + VAULT_EXPLORER_VIEW, +} from "./constants"; +import _ from "lodash"; +import EventManager from "./event/event-manager"; +import { preformMigrations } from "./migrations"; +import Logger from "js-logger"; +import { formatMessageForLogger, stringToLogLevel } from "./logger"; +import { moveFocus } from "./focus-utils"; +import { loadDeviceId } from "./svelte/shared/services/device-id-utils"; +import License from "./svelte/shared/services/license"; export default class VaultExplorerPlugin extends Plugin { settings: VaultExplorerPluginSettings = DEFAULT_SETTINGS; @@ -40,7 +44,10 @@ export default class VaultExplorerPlugin extends Plugin { }); this.registerEvents(); - this.registerHoverLinkSource(HOVER_LINK_SOURCE_ID, { display: this.manifest.name, defaultMod: true }); + this.registerHoverLinkSource(HOVER_LINK_SOURCE_ID, { + display: this.manifest.name, + defaultMod: true, + }); this.addSettingTab(new VaultExplorerSettingsTab(this.app, this)); this.app.workspace.onLayoutReady(() => { @@ -49,60 +56,79 @@ export default class VaultExplorerPlugin extends Plugin { await loadDeviceId(); await License.getInstance().verifyLicense(); - } private registerEvents() { //Callback if the file is renamed or moved //This callback is already debounced by Obsidian - this.registerEvent(this.app.vault.on("rename", (file: TAbstractFile, oldPath: string) => { - if (file instanceof TFolder) { - EventManager.getInstance().emit("folder-rename", oldPath, file); - } else if (file instanceof TFile) { - EventManager.getInstance().emit("file-rename", oldPath, file); - } - })); + this.registerEvent( + this.app.vault.on( + "rename", + (file: TAbstractFile, oldPath: string) => { + if (file instanceof TFolder) { + EventManager.getInstance().emit( + "folder-rename", + oldPath, + file + ); + } else if (file instanceof TFile) { + EventManager.getInstance().emit( + "file-rename", + oldPath, + file + ); + } + } + ) + ); //Callback if a file is deleted //This callback is already debounced by Obsidian - this.registerEvent(this.app.vault.on("delete", (file: TAbstractFile) => { - if (file instanceof TFolder) { - EventManager.getInstance().emit("folder-delete", file.path); - } else - if (file instanceof TFile) { + this.registerEvent( + this.app.vault.on("delete", (file: TAbstractFile) => { + if (file instanceof TFolder) { + EventManager.getInstance().emit("folder-delete", file.path); + } else if (file instanceof TFile) { EventManager.getInstance().emit("file-delete", file.path); } - })); + }) + ); //Callback if a file is created //This callback is already debounced by Obsidian - this.registerEvent(this.app.vault.on("create", (file: TAbstractFile) => { - //For some reason Obsidian will call this event for every file in the vault when the plugin is loaded - //We need to ignore these events - if (!this.layoutReady) return; + this.registerEvent( + this.app.vault.on("create", (file: TAbstractFile) => { + //For some reason Obsidian will call this event for every file in the vault when the plugin is loaded + //We need to ignore these events + if (!this.layoutReady) return; - if (file instanceof TFolder) { - EventManager.getInstance().emit("folder-create", file); - } else if (file instanceof TFile) { - EventManager.getInstance().emit("file-create", file); - } - })); + if (file instanceof TFolder) { + EventManager.getInstance().emit("folder-create", file); + } else if (file instanceof TFile) { + EventManager.getInstance().emit("file-create", file); + } + }) + ); //Callback if a file is modified //This callback is already debounced by Obsidian - this.registerEvent(this.app.vault.on("modify", (file: TAbstractFile) => { - if (file instanceof TFile) { - if (file.extension !== "md") return; - EventManager.getInstance().emit("file-modify", file); - } - })); + this.registerEvent( + this.app.vault.on("modify", (file: TAbstractFile) => { + if (file instanceof TFile) { + if (file.extension !== "md") return; + EventManager.getInstance().emit("file-modify", file); + } + }) + ); //Callback if the frontmatter is changed //This callback is already debounced by Obsidian - this.registerEvent(this.app.metadataCache.on("changed", (file) => { - if (file.extension !== "md") return; - EventManager.getInstance().emit("metadata-change", file); - })); + this.registerEvent( + this.app.metadataCache.on("changed", (file) => { + if (file.extension !== "md") return; + EventManager.getInstance().emit("metadata-change", file); + }) + ); this.registerDomEvent(document, "keydown", (event) => { if (event.key === "ArrowLeft") { @@ -111,21 +137,20 @@ export default class VaultExplorerPlugin extends Plugin { moveFocus("next"); } }); - } - onunload() { - - } + onunload() {} async loadSettings() { - const loadedData: Record | null = await this.loadData(); + const loadedData: Record | null = + await this.loadData(); let currentData: Record = {}; if (loadedData !== null) { //This will be undefined if the settings are from a version before 0.3.0 - const loadedVersion = loadedData["pluginVersion"] as string ?? null; + const loadedVersion = + (loadedData["pluginVersion"] as string) ?? null; if (loadedVersion !== null) { const newData = preformMigrations(loadedVersion, loadedData); currentData = newData; @@ -140,8 +165,19 @@ export default class VaultExplorerPlugin extends Plugin { } async saveSettings() { - Logger.trace({ fileName: "main.ts", functionName: "saveSettings", message: "called" }); - Logger.debug({ fileName: "main.ts", functionName: "saveSettings", message: "saving settings" }, this.settings); + Logger.trace({ + fileName: "main.ts", + functionName: "saveSettings", + message: "called", + }); + Logger.debug( + { + fileName: "main.ts", + functionName: "saveSettings", + message: "saving settings", + }, + this.settings + ); await this.saveData(this.settings); } diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 70365ec..aa64d4e 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -13,6 +13,7 @@ import Migrate_1_17_0 from "./migrate_1_17_0"; import Migrate_1_13_0 from "./migrate_1_13_0"; import Migrate_1_10_0 from "./migrate_1_10_0"; import { isVersionLessThan } from "src/utils"; +import Migrate_1_21_0 from "./migrate_1_21_0"; const migrations: TMigration[] = [ { @@ -80,9 +81,17 @@ const migrations: TMigration[] = [ to: "1.17.0", migrate: Migrate_1_17_0, }, + { + from: "1.20.0", + to: "1.21.0", + migrate: Migrate_1_21_0, + }, ]; -export const preformMigrations = (settingsVersion: string, data: Record) => { +export const preformMigrations = ( + settingsVersion: string, + data: Record +) => { let updatedData = structuredClone(data); for (const migration of migrations) { @@ -96,4 +105,4 @@ export const preformMigrations = (settingsVersion: string, data: Record) { - const typedData = (data as unknown) as VaultExplorerPluginSettings_1_16_0; - const newData: VaultExplorerPluginSettings = { + const typedData = data as unknown as VaultExplorerPluginSettings_1_16_0; + const newData: VaultExplorerPluginSettings_1_20_0 = { ...typedData, views: { dashboard: { - isEnabled: false + isEnabled: false, }, grid: { - isEnabled: true + isEnabled: true, }, list: { - isEnabled: true + isEnabled: true, }, table: { - isEnabled: false + isEnabled: false, }, feed: { - isEnabled: true + isEnabled: true, }, recommended: { - isEnabled: false + isEnabled: false, }, related: { - isEnabled: false - } + isEnabled: false, + }, }, viewOrder: typedData.views.order as unknown as TExplorerView[], enableClockUpdates: typedData.views.enableClockUpdates, - currentView: typedData.views.currentView as unknown as TExplorerView, - titleWrapping: typedData.views.titleWrapping - } + currentView: typedData.views + .currentView as unknown as TExplorerView, + titleWrapping: typedData.views.titleWrapping, + }; delete (newData as any).views.order; delete (newData as any).views.currentView; delete (newData as any).views.enableClockUpdates; diff --git a/src/migrations/migrate_1_21_0.ts b/src/migrations/migrate_1_21_0.ts new file mode 100644 index 0000000..6204e15 --- /dev/null +++ b/src/migrations/migrate_1_21_0.ts @@ -0,0 +1,14 @@ +import { VaultExplorerPluginSettings } from "src/types"; +import MigrationInterface from "./migration_interface"; +import { VaultExplorerPluginSettings_1_20_0 } from "src/types/types-1.20.0"; + +export default class Migrate_1_21_0 implements MigrationInterface { + migrate(data: Record) { + const typedData = data as unknown as VaultExplorerPluginSettings_1_20_0; + const newData: VaultExplorerPluginSettings = { + ...typedData, + enableFileIcons: true, + }; + return newData as unknown as Record; + } +} diff --git a/src/obsidian/vault-explorer-settings-tab.ts b/src/obsidian/vault-explorer-settings-tab.ts index 80ed5a4..3f121dd 100644 --- a/src/obsidian/vault-explorer-settings-tab.ts +++ b/src/obsidian/vault-explorer-settings-tab.ts @@ -1,7 +1,17 @@ import { App, PluginSettingTab, Setting } from "obsidian"; import VaultExplorerPlugin from "src/main"; -import { getDropdownOptionsForProperties, getObsidianPropertiesByType } from "./utils"; -import { LOG_LEVEL_DEBUG, LOG_LEVEL_ERROR, LOG_LEVEL_INFO, LOG_LEVEL_OFF, LOG_LEVEL_TRACE, LOG_LEVEL_WARN } from "src/logger/constants"; +import { + getDropdownOptionsForProperties, + getObsidianPropertiesByType, +} from "./utils"; +import { + LOG_LEVEL_DEBUG, + LOG_LEVEL_ERROR, + LOG_LEVEL_INFO, + LOG_LEVEL_OFF, + LOG_LEVEL_TRACE, + LOG_LEVEL_WARN, +} from "src/logger/constants"; import Logger from "js-logger"; import { stringToLogLevel } from "src/logger"; import { TExplorerView, WordBreak } from "src/types"; @@ -25,160 +35,214 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab { const textProperties = getObsidianPropertiesByType(this.app, "text"); const dateProperties = getObsidianPropertiesByType(this.app, "date"); - const dateTimeProperties = getObsidianPropertiesByType(this.app, "datetime"); - const checkboxProperties = getObsidianPropertiesByType(this.app, "checkbox"); + const dateTimeProperties = getObsidianPropertiesByType( + this.app, + "datetime" + ); + const checkboxProperties = getObsidianPropertiesByType( + this.app, + "checkbox" + ); new Setting(containerEl).setName("General").setHeading(); - new Setting(containerEl).setName("Page size").setDesc("The number of items to display per page.").addDropdown(dropdown => dropdown - .addOptions({ - "10": "10", - "25": "25", - "50": "50", - "100": "100", - "250": "250", - "500": "500", - }) - .setValue(this.plugin.settings.pageSize.toString()) - .onChange(async (value) => { - this.plugin.settings.pageSize = parseInt(value); - await this.plugin.saveSettings(); - EventManager.getInstance().emit("page-size-setting-change"); - })); new Setting(containerEl) .setName("Title wrapping") - .setDesc( - "Sets the wrapping style for the title." - ) + .setDesc("Set the wrapping style for the title.") .addDropdown((cb) => { cb.addOptions({ - "normal": "Normal", + normal: "Normal", "break-word": "Break Word", - }) + }); cb.setValue(this.plugin.settings.titleWrapping).onChange( async (value) => { this.plugin.settings.titleWrapping = value as WordBreak; await this.plugin.saveSettings(); - EventManager.getInstance().emit("title-wrapping-setting-change"); + EventManager.getInstance().emit( + "title-wrapping-setting-change" + ); } ); }); new Setting(containerEl) - .setName("Enable scroll buttons") - .setDesc("When enabled, scroll buttons will be displayed for scrollable content.") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableScrollButtons) - .onChange(async (value) => { - this.plugin.settings.enableScrollButtons = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("scroll-buttons-setting-change"); - })); + .setName("File icons") + .setDesc("Display an icon next to the file name.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableFileIcons) + .onChange(async (value) => { + this.plugin.settings.enableFileIcons = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "file-icons-setting-change" + ); + }) + ); + new Setting(containerEl) + .setName("Scroll buttons") + .setDesc("Display scroll buttons for scrollable content.") + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableScrollButtons) + .onChange(async (value) => { + this.plugin.settings.enableScrollButtons = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "scroll-buttons-setting-change" + ); + }) + ); + + new Setting(containerEl) + .setName("Page size") + .setDesc("Number of items to display per page.") + .addDropdown((dropdown) => + dropdown + .addOptions({ + "10": "10", + "25": "25", + "50": "50", + "100": "100", + "250": "250", + "500": "500", + }) + .setValue(this.plugin.settings.pageSize.toString()) + .onChange(async (value) => { + this.plugin.settings.pageSize = parseInt(value); + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "page-size-setting-change" + ); + }) + ); new Setting(containerEl).setName("Filters").setHeading(); - new Setting(containerEl) - .setName("Search filter") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Search filter").addToggle((toggle) => + toggle .setValue(this.plugin.settings.filters.search.isEnabled) .onChange(async (value) => { this.plugin.settings.filters.search.isEnabled = value; await this.plugin.saveSettings(); - EventManager.getInstance().emit("filter-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "filter-toggle-setting-change" + ); + }) + ); new Setting(containerEl) .setName("Favorites filter") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.filters.favorites.isEnabled) - .onChange(async (value) => { - this.plugin.settings.filters.favorites.isEnabled = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("filter-toggle-setting-change"); - })); + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.filters.favorites.isEnabled) + .onChange(async (value) => { + this.plugin.settings.filters.favorites.isEnabled = + value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "filter-toggle-setting-change" + ); + }) + ); new Setting(containerEl) .setName("Timestamp filter") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.filters.timestamp.isEnabled) - .onChange(async (value) => { - this.plugin.settings.filters.timestamp.isEnabled = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("filter-toggle-setting-change"); - })); + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.filters.timestamp.isEnabled) + .onChange(async (value) => { + this.plugin.settings.filters.timestamp.isEnabled = + value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "filter-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("Sort filter") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Sort filter").addToggle((toggle) => + toggle .setValue(this.plugin.settings.filters.sort.isEnabled) .onChange(async (value) => { this.plugin.settings.filters.sort.isEnabled = value; await this.plugin.saveSettings(); - EventManager.getInstance().emit("filter-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "filter-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("Custom filter") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Custom filter").addToggle((toggle) => + toggle .setValue(this.plugin.settings.filters.custom.isEnabled) .onChange(async (value) => { this.plugin.settings.filters.custom.isEnabled = value; await this.plugin.saveSettings(); - EventManager.getInstance().emit("filter-toggle-setting-change"); - })); - + EventManager.getInstance().emit( + "filter-toggle-setting-change" + ); + }) + ); new Setting(containerEl).setName("Views").setHeading(); - new Setting(containerEl) - .setName("Dashboard view") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Dashboard view").addToggle((toggle) => + toggle .setDisabled(true) //TODO - Implement dashboard view .setValue(this.plugin.settings.views.dashboard.isEnabled) .onChange(async (value) => { this.plugin.settings.views.dashboard.isEnabled = value; this.updateViewOrder(TExplorerView.DASHBOARD, value); await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("Grid view") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Grid view").addToggle((toggle) => + toggle .setValue(this.plugin.settings.views.grid.isEnabled) .onChange(async (value) => { this.plugin.settings.views.grid.isEnabled = value; this.updateViewOrder(TExplorerView.GRID, value); await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("List view") - .addToggle(toggle => toggle + new Setting(containerEl).setName("List view").addToggle((toggle) => + toggle .setValue(this.plugin.settings.views.list.isEnabled) .onChange(async (value) => { this.plugin.settings.views.list.isEnabled = value; this.updateViewOrder(TExplorerView.LIST, value); await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("Feed view") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Feed view").addToggle((toggle) => + toggle .setValue(this.plugin.settings.views.feed.isEnabled) .onChange(async (value) => { this.plugin.settings.views.feed.isEnabled = value; this.updateViewOrder(TExplorerView.FEED, value); await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("Table view") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Table view").addToggle((toggle) => + toggle .setDisabled(true) //TODO implement .setTooltip("This view is not yet implemented.") .setValue(this.plugin.settings.views.table.isEnabled) @@ -186,26 +250,32 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab { this.plugin.settings.views.table.isEnabled = value; this.updateViewOrder(TExplorerView.TABLE, value); await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); - + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); new Setting(containerEl) .setName("Recommended view") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.views.recommended.isEnabled) - .setDisabled(true) //TODO implement - .setTooltip("This view is not yet implemented.") - .onChange(async (value) => { - this.plugin.settings.views.recommended.isEnabled = value; - this.updateViewOrder(TExplorerView.RECOMMENDED, value); - await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.views.recommended.isEnabled) + .setDisabled(true) //TODO implement + .setTooltip("This view is not yet implemented.") + .onChange(async (value) => { + this.plugin.settings.views.recommended.isEnabled = + value; + this.updateViewOrder(TExplorerView.RECOMMENDED, value); + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); - new Setting(containerEl) - .setName("Related view") - .addToggle(toggle => toggle + new Setting(containerEl).setName("Related view").addToggle((toggle) => + toggle .setDisabled(true) //TODO implement .setTooltip("This view is not yet implemented.") .setValue(this.plugin.settings.views.related.isEnabled) @@ -213,119 +283,178 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab { this.plugin.settings.views.related.isEnabled = value; this.updateViewOrder(TExplorerView.RELATED, value); await this.plugin.saveSettings(); - EventManager.getInstance().emit("view-toggle-setting-change"); - })); + EventManager.getInstance().emit( + "view-toggle-setting-change" + ); + }) + ); new Setting(containerEl).setName("Built-in properties").setHeading(); new Setting(containerEl) - .setName('Favorite property') - .setDesc('The property used to mark a note as a favorite. This must be a checkbox property.') - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties(checkboxProperties)) - .setValue(this.plugin.settings.properties.favorite) - .onChange(async (value) => { - this.plugin.settings.properties.favorite = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .setName("Favorite property") + .setDesc( + "Property used to mark a note as a favorite. This must be a checkbox property." + ) + .addDropdown((dropdown) => + dropdown + .addOptions( + getDropdownOptionsForProperties(checkboxProperties) + ) + .setValue(this.plugin.settings.properties.favorite) + .onChange(async (value) => { + this.plugin.settings.properties.favorite = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); new Setting(containerEl) - .setName('URL property') - .setDesc('The property used to store the URL of the content. This must be a text property.') - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties(textProperties)) - .setValue(this.plugin.settings.properties.url) - .onChange(async (value) => { - this.plugin.settings.properties.url = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .setName("URL property") + .setDesc( + "Property used to store the URL of the content. This must be a text property." + ) + .addDropdown((dropdown) => + dropdown + .addOptions(getDropdownOptionsForProperties(textProperties)) + .setValue(this.plugin.settings.properties.url) + .onChange(async (value) => { + this.plugin.settings.properties.url = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); const creationDateDesc = new DocumentFragment(); creationDateDesc.createDiv({ - text: "The property containing the creation date. This must be a date or datetime property.", + text: "Property containing the creation date. This must be a date or datetime property.", }); creationDateDesc.createDiv({ text: "If set to 'Select a property', the file's created at date will be used.", }); - new Setting(containerEl) .setName("Creation date property") .setDesc(creationDateDesc) - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties([...dateProperties, ...dateTimeProperties])) - .setValue(this.plugin.settings.properties.createdDate) - .onChange(async (value) => { - this.plugin.settings.properties.createdDate = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .addDropdown((dropdown) => + dropdown + .addOptions( + getDropdownOptionsForProperties([ + ...dateProperties, + ...dateTimeProperties, + ]) + ) + .setValue(this.plugin.settings.properties.createdDate) + .onChange(async (value) => { + this.plugin.settings.properties.createdDate = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); const modificationDateDesc = new DocumentFragment(); modificationDateDesc.createDiv({ - text: "The property containing the modification date. This must be a date or datetime property.", + text: "Property containing the modification date. This must be a date or datetime property.", }); modificationDateDesc.createDiv({ text: "If set to 'Select a property', the file's modified at date will be used.", }); new Setting(containerEl) - .setName('Modification date property') + .setName("Modification date property") .setDesc(modificationDateDesc) - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties([...dateProperties, ...dateTimeProperties])) - .setValue(this.plugin.settings.properties.modifiedDate) - .onChange(async (value) => { - this.plugin.settings.properties.modifiedDate = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .addDropdown((dropdown) => + dropdown + .addOptions( + getDropdownOptionsForProperties([ + ...dateProperties, + ...dateTimeProperties, + ]) + ) + .setValue(this.plugin.settings.properties.modifiedDate) + .onChange(async (value) => { + this.plugin.settings.properties.modifiedDate = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); new Setting(containerEl).setName("Custom properties").setHeading(); new Setting(containerEl) - .setName('Custom property 1') - .setDesc('The first custom property. This must be a text property.') - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties(textProperties)) - .setValue(this.plugin.settings.properties.custom1) - .onChange(async (value) => { - this.plugin.settings.properties.custom1 = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .setName("Custom property 1") + .setDesc("First custom property. This must be a text property.") + .addDropdown((dropdown) => + dropdown + .addOptions(getDropdownOptionsForProperties(textProperties)) + .setValue(this.plugin.settings.properties.custom1) + .onChange(async (value) => { + this.plugin.settings.properties.custom1 = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); new Setting(containerEl) - .setName('Custom property 2') - .setDesc('The second custom property. This must be a text property.') - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties(textProperties)) - .setValue(this.plugin.settings.properties.custom2) - .onChange(async (value) => { - this.plugin.settings.properties.custom2 = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .setName("Custom property 2") + .setDesc("Second custom property. This must be a text property.") + .addDropdown((dropdown) => + dropdown + .addOptions(getDropdownOptionsForProperties(textProperties)) + .setValue(this.plugin.settings.properties.custom2) + .onChange(async (value) => { + this.plugin.settings.properties.custom2 = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); new Setting(containerEl) - .setName('Custom property 3') - .setDesc('The third custom property. This must be a text property.') - .addDropdown(dropdown => dropdown.addOptions(getDropdownOptionsForProperties(textProperties)) - .setValue(this.plugin.settings.properties.custom3) - .onChange(async (value) => { - this.plugin.settings.properties.custom3 = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("property-setting-change"); - })); + .setName("Custom property 3") + .setDesc("Third custom property. This must be a text property.") + .addDropdown((dropdown) => + dropdown + .addOptions(getDropdownOptionsForProperties(textProperties)) + .setValue(this.plugin.settings.properties.custom3) + .onChange(async (value) => { + this.plugin.settings.properties.custom3 = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "property-setting-change" + ); + }) + ); new Setting(containerEl).setName("Updates").setHeading(); new Setting(containerEl) .setName("Enable clock updates") - .setDesc("When enabled, time values will update every minute, refreshing the Vault Explorer view. When disabled, time values will only update when the view is first opened.") - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableClockUpdates) - .onChange(async (value) => { - this.plugin.settings.enableClockUpdates = value; - await this.plugin.saveSettings(); - EventManager.getInstance().emit("clock-updates-setting-change"); - })); + .setDesc( + "When enabled, time values will update every minute, refreshing the Vault Explorer view. When disabled, time values will only update when the view is first opened." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableClockUpdates) + .onChange(async (value) => { + this.plugin.settings.enableClockUpdates = value; + await this.plugin.saveSettings(); + EventManager.getInstance().emit( + "clock-updates-setting-change" + ); + }) + ); new Setting(containerEl).setName("Premium").setHeading(); @@ -337,7 +466,7 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab { new Setting(containerEl) .setName("Log level") .setDesc( - "Sets the log level. Please use trace to see all log messages." + "Set the log level. Please use trace to see all log messages." ) .addDropdown((cb) => { cb.addOptions({ @@ -346,8 +475,8 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab { [LOG_LEVEL_WARN]: "Warn", [LOG_LEVEL_INFO]: "Info", [LOG_LEVEL_DEBUG]: "Debug", - [LOG_LEVEL_TRACE]: "Trace" - }) + [LOG_LEVEL_TRACE]: "Trace", + }); cb.setValue(this.plugin.settings.logLevel).onChange( async (value) => { this.plugin.settings.logLevel = value; @@ -370,7 +499,9 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab { if (this.plugin.settings.currentView == null) this.plugin.settings.currentView = view; } else { - const filtered = this.plugin.settings.viewOrder.filter(v => v !== view); + const filtered = this.plugin.settings.viewOrder.filter( + (v) => v !== view + ); this.plugin.settings.viewOrder = filtered; //If the user turned off the current view, set the current view to the first view diff --git a/src/svelte/app/components/feed-card.svelte b/src/svelte/app/components/feed-card.svelte index bf61b04..e0d6814 100644 --- a/src/svelte/app/components/feed-card.svelte +++ b/src/svelte/app/components/feed-card.svelte @@ -10,19 +10,42 @@ import Stack from "src/svelte/shared/components/stack.svelte"; import Tag from "src/svelte/shared/components/tag.svelte"; import { removeFrontmatterBlock } from "../services/utils/frontmatter-utils"; + import Icon from "src/svelte/shared/components/icon.svelte"; + import { getIconIdForFile } from "../services/utils/file-icon-utils"; - export let name: string; + export let displayName: string; + export let baseName: string; + export let extension: string; export let path: string; export let tags: string[] | null; export let createdMillis: number; export let content: string | null; let wordBreak: WordBreak = "normal"; + let enableFileIcons = false; let plugin: VaultExplorerPlugin; store.plugin.subscribe((value) => { plugin = value; wordBreak = plugin.settings.titleWrapping; + enableFileIcons = plugin.settings.enableFileIcons; + }); + + onMount(() => { + function handleFileIconsChange() { + enableFileIcons = plugin.settings.enableFileIcons; + } + + EventManager.getInstance().on( + "file-icons-setting-change", + handleFileIconsChange, + ); + return () => { + EventManager.getInstance().off( + "file-icons-setting-change", + handleFileIconsChange, + ); + }; }); onMount(() => { @@ -93,7 +116,12 @@ }); }} > - {name} + + {#if enableFileIcons} + + {/if} + {displayName} + {#if displayContent != null && displayContent.length > 0}
diff --git a/src/svelte/app/components/feed-view.svelte b/src/svelte/app/components/feed-view.svelte index 3d09440..b7834c0 100644 --- a/src/svelte/app/components/feed-view.svelte +++ b/src/svelte/app/components/feed-view.svelte @@ -4,12 +4,17 @@ import { FileRenderData } from "../types"; import License from "src/svelte/shared/services/license"; import FeedCard from "./feed-card.svelte"; + import { onMount } from "svelte"; + import EventManager from "src/event/event-manager"; + import VaultExplorerPlugin from "src/main"; + import store from "src/svelte/shared/services/store"; export let enablePremiumFeatures = false; export let data: FileRenderData[] = []; export let startIndex; export let pageLength; + let enableFileIcons = false; let displayedItems: FileRenderData[] = []; License.getInstance() @@ -42,7 +47,9 @@ {#if enablePremiumFeatures} {#each displayedItems as fileData (fileData.path)} { + function handleFileIconsChange() { + enableFileIcons = plugin.settings.enableFileIcons; + } + + EventManager.getInstance().on( + "file-icons-setting-change", + handleFileIconsChange, + ); + return () => { + EventManager.getInstance().off( + "file-icons-setting-change", + handleFileIconsChange, + ); + }; }); onMount(() => { @@ -185,7 +208,12 @@ }); }} > - {name} + + {#if enableFileIcons} + + {/if} + {displayName} +
{#if url !== null} diff --git a/src/svelte/app/components/grid-view.svelte b/src/svelte/app/components/grid-view.svelte index 165c552..0dd6f48 100644 --- a/src/svelte/app/components/grid-view.svelte +++ b/src/svelte/app/components/grid-view.svelte @@ -25,8 +25,10 @@
{#each displayedItems as file (file.path)} { plugin = p; + enableFileIcons = plugin.settings.enableFileIcons; + }); + + onMount(() => { + function handleFileIconsChange() { + enableFileIcons = plugin.settings.enableFileIcons; + } + + EventManager.getInstance().on( + "file-icons-setting-change", + handleFileIconsChange, + ); + return () => { + EventManager.getInstance().off( + "file-icons-setting-change", + handleFileIconsChange, + ); + }; }); function handleTitleClick() { @@ -49,7 +75,12 @@ }); }} > - {name} + + {#if enableFileIcons} + + {/if} + {displayName} +
{#if tags != null}
diff --git a/src/svelte/app/components/list-view.svelte b/src/svelte/app/components/list-view.svelte index 85d66b8..810a24b 100644 --- a/src/svelte/app/components/list-view.svelte +++ b/src/svelte/app/components/list-view.svelte @@ -24,6 +24,12 @@
{#each displayedItems as file (file.path)} - + {/each}
diff --git a/src/svelte/app/index.svelte b/src/svelte/app/index.svelte index 90a6bf0..e513642 100644 --- a/src/svelte/app/index.svelte +++ b/src/svelte/app/index.svelte @@ -710,9 +710,13 @@ $: renderData = [...filteredTimestamp].sort((a, b) => { const { value } = sortFilter; if (value === "file-name-asc") { - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + return a.displayName + .toLowerCase() + .localeCompare(b.displayName.toLowerCase()); } else if (value === "file-name-desc") { - return b.name.toLowerCase().localeCompare(a.name.toLowerCase()); + return b.displayName + .toLowerCase() + .localeCompare(a.displayName.toLowerCase()); } else if (value === "modified-asc") { return a.modifiedMillis - b.modifiedMillis; } else if (value === "modified-desc") { diff --git a/src/svelte/app/services/file-content-store.ts b/src/svelte/app/services/file-content-store.ts index 7a8d385..414640e 100644 --- a/src/svelte/app/services/file-content-store.ts +++ b/src/svelte/app/services/file-content-store.ts @@ -18,8 +18,11 @@ function createFileContentStore() { for (let file of files) { promises.push( (async () => { - const { extension } = file; - if (extension === "md") { + const { basename, extension } = file; + if ( + extension === "md" && + !basename.endsWith(".excalidraw") + ) { const content = await app.vault.cachedRead(file); return { path: file.path, diff --git a/src/svelte/app/services/filters/search-filter.ts b/src/svelte/app/services/filters/search-filter.ts index 4520fa8..beb4900 100644 --- a/src/svelte/app/services/filters/search-filter.ts +++ b/src/svelte/app/services/filters/search-filter.ts @@ -8,9 +8,9 @@ export const filterBySearch = (file: FileRenderData, value: string) => { const compare = value.toLowerCase().trim(); - const { name, path, content, } = file; + const { displayName, path, content } = file; - if (name.toLowerCase().includes(compare)) { + if (displayName.toLowerCase().includes(compare)) { return true; } @@ -23,4 +23,4 @@ export const filterBySearch = (file: FileRenderData, value: string) => { } return false; -} +}; diff --git a/src/svelte/app/services/utils/file-icon-utils.ts b/src/svelte/app/services/utils/file-icon-utils.ts new file mode 100644 index 0000000..3171215 --- /dev/null +++ b/src/svelte/app/services/utils/file-icon-utils.ts @@ -0,0 +1,63 @@ +import Logger from "js-logger"; + +export const getIconIdForFile = (baseName: string, extension: string) => { + if (baseName.endsWith(".excalidraw")) { + return "excalidraw-icon"; + } + + switch (extension) { + case "md": + return "file"; + case "canvas": + return "layout-dashboard"; + case "zip": + return "file-archive"; + case "png": + case "jpg": + case "jpeg": + case "gif": + case "webp": + case "svg": + case "avif": + case "bmp": + return "file-image"; + case "mp3": + case "wav": + case "aac": + case "flac": + case "ogg": + case "wma": + case "alac": + case "aiff": + return "file-audio"; + case "mp4": + case "avi": + case "mkv": + case "mov": + case "wmv": + case "flv": + case "webm": + case "mpeg": + case "m4v": + case "3gp": + return "file-video"; + case "xls": + case "xlsx": + return "file-spreadsheet"; + case "xml": + case "json": + return "file-code"; + case "ppt": + case "pptx": + return "images"; + case "doc": + case "docx": + return "file-type"; + case "pdf": + case "txt": + return "file-text"; + default: + Logger.warn(`No icon found for file extension: ${extension}`); + return "file"; + } +}; diff --git a/src/svelte/app/services/utils/render-utils.ts b/src/svelte/app/services/utils/render-utils.ts index bfd21b8..59a94f5 100644 --- a/src/svelte/app/services/utils/render-utils.ts +++ b/src/svelte/app/services/utils/render-utils.ts @@ -95,8 +95,10 @@ export const formatFileDataForRender = ( const displayName = extension === "md" ? basename : name; return { - name: displayName, + displayName, + baseName: basename, path, + extension, url, content, tags, diff --git a/src/svelte/app/types.ts b/src/svelte/app/types.ts index 29ada80..1586b37 100644 --- a/src/svelte/app/types.ts +++ b/src/svelte/app/types.ts @@ -1,6 +1,8 @@ export interface FileRenderData { - name: string; + displayName: string; path: string; + extension: string; + baseName: string; content: string | null; url: string | null; tags: string[] | null; diff --git a/src/svelte/shared/components/icon-button.svelte b/src/svelte/shared/components/icon-button.svelte index 3455907..a79db16 100644 --- a/src/svelte/shared/components/icon-button.svelte +++ b/src/svelte/shared/components/icon-button.svelte @@ -1,6 +1,7 @@