mirror of
https://github.com/ccmdi/obsidian-map-plus.git
synced 2026-07-22 06:43:14 +00:00
feat: improved property css; rebuild cache setting
This commit is contained in:
parent
04cab36ad1
commit
640ce208e4
4 changed files with 127 additions and 89 deletions
|
|
@ -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', {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
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