feat: thumbnails added
This commit is contained in:
+76
-2
@@ -47,6 +47,7 @@ let batchMoveState = {
|
||||
let settingsState = {
|
||||
activeTab: "general",
|
||||
logsLoaded: false,
|
||||
showThumbnails: false,
|
||||
};
|
||||
let searchState = {
|
||||
pane: "left",
|
||||
@@ -200,6 +201,7 @@ function settingsElements() {
|
||||
generalTab: document.getElementById("settings-general-tab"),
|
||||
logsTab: document.getElementById("settings-logs-tab"),
|
||||
generalPanel: document.getElementById("settings-general-panel"),
|
||||
showThumbnailsInput: document.getElementById("settings-show-thumbnails"),
|
||||
logsPanel: document.getElementById("settings-logs-panel"),
|
||||
logsList: document.getElementById("settings-logs-list"),
|
||||
logsError: document.getElementById("settings-logs-error"),
|
||||
@@ -357,6 +359,58 @@ function currentRowItem(pane) {
|
||||
return model.visibleItems[model.currentRowIndex];
|
||||
}
|
||||
|
||||
function thumbnailsEnabled() {
|
||||
return !!settingsState.showThumbnails;
|
||||
}
|
||||
|
||||
function isThumbnailCandidate(entry) {
|
||||
if (!entry || entry.kind !== "file") {
|
||||
return false;
|
||||
}
|
||||
const lower = (entry.name || "").toLowerCase();
|
||||
return lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".png") || lower.endsWith(".webp");
|
||||
}
|
||||
|
||||
function createMediaSlot(entry) {
|
||||
const slot = document.createElement("span");
|
||||
slot.className = "entry-media-slot";
|
||||
if (thumbnailsEnabled() && isThumbnailCandidate(entry)) {
|
||||
const image = document.createElement("img");
|
||||
image.className = "entry-thumbnail";
|
||||
image.loading = "lazy";
|
||||
image.alt = "";
|
||||
image.src = `/api/files/thumbnail?${new URLSearchParams({ path: entry.path }).toString()}`;
|
||||
slot.append(image);
|
||||
return slot;
|
||||
}
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = `entry-media-icon ${entry.kind === "directory" ? "folder" : "file"}`;
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
slot.append(icon);
|
||||
return slot;
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const data = await apiRequest("GET", "/api/settings");
|
||||
settingsState.showThumbnails = !!data.show_thumbnails;
|
||||
const elements = settingsElements();
|
||||
if (elements.showThumbnailsInput) {
|
||||
elements.showThumbnailsInput.checked = settingsState.showThumbnails;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(update) {
|
||||
const data = await apiRequest("POST", "/api/settings", update);
|
||||
settingsState.showThumbnails = !!data.show_thumbnails;
|
||||
const elements = settingsElements();
|
||||
if (elements.showThumbnailsInput) {
|
||||
elements.showThumbnailsInput.checked = settingsState.showThumbnails;
|
||||
}
|
||||
renderPaneItems("left");
|
||||
renderPaneItems("right");
|
||||
}
|
||||
|
||||
function updateActionButtons() {
|
||||
const selectedItems = activePaneState().selectedItems;
|
||||
const count = selectedItems.length;
|
||||
@@ -547,6 +601,7 @@ function createBrowseItem(pane, entry, kind) {
|
||||
|
||||
const name = document.createElement("span");
|
||||
name.className = `entry-name ${kind === "directory" ? "entry-dir" : "entry-file"}`;
|
||||
name.append(createMediaSlot({ ...entry, kind }));
|
||||
|
||||
if (kind === "directory") {
|
||||
const open = document.createElement("button");
|
||||
@@ -558,10 +613,12 @@ function createBrowseItem(pane, entry, kind) {
|
||||
setActivePane(pane);
|
||||
navigateTo(pane, entry.path);
|
||||
};
|
||||
open.classList.add("entry-label");
|
||||
name.append(open);
|
||||
} else {
|
||||
const fileName = document.createElement("span");
|
||||
fileName.textContent = entry.name;
|
||||
fileName.className = "entry-label";
|
||||
fileName.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
setActivePane(pane);
|
||||
@@ -626,6 +683,11 @@ function renderPaneItems(pane) {
|
||||
renderPaneItems(pane);
|
||||
};
|
||||
up.append(document.createElement("span"));
|
||||
const upNameCell = document.createElement("span");
|
||||
upNameCell.className = "entry-name entry-dir";
|
||||
const upMedia = document.createElement("span");
|
||||
upMedia.className = "entry-media-slot";
|
||||
upNameCell.append(upMedia);
|
||||
const upName = document.createElement("button");
|
||||
upName.type = "button";
|
||||
upName.className = "dir-link";
|
||||
@@ -635,8 +697,7 @@ function renderPaneItems(pane) {
|
||||
setActivePane(pane);
|
||||
navigateTo(pane, entry.path);
|
||||
};
|
||||
const upNameCell = document.createElement("span");
|
||||
upNameCell.className = "entry-name entry-dir";
|
||||
upName.classList.add("entry-label");
|
||||
upNameCell.append(upName);
|
||||
up.append(upNameCell);
|
||||
const upSize = document.createElement("span");
|
||||
@@ -1630,6 +1691,17 @@ async function loadHistoryForSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleShowThumbnailsChange(event) {
|
||||
const input = event.target;
|
||||
try {
|
||||
await saveSettings({ show_thumbnails: !!input.checked });
|
||||
} catch (err) {
|
||||
input.checked = settingsState.showThumbnails;
|
||||
settingsElements().logsError.textContent = "";
|
||||
setError("actions-error", `Settings: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
settingsElements().overlay.classList.add("hidden");
|
||||
}
|
||||
@@ -2097,6 +2169,7 @@ function setupEvents() {
|
||||
setSettingsTab("logs");
|
||||
await loadHistoryForSettings();
|
||||
};
|
||||
settings.showThumbnailsInput.onchange = handleShowThumbnailsChange;
|
||||
settings.overlay.onclick = (event) => {
|
||||
if (event.target === settings.overlay) {
|
||||
closeSettings();
|
||||
@@ -2204,6 +2277,7 @@ async function init() {
|
||||
applyTheme(preferredTheme());
|
||||
setActivePane("left");
|
||||
setupEvents();
|
||||
await loadSettings();
|
||||
await Promise.all([loadBrowsePane("left"), loadBrowsePane("right")]);
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,11 @@
|
||||
</div>
|
||||
<section id="settings-general-panel" class="settings-panel" role="tabpanel" aria-labelledby="settings-general-tab">
|
||||
<div class="settings-placeholder-title">General</div>
|
||||
<div class="popup-meta">Future application settings will appear here.</div>
|
||||
<label class="settings-option" for="settings-show-thumbnails">
|
||||
<input id="settings-show-thumbnails" type="checkbox">
|
||||
<span>Show thumbnails</span>
|
||||
</label>
|
||||
<div class="popup-meta">Image thumbnails are available for jpg, jpeg, png and webp files.</div>
|
||||
</section>
|
||||
<section id="settings-logs-panel" class="settings-panel hidden" role="tabpanel" aria-labelledby="settings-logs-tab">
|
||||
<div id="settings-logs-error" class="error"></div>
|
||||
|
||||
@@ -318,6 +318,104 @@ button:disabled {
|
||||
background: var(--color-list-row-hover);
|
||||
}
|
||||
|
||||
.entry-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.entry-media-slot {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-button-secondary-bg);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entry-media-slot img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.entry-media-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.entry-media-icon.folder::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
top: 4px;
|
||||
width: 12px;
|
||||
height: 8px;
|
||||
border: 1.5px solid var(--color-text-muted);
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.entry-media-icon.folder::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: 5px;
|
||||
height: 3px;
|
||||
border: 1.5px solid var(--color-text-muted);
|
||||
border-bottom: 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.entry-media-icon.file::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 1px;
|
||||
width: 9px;
|
||||
height: 12px;
|
||||
border: 1.5px solid var(--color-text-muted);
|
||||
border-radius: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.entry-media-icon.file::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 1px;
|
||||
top: 1px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: var(--color-button-secondary-bg);
|
||||
border-top: 1.5px solid var(--color-text-muted);
|
||||
border-right: 1.5px solid var(--color-text-muted);
|
||||
transform: skew(-12deg, -12deg);
|
||||
}
|
||||
|
||||
.entry-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.settings-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 10px 0 8px;
|
||||
}
|
||||
|
||||
.list li.is-selected {
|
||||
background: var(--color-selection-bg);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user