mirror of
https://github.com/ccmdi/obsidian-map-plus.git
synced 2026-07-22 06:43:14 +00:00
commit
d67c0e1260
7 changed files with 563 additions and 31 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.0.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"name": "mapplus",
|
||||
"version": "1.0.2",
|
||||
"description": "A performant and customizable map inside Bases",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
12
src/main.ts
12
src/main.ts
|
|
@ -2,9 +2,11 @@ import { Plugin } from 'obsidian';
|
|||
import { MapBasesView } from './views/map-bases-view';
|
||||
import { MapTagSettings, DEFAULT_MAP_TAG_SETTINGS } from './settings/map-tag-settings';
|
||||
import { MapSettingTab, MapPluginSettings, DEFAULT_SETTINGS } from './settings/map-settings';
|
||||
import { ThumbnailCacheManager } from './thumbnail-cache';
|
||||
|
||||
export default class MapPlugin extends Plugin {
|
||||
settings: MapPluginSettings;
|
||||
thumbnailCache: ThumbnailCacheManager;
|
||||
|
||||
get tagSettings(): MapTagSettings {
|
||||
return this.settings.tagSettings;
|
||||
|
|
@ -13,6 +15,16 @@ export default class MapPlugin extends Plugin {
|
|||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
||||
this.thumbnailCache = new ThumbnailCacheManager(this, this.app);
|
||||
|
||||
if (this.settings.enableThumbnailCache) {
|
||||
await this.thumbnailCache.loadCache();
|
||||
|
||||
setTimeout(() => {
|
||||
this.thumbnailCache.generatePendingThumbnails();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
this.registerBasesView('map', {
|
||||
name: 'Map',
|
||||
icon: 'lucide-map',
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { MapView as MapViewType } from '@deck.gl/core';
|
|||
|
||||
import { MapTagSettings } from './settings/map-tag-settings';
|
||||
import type MapPlugin from './main';
|
||||
import type { ThumbnailCacheManager } from './thumbnail-cache';
|
||||
|
||||
function easeCubic(t: number): number {
|
||||
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
|
|
@ -52,6 +53,7 @@ export interface MapRendererOptions {
|
|||
app: App;
|
||||
settings: MapPlugin['settings'];
|
||||
tagSettings?: MapTagSettings;
|
||||
thumbnailCache?: ThumbnailCacheManager;
|
||||
options: {
|
||||
center?: [number, number];
|
||||
zoom?: number;
|
||||
|
|
@ -60,8 +62,6 @@ export interface MapRendererOptions {
|
|||
markerSize?: number;
|
||||
markerColor?: string;
|
||||
tileLayer?: string;
|
||||
showSearch?: boolean;
|
||||
showTags?: boolean;
|
||||
autoCenter?: boolean;
|
||||
onMarkerClick?: (point: MapPoint, event: MjolnirEvent) => void;
|
||||
onTilesLoaded?: () => void;
|
||||
|
|
@ -418,15 +418,20 @@ export async function createMapRenderer(config: MapRendererOptions): Promise<Dec
|
|||
|
||||
// show cover image
|
||||
if (point.cover && point.file) {
|
||||
const coverFile = app.metadataCache.getFirstLinkpathDest(point.cover, point.file.path);
|
||||
if (coverFile) {
|
||||
let thumbnailSrc: string | null = null;
|
||||
|
||||
if (config.thumbnailCache && settings.enableThumbnailCache) {
|
||||
thumbnailSrc = await config.thumbnailCache.getThumbnail(point.cover, point.file);
|
||||
}
|
||||
|
||||
// Check if this update is still valid after async operation
|
||||
if (thisUpdateId !== tooltipUpdateId) return;
|
||||
|
||||
if (thumbnailSrc) {
|
||||
const img = new Image();
|
||||
img.classList.add('map-tooltip-cover-image');
|
||||
img.src = thumbnailSrc;
|
||||
|
||||
const src = app.vault.getResourcePath(coverFile);
|
||||
img.src = src;
|
||||
|
||||
// Decode image off main thread, then append to DOM
|
||||
const imagePromise = img.decode()
|
||||
.then(() => {
|
||||
if (thisUpdateId === tooltipUpdateId) {
|
||||
|
|
@ -440,6 +445,29 @@ export async function createMapRenderer(config: MapRendererOptions): Promise<Dec
|
|||
});
|
||||
|
||||
renderPromises.push(imagePromise);
|
||||
} else {
|
||||
const coverFile = app.metadataCache.getFirstLinkpathDest(point.cover, point.file.path);
|
||||
if (coverFile) {
|
||||
const img = new Image();
|
||||
img.classList.add('map-tooltip-cover-image');
|
||||
|
||||
const src = app.vault.getResourcePath(coverFile);
|
||||
img.src = src;
|
||||
|
||||
const imagePromise = img.decode()
|
||||
.then(() => {
|
||||
if (thisUpdateId === tooltipUpdateId) {
|
||||
tooltip.insertBefore(img, tooltip.firstChild);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (thisUpdateId === tooltipUpdateId) {
|
||||
tooltip.insertBefore(img, tooltip.firstChild);
|
||||
}
|
||||
});
|
||||
|
||||
renderPromises.push(imagePromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,11 +479,13 @@ export async function createMapRenderer(config: MapRendererOptions): Promise<Dec
|
|||
const propsContainer = tooltip.createEl('div', { cls: 'map-tooltip-property-container' });
|
||||
|
||||
point.properties.forEach(prop => {
|
||||
if (prop.name.toLowerCase() === 'tags') return;
|
||||
|
||||
const propEl = propsContainer.createEl('div', { cls: 'map-tooltip-property' });
|
||||
|
||||
|
||||
const labelEl = propEl.createEl('span', { cls: 'map-tooltip-property-label' });
|
||||
labelEl.textContent = prop.name + ':';
|
||||
|
||||
|
||||
const valueEl = propEl.createEl('span', { cls: 'map-tooltip-property-value' });
|
||||
|
||||
// render value
|
||||
|
|
@ -473,14 +503,13 @@ export async function createMapRenderer(config: MapRendererOptions): Promise<Dec
|
|||
});
|
||||
}
|
||||
|
||||
//todo
|
||||
if (options.showTags && point.tags && point.tags.length > 0) {
|
||||
// show tags
|
||||
if (point.tags && point.tags.length > 0) {
|
||||
const tagsEl = tooltip.createEl('div', { cls: 'map-tooltip-tags' });
|
||||
point.tags.forEach((tag) => {
|
||||
const tagEl = tagsEl.createEl('a', { cls: 'tag' });
|
||||
tagEl.textContent = `#${tag}`;
|
||||
});
|
||||
tooltip.appendChild(tagsEl);
|
||||
}
|
||||
|
||||
// Wait for all images to load before showing tooltip
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ export interface MapPluginSettings {
|
|||
iconFill: boolean;
|
||||
autoCenter: boolean;
|
||||
transitionDuration: number;
|
||||
enableThumbnailCache: boolean;
|
||||
thumbnailTargetSize: number;
|
||||
tagSettings: MapTagSettings;
|
||||
}
|
||||
|
||||
|
|
@ -19,6 +21,8 @@ export const DEFAULT_SETTINGS: MapPluginSettings = {
|
|||
iconFill: false,
|
||||
autoCenter: true,
|
||||
transitionDuration: 1000,
|
||||
enableThumbnailCache: false,
|
||||
thumbnailTargetSize: 25,
|
||||
tagSettings: DEFAULT_MAP_TAG_SETTINGS
|
||||
};
|
||||
|
||||
|
|
@ -46,6 +50,9 @@ export class MapSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value) => {
|
||||
this.plugin.settings.latKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
setTimeout(() => {
|
||||
this.plugin.refreshAllMapViews();
|
||||
}, 1000);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
|
|
@ -57,6 +64,9 @@ export class MapSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value) => {
|
||||
this.plugin.settings.lngKey = value;
|
||||
await this.plugin.saveSettings();
|
||||
setTimeout(() => {
|
||||
this.plugin.refreshAllMapViews();
|
||||
}, 1000);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
|
|
@ -68,7 +78,9 @@ export class MapSettingTab extends PluginSettingTab {
|
|||
.onChange(async (value) => {
|
||||
this.plugin.settings.strokeWidth = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.refreshAllMapViews();
|
||||
setTimeout(() => {
|
||||
this.plugin.refreshAllMapViews();
|
||||
}, 250);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
|
|
@ -103,6 +115,106 @@ export class MapSettingTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
containerEl.createEl('h3', { text: 'Performance' });
|
||||
|
||||
const thumbnailToggle = new Setting(containerEl)
|
||||
.setName('Enable thumbnail cache')
|
||||
.setDesc('Cache thumbnails of location cover images - instant tooltips')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableThumbnailCache)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableThumbnailCache = value;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
if (value) {
|
||||
await this.plugin.thumbnailCache.loadCache();
|
||||
setTimeout(() => {
|
||||
this.plugin.thumbnailCache.generatePendingThumbnails();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
this.display();
|
||||
}));
|
||||
|
||||
if (this.plugin.settings.enableThumbnailCache) {
|
||||
new Setting(containerEl)
|
||||
.setName('Thumbnail target size')
|
||||
.setDesc('Target file size for cached thumbnails (in KB)')
|
||||
.addSlider(slider => slider
|
||||
.setLimits(10, 50, 5)
|
||||
.setValue(this.plugin.settings.thumbnailTargetSize)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.thumbnailTargetSize = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
const stats = this.plugin.thumbnailCache.getCacheStats();
|
||||
const sizeKB = (stats.totalSize / 1024).toFixed(1);
|
||||
const isGenerating = this.plugin.thumbnailCache.isGenerating();
|
||||
|
||||
const statusSetting = new Setting(containerEl)
|
||||
.setName('Thumbnail cache status');
|
||||
|
||||
const updateStatus = () => {
|
||||
const currentStats = this.plugin.thumbnailCache.getCacheStats();
|
||||
const currentSizeKB = (currentStats.totalSize / 1024).toFixed(1);
|
||||
|
||||
if (currentStats.pending > 0) {
|
||||
statusSetting.setDesc(`${currentStats.count} thumbnails cached (${currentSizeKB} KB), ${currentStats.pending} pending`);
|
||||
} else {
|
||||
statusSetting.setDesc(`${currentStats.count} thumbnails cached (${currentSizeKB} KB)`);
|
||||
}
|
||||
};
|
||||
|
||||
updateStatus();
|
||||
|
||||
if (stats.pending > 0 || isGenerating) {
|
||||
const refreshInterval = window.setInterval(() => {
|
||||
updateStatus();
|
||||
if (this.plugin.thumbnailCache.getCacheStats().pending === 0 && !this.plugin.thumbnailCache.isGenerating()) {
|
||||
clearInterval(refreshInterval);
|
||||
this.display();
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
statusSetting.addButton(button => button
|
||||
.setButtonText('Rebuild cache')
|
||||
.onClick(async () => {
|
||||
button.setDisabled(true);
|
||||
button.setButtonText('Rebuilding...');
|
||||
|
||||
const progressInterval = window.setInterval(() => {
|
||||
updateStatus();
|
||||
}, 500);
|
||||
|
||||
// force refresh markers for cover context
|
||||
this.plugin.refreshAllMapViews();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
await this.plugin.thumbnailCache.rebuildCache((current, total) => {
|
||||
button.setButtonText(`Rebuilding ${current}/${total}...`);
|
||||
});
|
||||
|
||||
clearInterval(progressInterval);
|
||||
button.setButtonText('Rebuild cache');
|
||||
button.setDisabled(false);
|
||||
this.display();
|
||||
}));
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Clear thumbnail cache')
|
||||
.setDesc('Delete all cached thumbnails')
|
||||
.addButton(button => button
|
||||
.setButtonText('Clear cache')
|
||||
.setWarning()
|
||||
.onClick(async () => {
|
||||
await this.plugin.thumbnailCache.clearCache();
|
||||
this.display();
|
||||
}));
|
||||
}
|
||||
|
||||
renderTagCustomizations(containerEl, this.app, this.plugin);
|
||||
}
|
||||
}
|
||||
368
src/thumbnail-cache.ts
Normal file
368
src/thumbnail-cache.ts
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
import { App, TFile, normalizePath } from 'obsidian';
|
||||
import type MapPlugin from './main';
|
||||
|
||||
export interface ThumbnailCacheEntry {
|
||||
thumbnailPath: string;
|
||||
sourceModified: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ThumbnailCacheMetadata {
|
||||
entries: Record<string, ThumbnailCacheEntry>;
|
||||
pendingGeneration: string[];
|
||||
}
|
||||
|
||||
const THUMBNAIL_WIDTH = 300;
|
||||
const THUMBNAIL_HEIGHT = 200;
|
||||
const CACHE_DIR = '.obsidian/plugins/mapplus/.cache';
|
||||
|
||||
export class ThumbnailCacheManager {
|
||||
private plugin: MapPlugin;
|
||||
private app: App;
|
||||
private cache: ThumbnailCacheMetadata;
|
||||
private cacheLoaded: boolean = false;
|
||||
private generationInProgress: boolean = false;
|
||||
private cacheDir: string;
|
||||
|
||||
constructor(plugin: MapPlugin, app: App) {
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
this.cache = {
|
||||
entries: {},
|
||||
pendingGeneration: []
|
||||
};
|
||||
this.cacheDir = normalizePath(CACHE_DIR);
|
||||
}
|
||||
|
||||
async loadCache(): Promise<void> {
|
||||
if (this.cacheLoaded) return;
|
||||
|
||||
try {
|
||||
await this.ensureCacheDir();
|
||||
|
||||
const metadataPath = normalizePath(`${this.cacheDir}/metadata.json`);
|
||||
const exists = await this.app.vault.adapter.exists(metadataPath);
|
||||
|
||||
if (exists) {
|
||||
const content = await this.app.vault.adapter.read(metadataPath);
|
||||
this.cache = JSON.parse(content);
|
||||
}
|
||||
|
||||
this.cacheLoaded = true;
|
||||
} catch (e) {
|
||||
console.error('Failed to load thumbnail cache:', e);
|
||||
this.cache = {
|
||||
entries: {},
|
||||
pendingGeneration: []
|
||||
};
|
||||
this.cacheLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
async saveCache(): Promise<void> {
|
||||
try {
|
||||
await this.ensureCacheDir();
|
||||
|
||||
const metadataPath = normalizePath(`${this.cacheDir}/metadata.json`);
|
||||
await this.app.vault.adapter.write(metadataPath, JSON.stringify(this.cache, null, 2));
|
||||
} catch (e) {
|
||||
console.error('Failed to save thumbnail cache:', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureCacheDir(): Promise<void> {
|
||||
const exists = await this.app.vault.adapter.exists(this.cacheDir);
|
||||
if (!exists) {
|
||||
await this.app.vault.adapter.mkdir(this.cacheDir);
|
||||
}
|
||||
}
|
||||
|
||||
private getThumbnailFilename(sourcePath: string): string {
|
||||
const hash = this.hashString(sourcePath);
|
||||
return `thumb_${hash}.jpg`;
|
||||
}
|
||||
|
||||
private hashString(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return Math.abs(hash).toString(36);
|
||||
}
|
||||
|
||||
async getThumbnail(coverPath: string, sourceFile: TFile): Promise<string | null> {
|
||||
if (!this.plugin.settings.enableThumbnailCache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.loadCache();
|
||||
|
||||
const coverFile = this.app.metadataCache.getFirstLinkpathDest(coverPath, sourceFile.path);
|
||||
if (!coverFile) return null;
|
||||
|
||||
const cacheKey = coverFile.path;
|
||||
const cached = this.cache.entries[cacheKey];
|
||||
|
||||
if (cached) {
|
||||
if (cached.sourceModified === coverFile.stat.mtime) {
|
||||
const thumbnailExists = await this.app.vault.adapter.exists(cached.thumbnailPath);
|
||||
if (thumbnailExists) {
|
||||
const arrayBuffer = await this.app.vault.adapter.readBinary(cached.thumbnailPath);
|
||||
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
}
|
||||
|
||||
delete this.cache.entries[cacheKey];
|
||||
await this.saveCache();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async generateThumbnailFromFile(file: TFile): Promise<{ thumbnailPath: string; size: number } | null> {
|
||||
try {
|
||||
const targetSize = this.plugin.settings.thumbnailTargetSize * 1000;
|
||||
const arrayBuffer = await this.app.vault.readBinary(file);
|
||||
const blob = new Blob([arrayBuffer]);
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return null;
|
||||
|
||||
const aspectRatio = bitmap.width / bitmap.height;
|
||||
let width = THUMBNAIL_WIDTH;
|
||||
let height = THUMBNAIL_HEIGHT;
|
||||
|
||||
if (aspectRatio > width / height) {
|
||||
height = width / aspectRatio;
|
||||
} else {
|
||||
width = height * aspectRatio;
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
ctx.drawImage(bitmap, 0, 0, width, height);
|
||||
bitmap.close();
|
||||
|
||||
// Start with max quality and only reduce if needed
|
||||
let quality = 1.0;
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', quality);
|
||||
const base64Data = dataUrl.split(',')[1];
|
||||
const binaryData = atob(base64Data);
|
||||
let bytes = new Uint8Array(binaryData.length);
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
bytes[i] = binaryData.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Only reduce quality if we exceed the target
|
||||
while (bytes.length > targetSize && quality > 0.1) {
|
||||
quality -= 0.05;
|
||||
const dataUrl = canvas.toDataURL('image/jpeg', quality);
|
||||
const base64Data = dataUrl.split(',')[1];
|
||||
const binaryData = atob(base64Data);
|
||||
bytes = new Uint8Array(binaryData.length);
|
||||
for (let i = 0; i < binaryData.length; i++) {
|
||||
bytes[i] = binaryData.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailFilename = this.getThumbnailFilename(file.path);
|
||||
const thumbnailPath = normalizePath(`${this.cacheDir}/${thumbnailFilename}`);
|
||||
|
||||
await this.ensureCacheDir();
|
||||
await this.app.vault.adapter.writeBinary(thumbnailPath, bytes.buffer);
|
||||
|
||||
return {
|
||||
thumbnailPath,
|
||||
size: bytes.length
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to generate thumbnail:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async generateThumbnail(coverPath: string, sourceFile: TFile): Promise<string | null> {
|
||||
const coverFile = this.app.metadataCache.getFirstLinkpathDest(coverPath, sourceFile.path);
|
||||
if (!coverFile) return null;
|
||||
|
||||
const result = await this.generateThumbnailFromFile(coverFile);
|
||||
if (!result) return null;
|
||||
|
||||
const cacheKey = coverFile.path;
|
||||
this.cache.entries[cacheKey] = {
|
||||
thumbnailPath: result.thumbnailPath,
|
||||
sourceModified: coverFile.stat.mtime,
|
||||
size: result.size
|
||||
};
|
||||
|
||||
await this.saveCache();
|
||||
|
||||
const arrayBuffer = await this.app.vault.adapter.readBinary(result.thumbnailPath);
|
||||
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
async markForGeneration(coverPath: string, sourceFile: TFile): Promise<void> {
|
||||
const coverFile = this.app.metadataCache.getFirstLinkpathDest(coverPath, sourceFile.path);
|
||||
if (!coverFile) return;
|
||||
|
||||
await this.loadCache();
|
||||
|
||||
const cacheKey = coverFile.path;
|
||||
if (!this.cache.pendingGeneration.includes(cacheKey) && !this.cache.entries[cacheKey]) {
|
||||
this.cache.pendingGeneration.push(cacheKey);
|
||||
await this.saveCache();
|
||||
}
|
||||
}
|
||||
|
||||
async generatePendingThumbnails(onProgress?: (current: number, total: number) => void): Promise<void> {
|
||||
if (this.generationInProgress || !this.plugin.settings.enableThumbnailCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadCache();
|
||||
|
||||
if (this.cache.pendingGeneration.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.generationInProgress = true;
|
||||
|
||||
const pending = [...this.cache.pendingGeneration];
|
||||
const total = pending.length;
|
||||
|
||||
for (let i = 0; i < pending.length; i++) {
|
||||
const imagePath = pending[i];
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(imagePath);
|
||||
if (file instanceof TFile) {
|
||||
const result = await this.generateThumbnailFromFile(file);
|
||||
if (result) {
|
||||
this.cache.entries[imagePath] = {
|
||||
thumbnailPath: result.thumbnailPath,
|
||||
sourceModified: file.stat.mtime,
|
||||
size: result.size
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const idx = this.cache.pendingGeneration.indexOf(imagePath);
|
||||
if (idx > -1) {
|
||||
this.cache.pendingGeneration.splice(idx, 1);
|
||||
}
|
||||
|
||||
await this.saveCache();
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(i + 1, total);
|
||||
}
|
||||
}
|
||||
|
||||
this.generationInProgress = false;
|
||||
}
|
||||
|
||||
async rebuildCache(onProgress?: (current: number, total: number) => void): Promise<void> {
|
||||
if (this.generationInProgress || !this.plugin.settings.enableThumbnailCache) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.loadCache();
|
||||
|
||||
const allImagePaths = new Set<string>([
|
||||
...Object.keys(this.cache.entries),
|
||||
...this.cache.pendingGeneration
|
||||
]);
|
||||
|
||||
if (allImagePaths.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.generationInProgress = true;
|
||||
|
||||
const imagePaths = Array.from(allImagePaths);
|
||||
const total = imagePaths.length;
|
||||
|
||||
for (let i = 0; i < imagePaths.length; i++) {
|
||||
const imagePath = imagePaths[i];
|
||||
|
||||
const file = this.app.vault.getAbstractFileByPath(imagePath);
|
||||
if (file instanceof TFile) {
|
||||
const oldEntry = this.cache.entries[imagePath];
|
||||
if (oldEntry) {
|
||||
try {
|
||||
const exists = await this.app.vault.adapter.exists(oldEntry.thumbnailPath);
|
||||
if (exists) {
|
||||
await this.app.vault.adapter.remove(oldEntry.thumbnailPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to remove old thumbnail ${oldEntry.thumbnailPath}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.generateThumbnailFromFile(file);
|
||||
if (result) {
|
||||
this.cache.entries[imagePath] = {
|
||||
thumbnailPath: result.thumbnailPath,
|
||||
sourceModified: file.stat.mtime,
|
||||
size: result.size
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const pendingIdx = this.cache.pendingGeneration.indexOf(imagePath);
|
||||
if (pendingIdx > -1) {
|
||||
this.cache.pendingGeneration.splice(pendingIdx, 1);
|
||||
}
|
||||
|
||||
await this.saveCache();
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(i + 1, total);
|
||||
}
|
||||
}
|
||||
|
||||
this.generationInProgress = false;
|
||||
}
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
await this.loadCache();
|
||||
|
||||
const thumbnailPaths = Object.values(this.cache.entries).map(entry => entry.thumbnailPath);
|
||||
|
||||
for (const path of thumbnailPaths) {
|
||||
try {
|
||||
const exists = await this.app.vault.adapter.exists(path);
|
||||
if (exists) {
|
||||
await this.app.vault.adapter.remove(path);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to remove thumbnail ${path}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
this.cache = {
|
||||
entries: {},
|
||||
pendingGeneration: []
|
||||
};
|
||||
await this.saveCache();
|
||||
}
|
||||
|
||||
getCacheStats(): { count: number; totalSize: number; pending: number } {
|
||||
const entries = Object.values(this.cache.entries);
|
||||
return {
|
||||
count: entries.length,
|
||||
totalSize: entries.reduce((sum, entry) => sum + entry.size, 0),
|
||||
pending: this.cache.pendingGeneration.length
|
||||
};
|
||||
}
|
||||
|
||||
isGenerating(): boolean {
|
||||
return this.generationInProgress;
|
||||
}
|
||||
}
|
||||
|
|
@ -146,12 +146,11 @@ export class MapBasesView extends BasesView {
|
|||
}
|
||||
|
||||
private async renderMap(): Promise<void> {
|
||||
// Need either coordinates property OR global lat/lng keys
|
||||
if (!this.data || (!this.coordinatesProp && (!this.plugin.settings.latKey || !this.plugin.settings.lngKey))) {
|
||||
if (!this.data) {
|
||||
this.containerEl.removeClass('is-loading');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const loadingOverlay = this.containerEl.createDiv({ cls: 'map-loading-overlay' });
|
||||
const spinner = loadingOverlay.createDiv();
|
||||
|
||||
|
|
@ -228,13 +227,13 @@ export class MapBasesView extends BasesView {
|
|||
app: this.app,
|
||||
settings: settings,
|
||||
tagSettings: tagSettings,
|
||||
thumbnailCache: this.plugin.thumbnailCache,
|
||||
options: {
|
||||
center: centerToUse,
|
||||
zoom: zoomToUse,
|
||||
height: height,
|
||||
markerType: this.markerType,
|
||||
tileLayer: this.tileLayer,
|
||||
showSearch: false,
|
||||
onTilesLoaded: () => {
|
||||
tilesLoaded = true;
|
||||
hideOverlay();
|
||||
|
|
@ -320,13 +319,17 @@ export class MapBasesView extends BasesView {
|
|||
const coverVal = entry.getValue(this.coverProp);
|
||||
if (coverVal) {
|
||||
point.cover = coverVal.toString();
|
||||
|
||||
if (this.plugin.settings.enableThumbnailCache) {
|
||||
this.plugin.thumbnailCache.markForGeneration(point.cover, entry.file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const properties: Array<{ name: string; value: string }> = [];
|
||||
if (this.data.properties) {
|
||||
for (const prop of this.data.properties.slice(0, 20)) {
|
||||
if (prop === this.coordinatesProp || prop === this.coverProp) continue;
|
||||
if (prop === this.coordinatesProp) continue;
|
||||
|
||||
try {
|
||||
const value = entry.getValue(prop);
|
||||
|
|
|
|||
26
styles.css
26
styles.css
|
|
@ -69,22 +69,25 @@
|
|||
position: absolute;
|
||||
left: var(--tooltip-x, 0);
|
||||
top: var(--tooltip-y, 0);
|
||||
padding: 8px 12px;
|
||||
padding: 10px 14px;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
z-index: 1000;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
max-width: 250px;
|
||||
font-size: 13px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
|
||||
width: max-content;
|
||||
max-width: 400px;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
.map-tooltip-property {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.map-tooltip-tags {
|
||||
|
|
@ -97,12 +100,15 @@
|
|||
.map-tooltip-property-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.map-tooltip-property-value {
|
||||
font-size: 12px;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.map-tooltip-cover-image {
|
||||
|
|
@ -115,7 +121,9 @@
|
|||
}
|
||||
|
||||
.map-tooltip-title {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.map-tooltip-property-container {
|
||||
|
|
|
|||
Loading…
Reference in a new issue