05/09 Update: favorite item
This commit is contained in:
@@ -2318,3 +2318,35 @@ html.sidebar-open {
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ── Favorite star button ───────────────────────────────────────────────── */
|
||||
|
||||
/* Outline star — subtle, not distracting */
|
||||
.btn-favorite {
|
||||
color: #d1d5db;
|
||||
font-size: 16px;
|
||||
transition:
|
||||
color var(--transition),
|
||||
transform 0.12s ease;
|
||||
}
|
||||
|
||||
.btn-favorite:hover {
|
||||
color: #f59e0b;
|
||||
background: #fef9c3;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
/* Filled gold star — item is favorited */
|
||||
.btn-favorite.is-favorite {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.btn-favorite.is-favorite:hover {
|
||||
color: #d97706;
|
||||
background: #fef3c7;
|
||||
}
|
||||
|
||||
/* Brief pop animation when toggling */
|
||||
.btn-favorite:active {
|
||||
transform: scale(0.88);
|
||||
}
|
||||
|
||||
+82
-1
@@ -511,7 +511,9 @@ const Vault = (() => {
|
||||
item.item_type === "password" &&
|
||||
!!extractTotpSecret(item.plain?.totp_uri);
|
||||
|
||||
const tagBadges = itemTags
|
||||
const isFavorite = itemTags.includes("favorite");
|
||||
const visibleTags = itemTags.filter((t) => t !== "favorite");
|
||||
const tagBadges = visibleTags
|
||||
.map((t) => `<span class="item-tag">${escHtml(t)}</span>`)
|
||||
.join("");
|
||||
|
||||
@@ -528,6 +530,7 @@ const Vault = (() => {
|
||||
${showCopyUser ? `<button class="btn-icon" title="Copy username" data-action="copy-user">👤</button>` : ""}
|
||||
${showCopyPass ? `<button class="btn-icon" title="Copy secret" data-action="copy-pass">📋</button>` : ""}
|
||||
${showTotp ? `<button class="btn-icon" title="Copy 2FA code" data-action="copy-totp">🔐</button>` : ""}
|
||||
<button class="btn-icon btn-favorite${isFavorite ? " is-favorite" : ""}" title="${isFavorite ? "Remove from favorites" : "Add to favorites"}" data-action="favorite">${isFavorite ? "★" : "☆"}</button>
|
||||
<button class="btn-icon" title="Edit" data-action="edit">✏️</button>
|
||||
<button class="btn-icon" title="Delete" data-action="delete">🗑️</button>
|
||||
</div>`;
|
||||
@@ -603,6 +606,13 @@ const Vault = (() => {
|
||||
},
|
||||
);
|
||||
}
|
||||
li.querySelector('[data-action="favorite"]').addEventListener(
|
||||
"click",
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
toggleFavorite(item);
|
||||
},
|
||||
);
|
||||
li.querySelector('[data-action="edit"]').addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
openModal("edit", item);
|
||||
@@ -3125,6 +3135,67 @@ const Vault = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the "favorite" tag on a vault item.
|
||||
* Re-encrypts and PUTs the item in-place, then patches _items and
|
||||
* re-renders so the star flips instantly without a full vault reload.
|
||||
*/
|
||||
async function toggleFavorite(item) {
|
||||
const vaultKey = VaultSession.getKey();
|
||||
if (!vaultKey) {
|
||||
showUnlockOverlay();
|
||||
return;
|
||||
}
|
||||
if (!item.plain) {
|
||||
showToast("Item could not be decrypted", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTags = item.plain.tags || [];
|
||||
const isFavorite = currentTags.includes("favorite");
|
||||
const newTags = isFavorite
|
||||
? currentTags.filter((t) => t !== "favorite")
|
||||
: [...currentTags, "favorite"].sort();
|
||||
|
||||
const updatedPlain = { ...item.plain, tags: newTags };
|
||||
|
||||
try {
|
||||
const { enc_data, iv } = await Crypto.encryptItem(vaultKey, updatedPlain);
|
||||
const { enc_name, iv_name } = await Crypto.encryptName(vaultKey, item.name);
|
||||
|
||||
const payload = {
|
||||
name: item.item_type,
|
||||
item_type: item.item_type,
|
||||
folder_id: item.folder_id || null,
|
||||
enc_data,
|
||||
iv,
|
||||
enc_name,
|
||||
iv_name,
|
||||
};
|
||||
|
||||
const res = await apiFetch(`/api/vault/${item.id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res) return;
|
||||
|
||||
// Patch the in-memory item so the re-render is instant.
|
||||
const idx = _items.findIndex((i) => i.id === item.id);
|
||||
if (idx !== -1) {
|
||||
_items[idx] = { ..._items[idx], plain: updatedPlain };
|
||||
}
|
||||
|
||||
showToast(isFavorite ? "Removed from favorites" : "Added to favorites ★");
|
||||
|
||||
// Refresh the sidebar tag list ("favorite" tag may appear/disappear).
|
||||
renderTagList();
|
||||
// Re-render the current view preserving the active filter.
|
||||
applyCurrentFilter();
|
||||
} catch (err) {
|
||||
showToast("Could not update favorite: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Logout ────────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleLogout() {
|
||||
@@ -3450,6 +3521,16 @@ const Vault = (() => {
|
||||
updateVaultTitle("All Items");
|
||||
renderItemList(_items);
|
||||
});
|
||||
document.getElementById("sidebar-favorites")?.addEventListener("click", () => {
|
||||
_activeFilter = { type: "tag", value: "favorite" };
|
||||
switchView("vault");
|
||||
document
|
||||
.querySelectorAll(".sidebar-item")
|
||||
.forEach((el) => el.classList.remove("active"));
|
||||
document.getElementById("sidebar-favorites").classList.add("active");
|
||||
updateVaultTitle("⭐ Favorites");
|
||||
renderItemList(_items.filter((i) => (i.plain?.tags || []).includes("favorite")));
|
||||
});
|
||||
document
|
||||
.getElementById("sidebar-security")
|
||||
?.addEventListener("click", () => switchView("security"));
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
<span class="sidebar-icon">🏠</span>
|
||||
<span class="sidebar-label">All Items</span>
|
||||
</li>
|
||||
<li
|
||||
class="sidebar-item"
|
||||
id="sidebar-favorites"
|
||||
data-tooltip="Favorites"
|
||||
>
|
||||
<span class="sidebar-icon">⭐</span>
|
||||
<span class="sidebar-label">Favorites</span>
|
||||
</li>
|
||||
<li class="sidebar-section-header">Item Types</li>
|
||||
<li
|
||||
class="sidebar-item"
|
||||
|
||||
Reference in New Issue
Block a user