Finish cover image system (#284)

* chore: bump version

* refactor: remove enableScrollButtons

* feat: handle render cases

* feat: remove links not used in properties

* test: update test vault files for cover image testing

* refactor: remove unused imports

* fix: don't show broken link when social media image isn't loaded

* feat: add image source setting app

* refactor: remove unused properties

* feat: make cover image source options draggable

* fix: prevent url's that don't have a social image from loading everytime the grid mounts

* fix: remove extra padding

* refactor: rename to social media image cache
This commit is contained in:
DecafDev 2024-08-01 12:53:08 -06:00 committed by GitHub
parent e0f1d64e81
commit c3051dcf51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 829 additions and 259 deletions

View file

@ -1,7 +1,7 @@
{
"id": "vault-explorer",
"name": "Vault Explorer",
"version": "1.36.3",
"version": "1.37.0",
"minAppVersion": "1.4.13",
"description": "Explore your vault in visual format",
"author": "DecafDev",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-vault-explorer",
"version": "1.36.3",
"version": "1.37.0",
"description": "Explore your vault in visual format",
"main": "main.js",
"scripts": {

View file

@ -9,7 +9,7 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = {
properties: {
favorite: "",
url: "",
imageUrl: "",
image: "",
createdDate: "",
modifiedDate: "",
custom1: "",
@ -45,8 +45,25 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = {
},
grid: {
isEnabled: true,
coverImageSources: [
{
type: "image-property",
isEnabled: true,
},
{
type: "url-property",
isEnabled: true,
},
{
type: "frontmatter",
isEnabled: true,
},
{
type: "body",
isEnabled: true,
},
],
loadSocialMediaImage: true,
coverImageSource: "frontmatter-and-body",
},
list: {
isEnabled: true,
@ -74,7 +91,6 @@ export const DEFAULT_SETTINGS: VaultExplorerPluginSettings = {
titleWrapping: "normal",
enableClockUpdates: true,
enableFileIcons: false,
enableScrollButtons: true,
filterGroupsWidth: "300px",
shouldWrapFilterGroups: false,
pageSize: 25,

View file

@ -26,6 +26,7 @@ import Migrate_1_29_0 from "./migrate_1_29_0";
import Migrate_1_30_0 from "./migrate_1_30_5";
import Migrate_1_31_0 from "./migrate_1_31_0";
import Migrate_1_33_0 from "./migrate_1_33_0";
import Migrate_1_37_0 from "./migrate_1_37_0";
const migrations: TMigration[] = [
{
@ -153,6 +154,11 @@ const migrations: TMigration[] = [
to: "1.33.0",
migrate: Migrate_1_33_0,
},
{
from: "1.36.3",
to: "1.37.0",
migrate: Migrate_1_37_0,
},
];
export const preformMigrations = (

View file

@ -1,11 +1,11 @@
import { VaultExplorerPluginSettings } from "src/types";
import MigrationInterface from "./migration_interface";
import { VaultExplorerPluginSettings_1_30_5 } from "src/types/types-1.30.5";
import { VaultExplorerPluginSettings_1_32_2 } from "src/types/types-1.32.2";
export default class Migrate_1_31_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_30_5;
const newData: VaultExplorerPluginSettings = {
const newData: VaultExplorerPluginSettings_1_32_2 = {
...typedData,
shouldWrapFilterGroups: typedData.filterGroupsWrapping === "wrap",
};

View file

@ -1,11 +1,11 @@
import { VaultExplorerPluginSettings } from "src/types";
import MigrationInterface from "./migration_interface";
import { VaultExplorerPluginSettings_1_32_2 } from "src/types/types-1.32.2";
import { VaultExplorerPluginSettings_1_36_3 } from "src/types/types-1.36.3";
export default class Migrate_1_33_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_32_2;
const newData: VaultExplorerPluginSettings = {
const newData: VaultExplorerPluginSettings_1_36_3 = {
...typedData,
};

View file

@ -0,0 +1,45 @@
import { VaultExplorerPluginSettings } from "src/types";
import MigrationInterface from "./migration_interface";
import { VaultExplorerPluginSettings_1_36_3 } from "src/types/types-1.36.3";
export default class Migrate_1_37_0 implements MigrationInterface {
migrate(data: Record<string, unknown>) {
const typedData = data as unknown as VaultExplorerPluginSettings_1_36_3;
const newData: VaultExplorerPluginSettings = {
...typedData,
views: {
...typedData.views,
grid: {
...typedData.views.grid,
coverImageSources: [
{
type: "image-property",
isEnabled: true,
},
{
type: "url-property",
isEnabled: true,
},
{
type: "frontmatter",
isEnabled: true,
},
{
type: "body",
isEnabled: true,
},
],
},
},
properties: {
...typedData.properties,
image: typedData.properties.imageUrl,
},
};
delete (newData as any).enableScrollButtons;
delete (newData as any).views.grid.coverImageSource;
delete (newData as any).properties.imageUrl;
return newData as unknown as Record<string, unknown>;
}
}

View file

@ -14,9 +14,10 @@ import {
} from "src/logger/constants";
import Logger from "js-logger";
import { stringToLogLevel } from "src/logger";
import { CollapseStyle, CoverImageSource, TExplorerView } from "src/types";
import { CollapseStyle, TExplorerView } from "src/types";
import EventManager from "src/event/event-manager";
import LicenseKeyApp from "../svelte/license-key-app/index.svelte";
import ImageSourceApp from "../svelte/image-source-app/index.svelte";
import { PluginEvent } from "src/event/types";
import "./styles.css";
@ -24,12 +25,15 @@ import { clearSocialMediaImageCache } from "src/svelte/app/services/social-media
export default class VaultExplorerSettingsTab extends PluginSettingTab {
plugin: VaultExplorerPlugin;
component: LicenseKeyApp | null;
licenseKeyApp: LicenseKeyApp | null;
imageSourceApp: ImageSourceApp | null;
constructor(app: App, plugin: VaultExplorerPlugin) {
super(app, plugin);
this.plugin = plugin;
this.component = null;
this.licenseKeyApp = null;
this.imageSourceApp = null;
}
display(): void {
@ -97,22 +101,6 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
})
);
// TODO remove?
// 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.")
@ -309,44 +297,9 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
new Setting(containerEl).setName("Grid view").setHeading();
new Setting(containerEl)
.setName("Automatic cover image detection")
.setDesc("Set where cover images are automatically loaded from.")
.addDropdown((cb) =>
cb
.addOptions({
"frontmatter-and-body": "Frontmatter and body",
"frontmatter-only": "Frontmatter only",
off: "Off",
})
.setValue(this.plugin.settings.views.grid.coverImageSource)
.onChange(async (value) => {
this.plugin.settings.views.grid.coverImageSource =
value as CoverImageSource;
await this.plugin.saveSettings();
EventManager.getInstance().emit(
PluginEvent.COVER_IMAGE_SOURCE_SETTING_CHANGE
);
})
);
new Setting(containerEl)
.setName("Preferred cover image property")
.setDesc(
"If set, the value of the selected property will be preferred for the cover image"
)
.addDropdown((dropdown) =>
dropdown
.addOptions(getDropdownOptionsForProperties(textProperties))
.setValue(this.plugin.settings.properties.imageUrl)
.onChange(async (value) => {
this.plugin.settings.properties.imageUrl = value;
await this.plugin.saveSettings();
EventManager.getInstance().emit(
PluginEvent.PROPERTY_SETTING_CHANGE
);
})
);
this.imageSourceApp = new ImageSourceApp({
target: containerEl,
});
new Setting(containerEl)
.setName("Load social media image")
@ -498,6 +451,24 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
})
);
new Setting(containerEl)
.setName("Image property")
.setDesc(
"Property used to store an image. This must be a text property."
)
.addDropdown((dropdown) =>
dropdown
.addOptions(getDropdownOptionsForProperties(textProperties))
.setValue(this.plugin.settings.properties.image)
.onChange(async (value) => {
this.plugin.settings.properties.image = value;
await this.plugin.saveSettings();
EventManager.getInstance().emit(
PluginEvent.PROPERTY_SETTING_CHANGE
);
})
);
new Setting(containerEl)
.setName("URL property")
.setDesc(
@ -645,7 +616,7 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
new Setting(containerEl).setName("Premium").setHeading();
this.component = new LicenseKeyApp({
this.licenseKeyApp = new LicenseKeyApp({
target: containerEl,
});
@ -697,7 +668,7 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Clear cache data")
.setName("Social media image cache")
.addButton((button) =>
button
.setClass("mod-destructive")
@ -709,7 +680,8 @@ export default class VaultExplorerSettingsTab extends PluginSettingTab {
}
onClose() {
this.component?.$destroy();
this.licenseKeyApp?.$destroy();
this.imageSourceApp?.$destroy();
}
private updateViewOrder(view: TExplorerView, value: boolean) {

View file

@ -5,7 +5,7 @@
import store from "../../shared/services/store";
import Wrap from "src/svelte/shared/components/wrap.svelte";
import Stack from "src/svelte/shared/components/stack.svelte";
import { createEventDispatcher, onMount } from "svelte";
import { afterUpdate, createEventDispatcher, onMount } from "svelte";
import { WordBreak } from "src/types";
import { HOVER_LINK_SOURCE_ID } from "src/constants";
import EventManager from "src/event/event-manager";
@ -57,24 +57,17 @@
let renderKey = 0;
onMount(() => {
async function updateImgSrc() {
if (imageUrl !== null) {
const entry = await getSocialMediaImageEntry(imageUrl);
if (entry) {
const isExpired =
await isSocialMediaImageEntryExpired(entry);
if (!isExpired) {
imgSrc = entry.socialMediaImageUrl;
return;
}
}
imgSrc = imageUrl;
}
}
updateImgSrc();
});
afterUpdate(() => {
if (imageUrl === null) {
imgSrc = null;
} else {
updateImgSrc();
}
});
onMount(() => {
function handleLoadSocialMediaImageChange() {
const newValue = plugin.settings.views.grid.loadSocialMediaImage;
@ -128,6 +121,30 @@
};
});
async function updateImgSrc() {
if (imageUrl !== null) {
const entry = await getSocialMediaImageEntry(imageUrl);
if (entry) {
const isExpired = await isSocialMediaImageEntryExpired(entry);
if (!isExpired) {
const socialUrl = entry.socialMediaImageUrl;
//The url is null when the social media image does not exist
//This will always happen for sites like Twitter (x.com)
//To avoid fetching the same non-existent image, we will set imgSrc to null
if (socialUrl === null) {
imgSrc = null;
} else {
imgSrc = socialUrl;
}
return;
}
}
imgSrc = imageUrl;
}
console.log("Image", imageUrl, imgSrc);
}
function handleUrlClick(e: Event) {
e.stopPropagation();
}
@ -181,15 +198,12 @@
if (socialUrl) {
putSocialMediaImageUrl(imgSrc, socialUrl);
target.src = socialUrl;
return;
} else {
isError = true;
putSocialMediaImageUrl(imgSrc, null);
}
}
}
let isError = false;
function handleImageLoad() {
isCoverImageLoaded = true;
}
@ -219,9 +233,7 @@
<img
class="vault-explorer-grid-card__image"
src={imgSrc}
style="display: {isCoverImageLoaded || isError
? 'block'
: 'none'};"
style="display: {isCoverImageLoaded ? 'block' : 'none'};"
on:load={handleImageLoad}
on:error={handleImageError}
/>
@ -336,11 +348,6 @@
border-radius: var(--radius-m);
}
/*
.vault-explorer-grid-card:hover {
background-color: var(--background-modifier-hover);
} */
.vault-explorer-grid-card:focus-visible {
box-shadow: 0 0 0 3px var(--background-modifier-border-focus);
}

View file

@ -10,18 +10,11 @@
import { Notice, TFile } from "obsidian";
import {
TCustomFilter,
TDashboardView,
TFavoritesFilter,
TSearchFilter,
TSortFilter,
TTimestampFilter,
TExplorerView,
TListView,
TGridView,
TRecommendedView,
TRelatedView,
TTableView,
TFeedView,
} from "src/types";
import store from "../shared/services/store";
import VaultExplorerPlugin from "src/main";
@ -104,6 +97,7 @@
let frontmatterCacheTime: number = Date.now();
let propertySettingsTime: number = Date.now();
let coverImageSourcesTime: number = Date.now();
let loadedFiles: LoadedFile[] = [];
let timeValuesUpdateInterval: NodeJS.Timer | null = null;
@ -112,43 +106,6 @@
let contentCache: FileContentCache = new Map();
let randomSortCache: RandomFileSortCache = new Map();
let dashboardView: TDashboardView = {
isEnabled: false,
};
let listView: TListView = {
isEnabled: false,
showTags: true,
};
let gridView: TGridView = {
loadSocialMediaImage: false,
isEnabled: false,
coverImageSource: "frontmatter-and-body",
};
//TODO use just isEnabled
let feedView: TFeedView = {
isEnabled: false,
removeH1: true,
lineClampSmall: 2,
lineClampMedium: 3,
lineClampLarge: 5,
collapseStyle: "no-new-lines",
};
let tableView: TTableView = {
isEnabled: false,
};
let recommendedView: TRecommendedView = {
isEnabled: false,
};
let relatedView: TRelatedView = {
isEnabled: false,
};
let viewOrder: TExplorerView[] = [];
// ============================================
@ -181,13 +138,6 @@
timestampFilter = settings.filters.timestamp;
currentView = settings.currentView;
customFilter = settings.filters.custom;
dashboardView = settings.views.dashboard;
listView = settings.views.list;
gridView = settings.views.grid;
feedView = settings.views.feed;
tableView = settings.views.table;
recommendedView = settings.views.recommended;
relatedView = settings.views.related;
viewOrder = settings.viewOrder;
if (settings.enableClockUpdates) {
@ -501,8 +451,7 @@
message: "called",
});
//TODO update?
updateFrontmatterCacheTime();
coverImageSourcesTime = Date.now();
}
EventManager.getInstance().on(
@ -567,13 +516,6 @@
plugin.settings.filters.sort = sortFilter;
plugin.settings.filters.timestamp = timestampFilter;
plugin.settings.filters.favorites = favoritesFilter;
plugin.settings.views.dashboard = dashboardView;
plugin.settings.views.list = listView;
plugin.settings.views.grid = gridView;
plugin.settings.views.feed = feedView;
plugin.settings.views.table = tableView;
plugin.settings.views.recommended = recommendedView;
plugin.settings.views.related = relatedView;
plugin.settings.currentView = currentView;
plugin.settings.filters.custom = customFilter;
plugin.settings.viewOrder = viewOrder;
@ -799,7 +741,7 @@
}
let formatted: FileRenderData[] = [];
$: if (propertySettingsTime) {
$: if (propertySettingsTime || coverImageSourcesTime) {
formatted = filteredCustom.map((loadedFile) => {
const { id, file } = loadedFile;
const frontmatter =
@ -882,13 +824,6 @@
favoritesFilter,
currentView,
customFilter,
dashboardView,
listView,
gridView,
feedView,
tableView,
recommendedView,
relatedView,
viewOrder,
saveSettings();

View file

@ -14,17 +14,16 @@ import {
import { isImageExtension } from "./utils/image-utils";
import { removeFrontmatter } from "./utils/content-utils";
import { getURIForWikiLinkTarget } from "./utils/wiki-link-utils";
import {
isExternalEmbed,
isInternalEmbed,
isUrl,
isWikiLink,
} from "./link-utils/link-validators";
import { isUrl, isWikiLink } from "./link-utils/link-validators";
import {
getExternalEmbedTarget,
getInternalEmbedTarget,
getWikiLinkTarget,
} from "./link-utils/link-target-getters";
import {
getFirstExternalEmbed,
getFirstInternalEmbed,
} from "./link-utils/link-getters";
/**
* Formats the file's data for rendering
@ -56,12 +55,12 @@ export const formatFileDataForRender = ({
}): FileRenderData => {
const { name, basename, extension, path } = file;
const { coverImageSource } = settings.views.grid;
const { coverImageSources } = settings.views.grid;
const {
createdDate: createdDateProp,
modifiedDate: modifiedDateProp,
url: urlProp,
imageUrl: imageUrlProp,
image: imageProp,
favorite: favoriteProp,
custom1: custom1Prop,
custom2: custom2Prop,
@ -149,58 +148,62 @@ export const formatFileDataForRender = ({
imageUrl = app.vault.getResourcePath(file);
}
//Handle image property
if (imageUrl === null && coverImageSource !== "off") {
const loadedUrl = loadPropertyValue<string>(
fileFrontmatter,
imageUrlProp,
PropertyType.TEXT
);
if (loadedUrl !== null) {
if (isInternalEmbed(loadedUrl)) {
const target = getInternalEmbedTarget(loadedUrl);
if (target) {
const uri = getURIForWikiLinkTarget(app, target, path);
if (uri) {
imageUrl = uri;
for (const imageSource of coverImageSources) {
const { type, isEnabled } = imageSource;
if (isEnabled) {
switch (type) {
case "image-property": {
const loadedUrl = handleImagePropertyCoverSource(
app,
fileFrontmatter,
path,
imageProp
);
if (loadedUrl !== null) {
imageUrl = loadedUrl;
}
break;
}
} else if (isWikiLink(loadedUrl)) {
const target = getWikiLinkTarget(loadedUrl);
if (target) {
const uri = getURIForWikiLinkTarget(app, target, path);
if (uri) {
imageUrl = uri;
case "url-property": {
const loadedUrl = handleUrlPropertyCoverSource(
fileFrontmatter,
urlProp
);
if (loadedUrl !== null) {
imageUrl = loadedUrl;
}
break;
}
} else if (isExternalEmbed(loadedUrl)) {
const target = getExternalEmbedTarget(loadedUrl);
if (target) {
imageUrl = target;
case "frontmatter": {
const loadedUrl = handleFrontmatterCoverSource(
app,
fileFrontmatter,
path
);
if (loadedUrl !== null) {
imageUrl = loadedUrl;
}
break;
}
} else if (isUrl(loadedUrl)) {
imageUrl = loadedUrl;
case "body": {
const loadedUrl = handleBodyCoverSource(
app,
path,
fileContent
);
if (loadedUrl !== null) {
imageUrl = loadedUrl;
}
break;
}
default:
throw new Error(
`Unhandled cover image source type: ${type}`
);
}
if (imageUrl !== null) {
break; // Exit the loop if a non-null URL is found
}
}
}
//Handle image in frontmatter
if (imageUrl === null && coverImageSource !== "off") {
const textProperties: FileTextProperties = loadTextProperties(
app,
fileFrontmatter
);
for (const property of textProperties) {
//TODO implement
}
}
if (imageUrl === null && coverImageSource === "frontmatter-and-body") {
if (fileContent !== null) {
const body = removeFrontmatter(fileContent);
//TODO implement
}
}
@ -224,3 +227,105 @@ export const formatFileDataForRender = ({
custom3,
};
};
const handleImagePropertyCoverSource = (
app: App,
fileFrontmatter: FrontMatterCache | undefined,
filePath: string,
imageProperty: string
) => {
const propertyValue = loadPropertyValue<string>(
fileFrontmatter,
imageProperty,
PropertyType.TEXT
);
return getImageUrlFromProperty(app, filePath, propertyValue);
};
const handleUrlPropertyCoverSource = (
fileFrontmatter: FrontMatterCache | undefined,
urlProperty: string
) => {
const propertyValue = loadPropertyValue<string>(
fileFrontmatter,
urlProperty,
PropertyType.TEXT
);
if (propertyValue !== null) {
if (isUrl(propertyValue)) {
return propertyValue;
}
}
return null;
};
const handleFrontmatterCoverSource = (
app: App,
fileFrontmatter: FrontMatterCache | undefined,
filePath: string
) => {
const textProperties: FileTextProperties = loadTextProperties(
app,
fileFrontmatter
);
for (const property of textProperties) {
const { value } = property;
const imageUrl = getImageUrlFromProperty(app, filePath, value);
if (imageUrl) {
return imageUrl;
}
}
return null;
};
const handleBodyCoverSource = (
app: App,
filePath: string,
fileContent: string | null
) => {
if (fileContent === null) return null;
const body = removeFrontmatter(fileContent);
const firstInternalLink = getFirstInternalEmbed(body);
if (firstInternalLink) {
const target = getInternalEmbedTarget(firstInternalLink);
if (target) {
const uri = getURIForWikiLinkTarget(app, target, filePath);
if (uri) {
return uri;
}
}
}
const firstExternalLink = getFirstExternalEmbed(body);
if (firstExternalLink) {
const target = getExternalEmbedTarget(firstExternalLink);
if (target) {
return target;
}
}
return null;
};
const getImageUrlFromProperty = (
app: App,
filePath: string,
propertyValue: string | null
) => {
if (propertyValue === null) return null;
if (isWikiLink(propertyValue)) {
const target = getWikiLinkTarget(propertyValue);
if (target) {
const uri = getURIForWikiLinkTarget(app, target, filePath);
if (uri) {
return uri;
}
}
} else if (isUrl(propertyValue)) {
return propertyValue;
}
return null;
};

View file

@ -9,7 +9,7 @@ const ENTRY_EXPIRATION_TIME = ONE_WEEK_MILLIS;
interface SocialMediaImageEntry {
url: string;
socialMediaImageUrl: string;
socialMediaImageUrl: string | null; // null if no social media image found
timestamp: number;
}
@ -42,7 +42,7 @@ export const getSocialMediaImageEntry = async (url: string) => {
*/
export const putSocialMediaImageUrl = async (
url: string,
socialMediaImageUrl: string
socialMediaImageUrl: string | null
) => {
const db = await openDatabase();
db.put(STORE_NAME, {

View file

@ -0,0 +1,16 @@
import { CoverImageSourceType } from "src/types";
export const getDisplayNameForImageSource = (type: CoverImageSourceType) => {
switch (type) {
case "image-property":
return "Image Property";
case "url-property":
return "URL";
case "frontmatter":
return "Frontmatter";
case "body":
return "Body";
default:
return "";
}
};

View file

@ -0,0 +1,129 @@
<script lang="ts">
import store from "../shared/services/store";
import VaultExplorerPlugin from "src/main";
import { CoverImageSource, CoverImageSourceType } from "src/types";
import { getDisplayNameForImageSource } from "./display-name";
import Icon from "../shared/components/icon.svelte";
import { PluginEvent } from "src/event/types";
import EventManager from "src/event/event-manager";
let plugin: VaultExplorerPlugin | null = null;
let coverImageSources: CoverImageSource[] = [];
store.plugin.subscribe((p) => {
plugin = p;
coverImageSources = p.settings.views.grid.coverImageSources;
});
function handleSourceClick(type: CoverImageSourceType) {
const updatedSources = coverImageSources.map((source) => {
if (source.type === type) {
return {
...source,
isEnabled: !source.isEnabled,
};
}
return source;
});
coverImageSources = updatedSources;
}
async function saveSettings() {
if (!plugin) return;
plugin.settings.views.grid.coverImageSources = coverImageSources;
await plugin.saveSettings();
EventManager.getInstance().emit(
PluginEvent.COVER_IMAGE_SOURCE_SETTING_CHANGE,
);
}
function handleSourceDragOver(e: Event) {
e.preventDefault();
}
function handleSourceDragStart(e: Event, type: CoverImageSourceType) {
(e as any).dataTransfer.setData("text", type);
(e as any).dataTransfer.effectAllowed = "move";
}
function handleSourceDrop(e: Event, id: string) {
const dragId = (e as any).dataTransfer.getData("text");
(e as any).dataTransfer.dropEffect = "move";
const draggedIndex = coverImageSources.findIndex(
(source) => source.type === dragId,
);
const dragged = coverImageSources.find(
(source) => source.type === dragId,
);
const droppedIndex = coverImageSources.findIndex(
(source) => source.type === id,
);
const dropped = coverImageSources.find((source) => source.type === id);
if (!dragged || !dropped || draggedIndex === -1 || droppedIndex === -1)
return;
let newCoverImageSources = [...coverImageSources];
newCoverImageSources[draggedIndex] = dropped;
newCoverImageSources[droppedIndex] = dragged;
coverImageSources = newCoverImageSources;
}
$: coverImageSources, saveSettings();
</script>
<div class="setting-item">
<div class="setting-item-info">
<div class="setting-item-name">Image source</div>
<div class="setting-item-description">
<div class="vault-explorer-image-source-setting-container">
{#each coverImageSources as source}
<div
tabindex="0"
role="button"
draggable="true"
on:dragstart={(e) =>
handleSourceDragStart(e, source.type)}
on:dragover={handleSourceDragOver}
on:drop={(e) => handleSourceDrop(e, source.type)}
class="vault-explorer-image-source-setting-row"
on:click={() => handleSourceClick(source.type)}
on:keydown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleSourceClick(source.type);
}
}}
>
<div class="vault-explorer-image-source-setting-title">
<Icon iconId={source.isEnabled ? "check" : "x"} />
<div>
{getDisplayNameForImageSource(source.type)}
</div>
</div>
<Icon iconId="grip-horizontal" />
</div>
{/each}
</div>
</div>
</div>
</div>
<style>
.vault-explorer-image-source-setting-title {
display: flex;
column-gap: 8px;
}
.vault-explorer-image-source-setting-row {
display: flex;
justify-content: space-between;
padding: 8px 8px;
}
.vault-explorer-image-source-setting-row:hover {
background-color: var(--background-modifier-hover);
}
</style>

View file

@ -117,9 +117,6 @@
<style>
.vault-explorer-setting-message {
color: var(--text-muted);
font-size: var(--font-smallest);
padding-top: var(--size-4-1);
line-height: var(--line-height-tight);
}
.vault-explorer-setting-message--success {

View file

@ -15,7 +15,7 @@ export function isVaultExplorerPluginSettings(obj: unknown): obj is VaultExplore
typeof typedObj["properties"] === "function") &&
typeof typedObj["properties"]["favorite"] === "string" &&
typeof typedObj["properties"]["url"] === "string" &&
typeof typedObj["properties"]["imageUrl"] === "string" &&
typeof typedObj["properties"]["image"] === "string" &&
typeof typedObj["properties"]["createdDate"] === "string" &&
typeof typedObj["properties"]["modifiedDate"] === "string" &&
typeof typedObj["properties"]["custom1"] === "string" &&
@ -595,9 +595,17 @@ export function isVaultExplorerPluginSettings(obj: unknown): obj is VaultExplore
typeof typedObj["views"]["grid"] === "object" ||
typeof typedObj["views"]["grid"] === "function") &&
typeof typedObj["views"]["grid"]["isEnabled"] === "boolean" &&
(typedObj["views"]["grid"]["coverImageSource"] === "frontmatter-only" ||
typedObj["views"]["grid"]["coverImageSource"] === "frontmatter-and-body" ||
typedObj["views"]["grid"]["coverImageSource"] === "off") &&
Array.isArray(typedObj["views"]["grid"]["coverImageSources"]) &&
typedObj["views"]["grid"]["coverImageSources"].every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e["type"] === "image-property" ||
e["type"] === "url-property" ||
e["type"] === "frontmatter" ||
e["type"] === "body") &&
typeof e["isEnabled"] === "boolean"
) &&
typeof typedObj["views"]["grid"]["loadSocialMediaImage"] === "boolean" &&
(typedObj["views"]["list"] !== null &&
typeof typedObj["views"]["list"] === "object" ||
@ -638,7 +646,6 @@ export function isVaultExplorerPluginSettings(obj: unknown): obj is VaultExplore
typedObj["currentView"] === TExplorerView.TABLE ||
typedObj["currentView"] === TExplorerView.RECOMMENDED ||
typedObj["currentView"] === TExplorerView.RELATED) &&
typeof typedObj["enableScrollButtons"] === "boolean" &&
typeof typedObj["pageSize"] === "number" &&
typeof typedObj["filterGroupsWidth"] === "string" &&
typeof typedObj["shouldWrapFilterGroups"] === "boolean" &&

View file

@ -3,7 +3,7 @@ export interface VaultExplorerPluginSettings {
properties: {
favorite: string;
url: string;
imageUrl: string;
image: string;
createdDate: string;
modifiedDate: string;
custom1: string;
@ -30,7 +30,6 @@ export interface VaultExplorerPluginSettings {
enableClockUpdates: boolean;
enableFileIcons: boolean;
currentView: TExplorerView | null;
enableScrollButtons: boolean;
pageSize: number;
filterGroupsWidth: string;
shouldWrapFilterGroups: boolean;
@ -53,14 +52,20 @@ export interface TListView extends BaseView {
}
export interface TGridView extends BaseView {
coverImageSource: CoverImageSource;
coverImageSources: CoverImageSource[];
loadSocialMediaImage: boolean;
}
export type CoverImageSource =
| "frontmatter-only"
| "frontmatter-and-body"
| "off";
export interface CoverImageSource {
type: CoverImageSourceType;
isEnabled: boolean;
}
export type CoverImageSourceType =
| "image-property"
| "url-property"
| "frontmatter"
| "body";
export interface TDashboardView extends BaseView {}

318
src/types/types-1.36.3.ts Normal file
View file

@ -0,0 +1,318 @@
export interface VaultExplorerPluginSettings_1_36_3 {
properties: {
favorite: string;
url: string;
imageUrl: string;
createdDate: string;
modifiedDate: string;
custom1: string;
custom2: string;
custom3: string;
};
filters: {
search: TSearchFilter;
favorites: TFavoritesFilter;
sort: TSortFilter;
timestamp: TTimestampFilter;
custom: TCustomFilter;
};
views: {
dashboard: TDashboardView;
grid: TGridView;
list: TListView;
table: TTableView;
feed: TFeedView;
recommended: TRecommendedView;
related: TRelatedView;
};
titleWrapping: WordBreak;
enableClockUpdates: boolean;
enableFileIcons: boolean;
currentView: TExplorerView | null;
enableScrollButtons: boolean;
pageSize: number;
filterGroupsWidth: string;
shouldWrapFilterGroups: boolean;
viewOrder: TExplorerView[];
configDir: string;
pluginVersion: string | null;
logLevel: string;
}
type FileInteractionStyle = "title" | "content";
interface BaseView {
isEnabled: boolean;
}
interface TTableView extends BaseView {}
interface TListView extends BaseView {
showTags: boolean;
}
interface TGridView extends BaseView {
coverImageSource: CoverImageSource;
loadSocialMediaImage: boolean;
}
type CoverImageSource = "frontmatter-only" | "frontmatter-and-body" | "off";
interface TDashboardView extends BaseView {}
type CollapseStyle = "no-new-lines" | "no-extra-new-lines";
interface TFeedView extends BaseView {
collapseStyle: CollapseStyle;
removeH1: boolean;
lineClampSmall: number;
lineClampMedium: number;
lineClampLarge: number;
}
interface TRecommendedView extends BaseView {}
interface TRelatedView extends BaseView {}
interface BaseFilter {
isEnabled: boolean;
}
interface TSearchFilter extends BaseFilter {
value: string;
}
interface TFavoritesFilter extends BaseFilter {
value: boolean;
}
interface TSortFilter extends BaseFilter {
value: SortFilterOption;
}
interface TTimestampFilter extends BaseFilter {
value: TimestampFilterOption;
}
interface TCustomFilter extends BaseFilter {
selectedGroupId: string;
groups: TFilterGroup[];
}
type TimestampFilterOption =
| "created-today"
| "modified-today"
| "created-this-week"
| "modified-this-week"
| "created-2-weeks"
| "modified-2-weeks"
| "all";
type SortFilterOption =
| "file-name-asc"
| "file-name-desc"
| "modified-asc"
| "modified-desc"
| "random";
type WordBreak = "normal" | "break-word";
enum TExplorerView {
DASHBOARD = "dashboard",
GRID = "grid",
LIST = "list",
FEED = "feed",
TABLE = "table",
RECOMMENDED = "recommended",
RELATED = "related",
}
type FilterOperator = "and" | "or";
enum TextFilterCondition {
IS = "is",
IS_NOT = "is-not",
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
STARTS_WITH = "starts-with",
ENDS_WITH = "ends-with",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum ListFilterCondition {
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum NumberFilterCondition {
IS_EQUAL = "is-equal",
IS_NOT_EQUAL = "is-not-equal",
IS_GREATER = "is-greater",
IS_LESS = "is-less",
IS_GREATER_OR_EQUAL = "is-greater-or-equal",
IS_LESS_OR_EQUAL = "is-less-or-equal",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum CheckboxFilterCondition {
IS = "is",
IS_NOT = "is-not",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum DateFilterCondition {
IS = "is",
IS_BEFORE = "is-before",
IS_AFTER = "is-after",
IS_ON_OR_BEFORE = "is-on-or-before",
IS_ON_OR_AFTER = "is-on-or-after",
EXISTS = "exists",
DOES_NOT_EXIST = "does-not-exist",
}
enum ContentFilterCondition {
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
IS_EMPTY = "is-empty",
IS_NOT_EMPTY = "is-not-empty",
}
enum FolderFilterCondition {
IS = "is",
IS_NOT = "is-not",
}
enum FileNameFilterCondition {
IS = "is",
IS_NOT = "is-not",
CONTAINS = "contains",
DOES_NOT_CONTAIN = "does-not-contain",
STARTS_WITH = "starts-with",
ENDS_WITH = "ends-with",
}
type FilterCondition =
| TextFilterCondition
| NumberFilterCondition
| DateFilterCondition
| CheckboxFilterCondition
| ListFilterCondition
| ContentFilterCondition
| FolderFilterCondition
| FileNameFilterCondition;
//This matches the Obsidian property types
enum PropertyType {
TEXT = "text",
NUMBER = "number",
LIST = "list",
CHECKBOX = "checkbox",
DATE = "date",
DATETIME = "datetime",
}
enum FilterRuleType {
PROPERTY = "property",
FOLDER = "folder",
FILE_NAME = "file-name",
CONTENT = "content",
}
enum DatePropertyFilterValue {
TODAY = "today",
TOMORROW = "tomorrow",
YESTERDAY = "yesterday",
ONE_WEEK_FROM_NOW = "one-week-from-now",
ONE_WEEK_AGO = "one-week-ago",
ONE_MONTH_FROM_NOW = "one-month-from-now",
ONE_MONTH_AGO = "one-month-ago",
CUSTOM = "custom",
}
interface BaseFilterRule {
id: string;
operator: FilterOperator;
type: FilterRuleType;
condition: FilterCondition;
isEnabled: boolean;
value: string;
matchWhenPropertyDNE: boolean;
}
interface TextPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.TEXT;
propertyName: string;
condition: TextFilterCondition;
}
interface NumberPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.NUMBER;
propertyName: string;
condition: NumberFilterCondition;
}
interface ListPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.LIST;
propertyName: string;
condition: ListFilterCondition;
}
interface CheckboxPropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.CHECKBOX;
propertyName: string;
condition: CheckboxFilterCondition;
}
interface DatePropertyFilterRule extends BaseFilterRule {
type: FilterRuleType.PROPERTY;
propertyType: PropertyType.DATE | PropertyType.DATETIME;
propertyName: string;
condition: DateFilterCondition;
valueData: string;
}
interface FolderFilterRule extends BaseFilterRule {
type: FilterRuleType.FOLDER;
condition: FolderFilterCondition;
includeSubfolders: boolean;
}
interface FileNameFilterRule extends BaseFilterRule {
type: FilterRuleType.FILE_NAME;
condition: FileNameFilterCondition;
}
interface ContentFilterRule extends BaseFilterRule {
type: FilterRuleType.CONTENT;
condition: ContentFilterCondition;
}
type TFilterRule =
| PropertyFilterRule
| FolderFilterRule
| FileNameFilterRule
| ContentFilterRule;
type PropertyFilterRule =
| TextPropertyFilterRule
| NumberPropertyFilterRule
| ListPropertyFilterRule
| CheckboxPropertyFilterRule
| DatePropertyFilterRule;
interface TFilterGroup {
id: string;
name: string;
rules: TFilterRule[];
isEnabled: boolean;
isSticky: boolean;
}

View file

@ -8,7 +8,7 @@
"creationDate": "creation",
"modifiedDate": "modification",
"createdDate": "creation",
"imageUrl": "image"
"image": "image"
},
"filters": {
"search": {
@ -51,7 +51,24 @@
"grid": {
"isEnabled": true,
"loadSocialMediaImage": true,
"coverImageSource": "frontmatter-and-body"
"coverImageSources": [
{
"type": "image-property",
"isEnabled": true
},
{
"type": "url-property",
"isEnabled": true
},
{
"type": "frontmatter",
"isEnabled": true
},
{
"type": "body",
"isEnabled": true
}
]
},
"list": {
"isEnabled": true,
@ -82,7 +99,6 @@
"titleWrapping": "normal",
"enableClockUpdates": true,
"enableFileIcons": true,
"enableScrollButtons": true,
"filterGroupsWidth": "759px",
"shouldWrapFilterGroups": false,
"pageSize": 25,
@ -92,6 +108,6 @@
"feed"
],
"configDir": ".vaultexplorer",
"pluginVersion": "1.36.2",
"pluginVersion": "1.37.0",
"logLevel": "trace"
}

View file

@ -1,2 +1 @@
![cover](https://plus.unsplash.com/premium_photo-1721926057308-2aa7ce470776?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
![cover](https://plus.unsplash.com/premium_photo-1721926057308-2aa7ce470776?q=80&w=2940&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)

View file

@ -1,4 +0,0 @@
---
banner: "[[pexels-tomfisk-1653823.jpg]]"
image:
---

View file

@ -1,4 +0,0 @@
---
banner: "[[pexels-tomfisk-1653823.jpg]]"
url: https://vaultexplorer.com
---

View file

@ -0,0 +1,3 @@
---
image: "![[pexels-tomfisk-1653823.jpg]]"
---

View file

@ -0,0 +1,3 @@
---
image: https://vaultexplorer.com
---

View file

@ -0,0 +1,3 @@
---
image: "[[pexels-tomfisk-1653823.jpg]]"
---

View file

@ -1,4 +0,0 @@
---
image: "[[pexels-kelly-1179532-23732393.jpg]]"
favorite: false
---

View file

@ -1,4 +0,0 @@
---
url: https://vaultexplorer.com
banner: "[[pexels-tomfisk-1653823.jpg]]"
---

View file

@ -0,0 +1,3 @@
---
url: https://vaultexplorer.com
---

View file

@ -126,5 +126,6 @@
"1.36.0": "1.4.13",
"1.36.1": "1.4.13",
"1.36.2": "1.4.13",
"1.36.3": "1.4.13"
"1.36.3": "1.4.13",
"1.37.0": "1.4.13"
}