feat(ui): limit visible reactions and overhaul retro theme
- Limit the number of initially visible reactions per file to 2 and calculate the overflow count on the backend. - Redesign the retro theme CSS to mimic a classic Windows 98 Explorer window, including title bars, toolbars, and sunken panes. - Add local storage persistence for the file browser view preference (list vs. thumbnails).
This commit is contained in:
@@ -1,30 +1,25 @@
|
||||
(function () {
|
||||
const fileBrowser = document.querySelector("[data-file-browser]");
|
||||
const viewButtons = document.querySelectorAll("[data-view-button]");
|
||||
const previewImages = document.querySelector("[data-preview-images]");
|
||||
const previewActions = document.querySelectorAll("[data-preview-action]");
|
||||
const fileContextMenu = document.querySelector("[data-file-context-menu]");
|
||||
const fileBrowserWindow = document.querySelector("[data-file-browser-window]");
|
||||
|
||||
let ctrlCopyMode = false;
|
||||
let contextFile = null;
|
||||
const contextMenuCloseDistance = 80;
|
||||
const viewStorageKey = "warpbox.fileBrowser.view";
|
||||
|
||||
if (fileBrowser) {
|
||||
applySavedFileBrowserPreferences();
|
||||
|
||||
viewButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const view = button.getAttribute("data-view-button");
|
||||
fileBrowser.classList.toggle("is-list", view === "list");
|
||||
fileBrowser.classList.toggle("is-thumbs", view === "thumbs");
|
||||
viewButtons.forEach((item) => item.classList.toggle("is-active", item === button));
|
||||
setFileBrowserView(view);
|
||||
savePreference(viewStorageKey, view);
|
||||
});
|
||||
});
|
||||
|
||||
if (previewImages) {
|
||||
previewImages.addEventListener("click", () => {
|
||||
fileBrowser.classList.toggle("images-only");
|
||||
previewImages.classList.toggle("is-active");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (fileBrowser && fileContextMenu) {
|
||||
@@ -188,4 +183,40 @@
|
||||
y >= rect.top - contextMenuCloseDistance &&
|
||||
y <= rect.bottom + contextMenuCloseDistance;
|
||||
}
|
||||
|
||||
function applySavedFileBrowserPreferences() {
|
||||
const savedView = readPreference(viewStorageKey);
|
||||
setFileBrowserView(savedView === "list" ? "list" : "thumbs");
|
||||
}
|
||||
|
||||
function setFileBrowserView(view) {
|
||||
const normalized = view === "thumbs" ? "thumbs" : "list";
|
||||
fileBrowser.classList.toggle("is-list", normalized === "list");
|
||||
fileBrowser.classList.toggle("is-thumbs", normalized === "thumbs");
|
||||
if (fileBrowserWindow) {
|
||||
fileBrowserWindow.classList.toggle("is-list-view", normalized === "list");
|
||||
fileBrowserWindow.classList.toggle("is-icon-view", normalized === "thumbs");
|
||||
}
|
||||
viewButtons.forEach((item) => {
|
||||
const active = item.getAttribute("data-view-button") === normalized;
|
||||
item.classList.toggle("is-active", active);
|
||||
item.setAttribute("aria-pressed", active ? "true" : "false");
|
||||
});
|
||||
}
|
||||
|
||||
function readPreference(key) {
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function savePreference(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch (_) {
|
||||
// LocalStorage can be unavailable in private or locked-down browsers.
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
const panel = picker ? picker.querySelector(".reaction-picker-panel") : null;
|
||||
const search = picker ? picker.querySelector("[data-reaction-search]") : null;
|
||||
const closeButton = picker ? picker.querySelector("[data-reaction-close]") : null;
|
||||
const existingSection = picker ? picker.querySelector("[data-reaction-existing]") : null;
|
||||
const existingList = picker ? picker.querySelector("[data-reaction-existing-list]") : null;
|
||||
const readonlyNote = picker ? picker.querySelector("[data-reaction-readonly]") : null;
|
||||
const chooserElements = picker ? Array.from(picker.querySelectorAll(".reaction-picker-tabs, .reaction-search, .reaction-grid-wrap")) : [];
|
||||
const tabs = picker ? Array.from(picker.querySelectorAll("[data-reaction-tab]")) : [];
|
||||
const panels = picker ? Array.from(picker.querySelectorAll("[data-reaction-panel]")) : [];
|
||||
|
||||
@@ -17,6 +21,36 @@
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const pill = event.target.closest("[data-reaction-pill]");
|
||||
if (pill) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = pill.closest("[data-reaction-card]") || activeCard;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
if (card.dataset.reacted === "true") {
|
||||
openPickerForCard(card, pill);
|
||||
return;
|
||||
}
|
||||
submitReactionForCard(card, pill.dataset.reactionEmojiId);
|
||||
return;
|
||||
}
|
||||
|
||||
const more = event.target.closest("[data-reaction-more]");
|
||||
if (!more) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const card = more.closest("[data-reaction-card]");
|
||||
if (card) {
|
||||
openPickerForCard(card, more);
|
||||
}
|
||||
});
|
||||
|
||||
if (!picker || !panel) {
|
||||
return;
|
||||
}
|
||||
@@ -35,10 +69,10 @@
|
||||
|
||||
panel.addEventListener("click", async (event) => {
|
||||
const emoji = event.target.closest("[data-emoji-id]");
|
||||
if (!emoji || !activeButton || !activeCard) {
|
||||
if (!emoji || !activeCard || activeCard.dataset.reacted === "true") {
|
||||
return;
|
||||
}
|
||||
await submitReaction(emoji);
|
||||
await submitReactionForCard(activeCard, emoji.dataset.emojiId);
|
||||
});
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
@@ -62,6 +96,9 @@
|
||||
if (panel.contains(event.target) || event.target.closest("[data-reaction-button]")) {
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-reaction-more]") || event.target.closest("[data-reaction-pill]")) {
|
||||
return;
|
||||
}
|
||||
closePicker();
|
||||
});
|
||||
|
||||
@@ -78,15 +115,24 @@
|
||||
});
|
||||
|
||||
function openPicker(button) {
|
||||
activeButton = button;
|
||||
activeCard = button.closest("[data-reaction-card]");
|
||||
openPickerForCard(button.closest("[data-reaction-card]"), button);
|
||||
}
|
||||
|
||||
function openPickerForCard(card, trigger) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
activeButton = trigger || card.querySelector("[data-reaction-button]");
|
||||
activeCard = card;
|
||||
populateExistingReactions(card);
|
||||
setPickerReadonly(card.dataset.reacted === "true");
|
||||
picker.hidden = false;
|
||||
picker.classList.add("is-open");
|
||||
if (search) {
|
||||
search.value = "";
|
||||
filterEmoji("");
|
||||
}
|
||||
positionPicker(button);
|
||||
positionPicker(activeButton || card);
|
||||
}
|
||||
|
||||
function closePicker() {
|
||||
@@ -95,6 +141,7 @@
|
||||
document.documentElement.classList.remove("reaction-picker-open");
|
||||
picker.style.left = "";
|
||||
picker.style.top = "";
|
||||
setPickerReadonly(false);
|
||||
activeButton = null;
|
||||
activeCard = null;
|
||||
}
|
||||
@@ -146,12 +193,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
async function submitReaction(emoji) {
|
||||
async function submitReactionForCard(card, emojiID) {
|
||||
if (!card || !emojiID || card.dataset.reacted === "true") {
|
||||
return;
|
||||
}
|
||||
const body = new URLSearchParams();
|
||||
body.set("emoji_id", emoji.dataset.emojiId);
|
||||
body.set("emoji_id", emojiID);
|
||||
|
||||
activeButton.disabled = true;
|
||||
const response = await fetch(activeButton.dataset.reactUrl, {
|
||||
const reactButton = card.querySelector("[data-reaction-button]");
|
||||
if (reactButton) {
|
||||
reactButton.disabled = true;
|
||||
}
|
||||
const response = await fetch(card.dataset.reactUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
@@ -161,14 +214,19 @@
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
activeButton.disabled = false;
|
||||
if (reactButton) {
|
||||
reactButton.disabled = false;
|
||||
}
|
||||
closePicker();
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
renderReactions(activeCard, payload.reactions || []);
|
||||
activeButton.remove();
|
||||
renderReactions(card, payload.reactions || []);
|
||||
card.dataset.reacted = "true";
|
||||
if (reactButton) {
|
||||
reactButton.remove();
|
||||
}
|
||||
closePicker();
|
||||
}
|
||||
|
||||
@@ -179,20 +237,68 @@
|
||||
}
|
||||
list.replaceChildren();
|
||||
reactions.forEach((reaction) => {
|
||||
const pill = document.createElement("span");
|
||||
pill.className = "reaction-pill";
|
||||
pill.title = reaction.label || reaction.emojiId;
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = reaction.url;
|
||||
image.alt = reaction.label || reaction.emojiId;
|
||||
image.loading = "lazy";
|
||||
|
||||
const count = document.createElement("span");
|
||||
count.textContent = reaction.count;
|
||||
|
||||
pill.append(image, count);
|
||||
const pill = buildReactionPill(reaction);
|
||||
if (!reaction.visible) {
|
||||
pill.classList.add("is-hidden-summary");
|
||||
}
|
||||
list.append(pill);
|
||||
});
|
||||
const hiddenCount = reactions.length > 2 ? reactions.length - 2 : 0;
|
||||
if (hiddenCount > 0) {
|
||||
const more = document.createElement("button");
|
||||
more.className = "reaction-more";
|
||||
more.type = "button";
|
||||
more.dataset.reactionMore = "";
|
||||
more.textContent = `+${hiddenCount}`;
|
||||
more.setAttribute("aria-label", `Show ${hiddenCount} more reactions`);
|
||||
list.append(more);
|
||||
}
|
||||
}
|
||||
|
||||
function buildReactionPill(reaction) {
|
||||
const pill = document.createElement("button");
|
||||
pill.className = "reaction-pill";
|
||||
pill.type = "button";
|
||||
pill.title = reaction.label || reaction.emojiId;
|
||||
pill.dataset.reactionPill = "";
|
||||
pill.dataset.reactionEmojiId = reaction.emojiId;
|
||||
pill.dataset.reactionLabel = reaction.label || reaction.emojiId;
|
||||
pill.dataset.reactionUrl = reaction.url;
|
||||
pill.dataset.reactionCount = reaction.count;
|
||||
pill.setAttribute("aria-label", `React with ${reaction.label || reaction.emojiId}`);
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = reaction.url;
|
||||
image.alt = reaction.label || reaction.emojiId;
|
||||
image.loading = "lazy";
|
||||
|
||||
const count = document.createElement("span");
|
||||
count.textContent = reaction.count;
|
||||
|
||||
pill.append(image, count);
|
||||
return pill;
|
||||
}
|
||||
|
||||
function populateExistingReactions(card) {
|
||||
if (!existingSection || !existingList) {
|
||||
return;
|
||||
}
|
||||
existingList.replaceChildren();
|
||||
card.querySelectorAll("[data-reaction-pill]").forEach((pill) => {
|
||||
const clone = pill.cloneNode(true);
|
||||
clone.classList.remove("is-hidden-summary");
|
||||
existingList.append(clone);
|
||||
});
|
||||
existingSection.hidden = existingList.children.length === 0;
|
||||
}
|
||||
|
||||
function setPickerReadonly(readonly) {
|
||||
picker.classList.toggle("is-readonly", readonly);
|
||||
chooserElements.forEach((element) => {
|
||||
element.hidden = readonly;
|
||||
});
|
||||
if (readonlyNote) {
|
||||
readonlyNote.hidden = !readonly;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user