feat: improved property css; rebuild cache setting

This commit is contained in:
ccmdi 2025-10-15 19:08:37 -04:00
parent 04cab36ad1
commit 640ce208e4
No known key found for this signature in database
4 changed files with 127 additions and 89 deletions

View file

@ -19,13 +19,10 @@ export default class MapPlugin extends Plugin {
if (this.settings.enableThumbnailCache) {
await this.thumbnailCache.loadCache();
const stats = this.thumbnailCache.getCacheStats();
if (stats.pending > 0) {
setTimeout(() => {
this.thumbnailCache.generatePendingThumbnails();
}, 2000);
}
setTimeout(() => {
this.thumbnailCache.generatePendingThumbnails();
}, 2000);
}
this.registerBasesView('map', {

View file

@ -10,6 +10,7 @@ export interface MapPluginSettings {
autoCenter: boolean;
transitionDuration: number;
enableThumbnailCache: boolean;
thumbnailTargetSize: number;
tagSettings: MapTagSettings;
}
@ -21,6 +22,7 @@ export const DEFAULT_SETTINGS: MapPluginSettings = {
autoCenter: true,
transitionDuration: 1000,
enableThumbnailCache: false,
thumbnailTargetSize: 25,
tagSettings: DEFAULT_MAP_TAG_SETTINGS
};
@ -119,27 +121,40 @@ export class MapSettingTab extends PluginSettingTab {
}));
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 cacheInfo = new Setting(containerEl)
.setName('Thumbnail cache status')
.setDesc(`${stats.count} thumbnails cached (${sizeKB} KB)`);
if (stats.pending > 0) {
cacheInfo.setDesc(`${stats.count} thumbnails cached (${sizeKB} KB), ${stats.pending} pending generation`);
if (stats.pending > 0 || isGenerating) {
cacheInfo.setDesc(`${stats.count} thumbnails cached (${sizeKB} KB), ${stats.pending} pending`);
} else if (stats.count > 0) {
cacheInfo.addButton(button => button
.setButtonText('Generate thumbnails')
.setButtonText('Refresh cache')
.onClick(async () => {
button.setDisabled(true);
button.setButtonText('Generating...');
button.setButtonText('Refreshing...');
await this.plugin.thumbnailCache.generatePendingThumbnails((current, total) => {
button.setButtonText(`Generating ${current}/${total}...`);
await this.plugin.thumbnailCache.rebuildCache((current, total) => {
button.setButtonText(`Refreshing ${current}/${total}...`);
});
button.setButtonText('Generate thumbnails');
button.setButtonText('Refresh cache');
button.setDisabled(false);
this.display();
}));

View file

@ -12,7 +12,6 @@ export interface ThumbnailCacheMetadata {
pendingGeneration: string[];
}
const THUMBNAIL_MAX_SIZE = 25000; // 25kb target
const THUMBNAIL_WIDTH = 300;
const THUMBNAIL_HEIGHT = 200;
const CACHE_DIR = '.obsidian/plugins/mapplus/.cache';
@ -123,12 +122,10 @@ export class ThumbnailCacheManager {
return null;
}
async generateThumbnail(coverPath: string, sourceFile: TFile): Promise<string | null> {
const coverFile = this.app.metadataCache.getFirstLinkpathDest(coverPath, sourceFile.path);
if (!coverFile) return null;
private async generateThumbnailFromFile(file: TFile): Promise<{ thumbnailPath: string; size: number } | null> {
try {
const arrayBuffer = await this.app.vault.readBinary(coverFile);
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);
@ -155,7 +152,7 @@ export class ThumbnailCacheManager {
let quality = 0.7;
let dataUrl = canvas.toDataURL('image/jpeg', quality);
while (dataUrl.length > THUMBNAIL_MAX_SIZE && quality > 0.1) {
while (dataUrl.length > targetSize && quality > 0.1) {
quality -= 0.1;
dataUrl = canvas.toDataURL('image/jpeg', quality);
}
@ -167,29 +164,43 @@ export class ThumbnailCacheManager {
bytes[i] = binaryData.charCodeAt(i);
}
const thumbnailFilename = this.getThumbnailFilename(coverFile.path);
const thumbnailFilename = this.getThumbnailFilename(file.path);
const thumbnailPath = normalizePath(`${this.cacheDir}/${thumbnailFilename}`);
await this.ensureCacheDir();
await this.app.vault.adapter.writeBinary(thumbnailPath, bytes.buffer);
const cacheKey = coverFile.path;
this.cache.entries[cacheKey] = {
return {
thumbnailPath,
sourceModified: coverFile.stat.mtime,
size: bytes.length
};
await this.saveCache();
const resultBlob = new Blob([bytes], { type: 'image/jpeg' });
return URL.createObjectURL(resultBlob);
} 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;
@ -224,59 +235,13 @@ export class ThumbnailCacheManager {
const file = this.app.vault.getAbstractFileByPath(imagePath);
if (file instanceof TFile) {
try {
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) continue;
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();
let quality = 0.7;
let dataUrl = canvas.toDataURL('image/jpeg', quality);
while (dataUrl.length > THUMBNAIL_MAX_SIZE && quality > 0.1) {
quality -= 0.1;
dataUrl = canvas.toDataURL('image/jpeg', quality);
}
const base64Data = dataUrl.split(',')[1];
const binaryData = atob(base64Data);
const bytes = new Uint8Array(binaryData.length);
for (let j = 0; j < binaryData.length; j++) {
bytes[j] = binaryData.charCodeAt(j);
}
const thumbnailFilename = this.getThumbnailFilename(imagePath);
const thumbnailPath = normalizePath(`${this.cacheDir}/${thumbnailFilename}`);
await this.ensureCacheDir();
await this.app.vault.adapter.writeBinary(thumbnailPath, bytes.buffer);
const result = await this.generateThumbnailFromFile(file);
if (result) {
this.cache.entries[imagePath] = {
thumbnailPath,
thumbnailPath: result.thumbnailPath,
sourceModified: file.stat.mtime,
size: bytes.length
size: result.size
};
} catch (e) {
console.error(`Failed to generate thumbnail for ${imagePath}:`, e);
}
}
@ -295,6 +260,59 @@ export class ThumbnailCacheManager {
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 = Object.keys(this.cache.entries);
if (allImagePaths.length === 0) {
return;
}
this.generationInProgress = true;
const total = allImagePaths.length;
for (let i = 0; i < allImagePaths.length; i++) {
const imagePath = allImagePaths[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
};
}
}
await this.saveCache();
if (onProgress) {
onProgress(i + 1, total);
}
}
this.generationInProgress = false;
}
async clearCache(): Promise<void> {
await this.loadCache();

View file

@ -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 {