Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
**/out/
src/test/samples/env/
src/test/samples/folder*
.vscode-test-web/
.vscode-test-web/
.history
.github
.vscode-test
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
"watch-tests": "tsc -p . -w --outDir dist",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
"test": "node ./dist/test/runTest.js"
},
"devDependencies": {
"@types/glob": "^7.2.0",
Expand Down
3 changes: 3 additions & 0 deletions src/custom_typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ declare module 'custom_typings' {
mtime: number,
ctime: number,
status: "" | "refresh",
loaded?: boolean, // Whether metadata has been loaded
};

export type TFolder = {
Expand All @@ -17,5 +18,7 @@ declare module 'custom_typings' {
images: {
[imageId: string]: TImage,
},
imageCount?: number, // Quick count without loading metadata
loaded?: boolean, // Whether metadata has been loaded for this folder
};
}
105 changes: 90 additions & 15 deletions src/gallery/gallery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export function deactivate() {
class GalleryWebview {
private gFolders: Record<string, TFolder> = {};
private customSorter: CustomSorter = new CustomSorter();
private metadataLoadingInProgress: boolean = false;
private metadataProgress: { loaded: number; total: number } = { loaded: 0, total: 0 };

constructor(private readonly context: vscode.ExtensionContext) { }

Expand Down Expand Up @@ -70,24 +72,65 @@ class GalleryWebview {
);

const htmlProvider = new HTMLProvider(this.context, panel.webview);

// Fast initial load - just folder structure
const imageUris = await this.getImageUris(galleryFolder);
this.gFolders = await utils.getFolders(imageUris);
this.gFolders = await utils.getFoldersStructure(imageUris);
this.gFolders = this.customSorter.sort(this.gFolders);
panel.webview.html = htmlProvider.fullHTML();

const imageSizeStat = utils.getImageSizeStat(this.gFolders);
// Send quick telemetry for initial load
const totalImageCount = Object.values(this.gFolders).reduce((sum, folder) => sum + (folder.imageCount || 0), 0);
reporter.sendTelemetryEvent('gallery.createPanel', {}, {
"duration": Date.now() - startTime,
"folderCount": Object.keys(this.gFolders).length,
"imageCount": imageSizeStat.count,
"imageSizeMean": imageSizeStat.mean,
"imageSizeStd": imageSizeStat.std,
"imageCount": totalImageCount,
"imageSizeMean": 0, // Will be calculated when metadata loads
"imageSizeStd": 0,
});


return panel;
}

public messageListener(message: Record<string, any>, webview: vscode.Webview) {
private async loadMetadataForSort(folders: TFolder[], sortBy: "name" | "ext" | "size" | "ctime" | "mtime", ascending: boolean, webview: vscode.Webview) {
if (this.metadataLoadingInProgress) { return; }

this.metadataLoadingInProgress = true;
let loaded = 0;
const total = folders.length;

// Load folders one by one to show progress
for (const folder of folders) {
try {
this.gFolders[folder.id] = await utils.loadFolderMetadata(folder);
loaded++;

// Send progress update
webview.postMessage({
command: "POST.gallery.metadataProgress",
progress: { loaded, total }
});
} catch (error) {
console.error(`Failed to load metadata for folder ${folder.id}:`, error);
loaded++;
}
}

this.metadataLoadingInProgress = false;

// Now sort with all metadata loaded
this.gFolders = this.customSorter.sort(this.gFolders, sortBy, ascending);

// Send completion and updated content
webview.postMessage({
command: "POST.gallery.metadataComplete"
});

this.messageListener({ command: "POST.gallery.requestContentDOMs" }, webview).catch(console.error);
}

public async messageListener(message: Record<string, any>, webview: vscode.Webview) {
const telemetryPrefix = "gallery.messageListener";
switch (message.command) {
case "POST.gallery.openImageViewer":
Expand All @@ -106,26 +149,58 @@ class GalleryWebview {
break;

case "POST.gallery.requestSort":
this.gFolders = this.customSorter.sort(this.gFolders, message.valueName, message.ascending);
const needsMetadata = ["size", "ctime", "mtime"].includes(message.valueName);
const unloadedFolders = Object.values(this.gFolders).filter(folder => !folder.loaded);

if (needsMetadata && unloadedFolders.length > 0) {
// Show busy indicator
webview.postMessage({
command: "POST.gallery.sortBusy",
sortType: message.valueName,
progress: { loaded: 0, total: unloadedFolders.length }
});

// Load metadata for unloaded folders only
this.loadMetadataForSort(unloadedFolders, message.valueName, message.ascending, webview);
} else {
// Sort immediately
this.gFolders = this.customSorter.sort(this.gFolders, message.valueName, message.ascending);
}

reporter.sendTelemetryEvent(`${telemetryPrefix}.requestSort`, {
'valueName': this.customSorter.valueName,
'ascending': this.customSorter.ascending.toString(),
});
// DO NOT BREAK HERE; FALL THROUGH TO UPDATE DOMS

case "POST.gallery.loadFolderMetadata":
const folderId = message.folderId;
if (this.gFolders[folderId] && !this.gFolders[folderId].loaded) {
try {
// Load metadata for this specific folder
this.gFolders[folderId] = await utils.loadFolderMetadata(this.gFolders[folderId]);

// Send updated content for this folder
this.messageListener({ command: "POST.gallery.requestContentDOMs" }, webview).catch(console.error);
} catch (error) {
console.error(`Failed to load metadata for folder ${folderId}:`, error);
}
}
break;

case "POST.gallery.requestContentDOMs":
const htmlProvider = new HTMLProvider(this.context, webview);
const htmlProvider2 = new HTMLProvider(this.context, webview);
const response: Record<string, any> = {};
for (const [_idx, folder] of Object.values(this.gFolders).entries()) {
response[folder.id] = {
status: "",
barHtml: htmlProvider.folderBarHTML(folder),
gridHtml: htmlProvider.imageGridHTML(folder, true),
status: folder.loaded ? "loaded" : "structure",
barHtml: htmlProvider2.folderBarHTML(folder),
gridHtml: htmlProvider2.imageGridHTML(folder, true),
images: Object.fromEntries(
Object.values(folder.images).map(
image => [image.id, {
status: image.status,
containerHtml: htmlProvider.singleImageHTML(image),
containerHtml: htmlProvider2.singleImageHTML(image),
}]
)
),
Expand Down Expand Up @@ -169,7 +244,7 @@ class GalleryWebview {
} else {
this.gFolders[folder.id] = folder;
}
this.messageListener({ command: "POST.gallery.requestSort" }, webview);
this.messageListener({ command: "POST.gallery.requestSort" }, webview).catch(console.error);
reporter.sendTelemetryEvent(`${telemetryPrefix}.didCreate`, {}, getMeasurementProperties(folders));
});
watcher.onDidDelete(async uri => {
Expand All @@ -184,7 +259,7 @@ class GalleryWebview {
delete this.gFolders[folder.id];
}
}
this.messageListener({ command: "POST.gallery.requestSort" }, webview);
this.messageListener({ command: "POST.gallery.requestSort" }, webview).catch(console.error);
reporter.sendTelemetryEvent(`${telemetryPrefix}.didDelete`, {}, getMeasurementProperties(folders));
});
watcher.onDidChange(async uri => {
Expand All @@ -196,7 +271,7 @@ class GalleryWebview {
if (this.gFolders.hasOwnProperty(folder.id) && this.gFolders[folder.id].images.hasOwnProperty(image.id)) {
image.status = "refresh";
this.gFolders[folder.id].images[image.id] = image;
this.messageListener({ command: "POST.gallery.requestSort" }, webview);
this.messageListener({ command: "POST.gallery.requestSort" }, webview).catch(console.error);
this.gFolders[folder.id].images[image.id].status = "";
}
reporter.sendTelemetryEvent(`${telemetryPrefix}.didChange`, {}, getMeasurementProperties(folders));
Expand Down
144 changes: 144 additions & 0 deletions src/gallery/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ function init() {
initMessageListeners();
DOMManager.requestContentDOMs();
EventListener.addAllToToolbar();
// Initialize thumbnail size
EventListener.updateThumbnailSize(250);
}

function initMessageListeners() {
Expand All @@ -29,6 +31,15 @@ function initMessageListeners() {
DOMManager.updateGlobalDoms(message);
DOMManager.updateGalleryContent();
break;
case "POST.gallery.metadataProgress":
ProgressManager.updateProgress(message);
break;
case "POST.gallery.metadataComplete":
ProgressManager.hideProgress();
break;
case "POST.gallery.sortBusy":
ProgressManager.showSortBusy(message);
break;
}
});
}
Expand Down Expand Up @@ -144,6 +155,71 @@ class DOMManager {
content.innerHTML = "<p>No image found in this folder.</p>";
}
}

}

class ProgressManager {
static updateProgress(message) {
const progress = message.progress;
let progressBar = document.querySelector('.progress-bar');

if (!progressBar) {
// Create progress bar
const toolbar = document.querySelector('.toolbar');
const progressContainer = document.createElement('div');
progressContainer.className = 'progress-container';
progressContainer.innerHTML = `
<div class="progress-text">Loading metadata...</div>
<div class="progress-bar-container">
<div class="progress-bar"></div>
</div>
`;
toolbar.appendChild(progressContainer);
progressBar = progressContainer.querySelector('.progress-bar');
}

const percentage = Math.round((progress.loaded / progress.total) * 100);
progressBar.style.width = percentage + '%';

const progressText = document.querySelector('.progress-text');
if (progressText) {
progressText.textContent = `Loading metadata... ${progress.loaded}/${progress.total} (${percentage}%)`;
}
}

static hideProgress() {
const progressContainer = document.querySelector('.progress-container');
if (progressContainer) {
progressContainer.remove();
}
ProgressManager.hideSortBusy();
}

static showSortBusy(message) {
const dropdown = document.querySelector('.toolbar .dropdown');
const sortArrow = document.querySelector('.toolbar .sort-order-arrow');

// Disable sort controls
dropdown.disabled = true;
sortArrow.style.opacity = '0.5';
sortArrow.style.pointerEvents = 'none';

// Show busy indicator
const progressText = document.querySelector('.progress-text');
if (progressText) {
progressText.textContent = `Sorting by ${message.sortType}... ${message.progress.loaded}/${message.progress.total} loaded`;
}
}

static hideSortBusy() {
const dropdown = document.querySelector('.toolbar .dropdown');
const sortArrow = document.querySelector('.toolbar .sort-order-arrow');

// Re-enable sort controls
dropdown.disabled = false;
sortArrow.style.opacity = '1';
sortArrow.style.pointerEvents = 'auto';
}
}

class EventListener {
Expand All @@ -163,6 +239,18 @@ class EventListener {
EventListener.sortRequest();
}
);
const thumbnailSlider = document.querySelector("#thumbnail-size-slider");
if (thumbnailSlider) {
thumbnailSlider.addEventListener(
"input", (event) => EventListener.updateThumbnailSize(event.target.value)
);
}
const searchInput = document.querySelector("#search-input");
if (searchInput) {
searchInput.addEventListener(
"input", (event) => EventListener.filterImages(event.target.value)
);
}
}

static addToFolderBar(folderBar) {
Expand Down Expand Up @@ -257,6 +345,20 @@ class EventListener {

static expandFolderBar(folderDOM) {
const elements = EventListener.getFolderAssociatedElements(folderDOM);

// Load metadata for this folder if not loaded
if (folderDOM.dataset.loaded === "false") {
const countElement = folderDOM.querySelector(`#${folderDOM.id}-items-count`);
if (countElement) {
countElement.textContent = "⏳ Loading...";
}

vscode.postMessage({
command: "POST.gallery.loadFolderMetadata",
folderId: folderDOM.id
});
}

if (elements.arrowImg.src.includes("chevron-right.svg")) {
elements.arrowImg.src = elements.arrowImg.dataset.chevronDown;
}
Expand Down Expand Up @@ -304,6 +406,48 @@ class EventListener {
ascending: sortOrderDOM.src.includes("arrow-up.svg") ? true : false,
});
}

static updateThumbnailSize(size) {
document.documentElement.style.setProperty('--thumbnail-size', size + 'px');
}

static filterImages(searchTerm) {
const term = searchTerm.toLowerCase().trim();

// Get all image containers
Object.values(gFolders).forEach(folder => {
let visibleCount = 0;

Object.values(folder.images).forEach(image => {
const container = image.container;
const filename = container.querySelector('.filename').textContent.toLowerCase();

if (term === '' || filename.includes(term)) {
container.style.display = '';
visibleCount++;
} else {
container.style.display = 'none';
}
});

// Update folder visibility and count
const folderGrid = folder.grid;
if (visibleCount === 0 && term !== '') {
folderGrid.style.display = 'none';
folder.bar.style.display = 'none';
} else {
folderGrid.style.display = 'grid';
folder.bar.style.display = 'flex';
}

// Update the image count in folder bar
const countText = (object, count) => `${count} ${object}${count === 1 ? "" : "s"} found`;
const countElement = folder.bar.querySelector(`#${folder.bar.id}-items-count`);
if (countElement) {
countElement.textContent = countText("image", visibleCount);
}
});
}
}

(function () {
Expand Down
Loading