Don't forget about ungrouped entries when selecting export groups
This commit is contained in:
parent
fb8df00e91
commit
dc7c29b7ce
@ -21,7 +21,7 @@ src/
|
|||||||
│ └── SettingsDialog.svelte # Auto-lock and tab-switch settings
|
│ └── SettingsDialog.svelte # Auto-lock and tab-switch settings
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── crypto/crypto.js # Web Crypto API: PBKDF2 key derivation, AES-GCM encrypt/decrypt, password generator
|
│ ├── crypto/crypto.js # Web Crypto API: PBKDF2 key derivation, AES-GCM encrypt/decrypt, password generator
|
||||||
│ ├── storage/db.js # IndexedDB layer (idb wrapper): entries, groups, meta stores
|
│ ├── storage/db.js # IndexedDB layer (idb wrapper): entries, groups, meta stores, exportSelected(groupIds)
|
||||||
│ ├── models/schema.js # Data models: CredentialEntry, Group; validation; ID generation
|
│ ├── models/schema.js # Data models: CredentialEntry, Group; validation; ID generation
|
||||||
│ ├── autofocus.js # Svelte action for autofocus on mount
|
│ ├── autofocus.js # Svelte action for autofocus on mount
|
||||||
│ └── stores/
|
│ └── stores/
|
||||||
@ -81,6 +81,11 @@ Password verification uses a test payload (random string encrypted at vault crea
|
|||||||
- Clipboard auto-clears after 15 seconds.
|
- Clipboard auto-clears after 15 seconds.
|
||||||
- No browser fingerprinting or anti-keylogger protections.
|
- No browser fingerprinting or anti-keylogger protections.
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
- `exportSelected(groupIds)` replaces the old `exportAll()` — accepts an array of group IDs to export. Pass `null` or `[]` for a full export. Vault meta (salt, test payload) is always included for import decryption.
|
||||||
|
- `ImportExport.svelte` fetches groups/entries on modal open and shows a checkbox list for group selection with live entry count.
|
||||||
|
|
||||||
## Known Bug Fixes
|
## Known Bug Fixes
|
||||||
|
|
||||||
- `base64ToUint8Array` was used in `db.js` `importAll()` but never imported — now imported from `crypto.js`.
|
- `base64ToUint8Array` was used in `db.js` `importAll()` but never imported — now imported from `crypto.js`.
|
||||||
|
|||||||
@ -10,7 +10,7 @@ An offline-first password manager that runs entirely in your browser. No server,
|
|||||||
- **Full-text search** — Instant search across title, username, URL, and notes.
|
- **Full-text search** — Instant search across title, username, URL, and notes.
|
||||||
- **Password generator** — One-click random password generation in the entry form (🎲 button). Uses Web Crypto API for cryptographically secure randomness.
|
- **Password generator** — One-click random password generation in the entry form (🎲 button). Uses Web Crypto API for cryptographically secure randomness.
|
||||||
- **Copy to clipboard** — One-click copy with 15-second auto-clear.
|
- **Copy to clipboard** — One-click copy with 15-second auto-clear.
|
||||||
- **JSON import/export** — Export your entire vault as encrypted JSON. Import with merge or replace mode.
|
- **JSON import/export** — Export your vault as encrypted JSON with selective group filtering. Import with merge or replace mode.
|
||||||
- **Auto-lock** — Vault locks automatically on tab switch, visibility change, or 5-minute inactivity timer.
|
- **Auto-lock** — Vault locks automatically on tab switch, visibility change, or 5-minute inactivity timer.
|
||||||
- **Dark theme** — Responsive layout that works on desktop and mobile.
|
- **Dark theme** — Responsive layout that works on desktop and mobile.
|
||||||
|
|
||||||
|
|||||||
353
dist/index.html
vendored
353
dist/index.html
vendored
@ -4513,6 +4513,15 @@ function set_value(element, value) {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @param {Element} element
|
* @param {Element} element
|
||||||
|
* @param {boolean} checked
|
||||||
|
*/
|
||||||
|
function set_checked(element, checked) {
|
||||||
|
var attributes = get_attributes(element);
|
||||||
|
if (attributes.checked === (attributes.checked = checked ?? void 0)) return;
|
||||||
|
element.checked = checked;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Element} element
|
||||||
* @param {string} attribute
|
* @param {string} attribute
|
||||||
* @param {string | null} value
|
* @param {string | null} value
|
||||||
* @param {boolean} [skip_warning]
|
* @param {boolean} [skip_warning]
|
||||||
@ -5578,20 +5587,35 @@ async function moveEntryToGroup(entryId, groupId) {
|
|||||||
await db.put("entries", entry);
|
await db.put("entries", entry);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Export all data (entries + groups + meta) as a JSON object.
|
* Export data (entries + groups + meta) as a JSON object.
|
||||||
* Entries remain encrypted with the source vault's key. The import function
|
* Entries remain encrypted with the source vault's key. The import function
|
||||||
* requires the source vault's master password to decrypt and re-encrypt
|
* requires the source vault's master password to decrypt and re-encrypt
|
||||||
* entries under the target vault's key.
|
* entries under the target vault's key.
|
||||||
*
|
*
|
||||||
|
* @param {string[]} [groupIds] - Array of group IDs to export. If null/empty, exports everything.
|
||||||
|
* Include '' to export ungrouped entries.
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
async function exportAll() {
|
async function exportSelected(groupIds) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const entries = await db.getAll("entries");
|
const allEntries = await db.getAll("entries");
|
||||||
const groups = await db.getAll("groups");
|
const allGroups = await db.getAll("groups");
|
||||||
const saltRow = await db.get("meta", "salt");
|
const saltRow = await db.get("meta", "salt");
|
||||||
const testEncryptedRow = await db.get("meta", "testEncrypted");
|
const testEncryptedRow = await db.get("meta", "testEncrypted");
|
||||||
const testPlaintextRow = await db.get("meta", "testPlaintext");
|
const testPlaintextRow = await db.get("meta", "testPlaintext");
|
||||||
|
if (!groupIds || groupIds.length === 0) return {
|
||||||
|
version: DB_VERSION,
|
||||||
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
||||||
|
meta: {
|
||||||
|
salt: saltRow?.value || null,
|
||||||
|
testEncrypted: testEncryptedRow?.value || null,
|
||||||
|
testPlaintext: testPlaintextRow?.value || null
|
||||||
|
},
|
||||||
|
groups: allGroups,
|
||||||
|
entries: allEntries
|
||||||
|
};
|
||||||
|
const entries = allEntries.filter((e) => groupIds.includes(e.groupId));
|
||||||
|
const groups = allGroups.filter((g) => groupIds.includes(g.id));
|
||||||
return {
|
return {
|
||||||
version: DB_VERSION,
|
version: DB_VERSION,
|
||||||
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
||||||
@ -5973,9 +5997,9 @@ var search = new SearchStore();
|
|||||||
//#region src/components/Sidebar.svelte
|
//#region src/components/Sidebar.svelte
|
||||||
var root_2$5 = /* @__PURE__ */ from_html(`<div class="group-row svelte-181dlmc"><button><span class="group-color svelte-181dlmc"></span> <span class="group-name svelte-181dlmc"> </span> <span class="drop-icon svelte-181dlmc">📥</span></button> <div class="group-actions svelte-181dlmc"><button class="group-action-btn svelte-181dlmc" title="Edit group">✏️</button> <button class="group-action-btn svelte-181dlmc" title="Delete group">🗑</button></div></div>`);
|
var root_2$5 = /* @__PURE__ */ from_html(`<div class="group-row svelte-181dlmc"><button><span class="group-color svelte-181dlmc"></span> <span class="group-name svelte-181dlmc"> </span> <span class="drop-icon svelte-181dlmc">📥</span></button> <div class="group-actions svelte-181dlmc"><button class="group-action-btn svelte-181dlmc" title="Edit group">✏️</button> <button class="group-action-btn svelte-181dlmc" title="Delete group">🗑</button></div></div>`);
|
||||||
var root_4$5 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-181dlmc"> </div>`);
|
var root_4$5 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-181dlmc"> </div>`);
|
||||||
var root_5$4 = /* @__PURE__ */ from_html(`<button></button>`);
|
var root_5$5 = /* @__PURE__ */ from_html(`<button></button>`);
|
||||||
var root_3$5 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Group settings" tabindex="-1"><h3 class="svelte-181dlmc"> </h3> <!> <div class="form-group svelte-181dlmc"><label for="group-name" class="svelte-181dlmc">Group Name</label> <input id="group-name" type="text" placeholder="e.g. Work, Personal" class="svelte-181dlmc"/></div> <div class="form-group svelte-181dlmc"><span class="field-label svelte-181dlmc">Color</span> <div class="color-picker svelte-181dlmc"></div></div> <div class="modal-actions svelte-181dlmc"><button class="btn btn-primary svelte-181dlmc"> </button> <button class="btn btn-ghost svelte-181dlmc">Cancel</button></div></div></div>`);
|
var root_3$5 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Group settings" tabindex="-1"><h3 class="svelte-181dlmc"> </h3> <!> <div class="form-group svelte-181dlmc"><label for="group-name" class="svelte-181dlmc">Group Name</label> <input id="group-name" type="text" placeholder="e.g. Work, Personal" class="svelte-181dlmc"/></div> <div class="form-group svelte-181dlmc"><span class="field-label svelte-181dlmc">Color</span> <div class="color-picker svelte-181dlmc"></div></div> <div class="modal-actions svelte-181dlmc"><button class="btn btn-primary svelte-181dlmc"> </button> <button class="btn btn-ghost svelte-181dlmc">Cancel</button></div></div></div>`);
|
||||||
var root_6$4 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Delete group confirmation" tabindex="-1"><h3 class="svelte-181dlmc">Delete Group</h3> <p class="svelte-181dlmc">Delete "<strong class="svelte-181dlmc"> </strong>"? Entries in this group will become ungrouped.</p> <div class="modal-actions svelte-181dlmc"><button class="btn btn-danger svelte-181dlmc">Yes, delete</button> <button class="btn btn-ghost svelte-181dlmc">Cancel</button></div></div></div>`);
|
var root_6$3 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Delete group confirmation" tabindex="-1"><h3 class="svelte-181dlmc">Delete Group</h3> <p class="svelte-181dlmc">Delete "<strong class="svelte-181dlmc"> </strong>"? Entries in this group will become ungrouped.</p> <div class="modal-actions svelte-181dlmc"><button class="btn btn-danger svelte-181dlmc">Yes, delete</button> <button class="btn btn-ghost svelte-181dlmc">Cancel</button></div></div></div>`);
|
||||||
var root$6 = /* @__PURE__ */ from_html(`<div class="sidebar-content svelte-181dlmc"><div class="sidebar-header svelte-181dlmc"><h2 class="svelte-181dlmc">🔐 Vault</h2></div> <div class="search-box svelte-181dlmc"><input type="text" placeholder="Search entries..." class="svelte-181dlmc"/></div> <nav class="groups-nav svelte-181dlmc"><button><span class="group-icon svelte-181dlmc">📋</span> <span class="group-name svelte-181dlmc">All Entries</span></button> <!></nav> <div class="trash-section svelte-181dlmc"><button><span class="group-color svelte-181dlmc"></span> <span class="group-name svelte-181dlmc"> </span></button></div> <div class="sidebar-footer svelte-181dlmc"><button class="btn btn-ghost btn-sm w-full svelte-181dlmc">+ New Group</button></div> <!> <!></div>`);
|
var root$6 = /* @__PURE__ */ from_html(`<div class="sidebar-content svelte-181dlmc"><div class="sidebar-header svelte-181dlmc"><h2 class="svelte-181dlmc">🔐 Vault</h2></div> <div class="search-box svelte-181dlmc"><input type="text" placeholder="Search entries..." class="svelte-181dlmc"/></div> <nav class="groups-nav svelte-181dlmc"><button><span class="group-icon svelte-181dlmc">📋</span> <span class="group-name svelte-181dlmc">All Entries</span></button> <!></nav> <div class="trash-section svelte-181dlmc"><button><span class="group-color svelte-181dlmc"></span> <span class="group-name svelte-181dlmc"> </span></button></div> <div class="sidebar-footer svelte-181dlmc"><button class="btn btn-ghost btn-sm w-full svelte-181dlmc">+ New Group</button></div> <!> <!></div>`);
|
||||||
function Sidebar($$anchor, $$props) {
|
function Sidebar($$anchor, $$props) {
|
||||||
push($$props, true);
|
push($$props, true);
|
||||||
@ -6151,7 +6175,7 @@ function Sidebar($$anchor, $$props) {
|
|||||||
var div_10 = sibling(div_9, 2);
|
var div_10 = sibling(div_9, 2);
|
||||||
var div_11 = sibling(child(div_10), 2);
|
var div_11 = sibling(child(div_10), 2);
|
||||||
each(div_11, 21, () => GROUP_COLORS, index, ($$anchor, color) => {
|
each(div_11, 21, () => GROUP_COLORS, index, ($$anchor, color) => {
|
||||||
var button_6 = root_5$4();
|
var button_6 = root_5$5();
|
||||||
template_effect(() => {
|
template_effect(() => {
|
||||||
set_class(button_6, 1, `color-swatch ${get(groupColor) === get(color) ? "selected" : ""}`, "svelte-181dlmc");
|
set_class(button_6, 1, `color-swatch ${get(groupColor) === get(color) ? "selected" : ""}`, "svelte-181dlmc");
|
||||||
set_style(button_6, `background-color: ${get(color) ?? ""}`);
|
set_style(button_6, `background-color: ${get(color) ?? ""}`);
|
||||||
@ -6185,7 +6209,7 @@ function Sidebar($$anchor, $$props) {
|
|||||||
});
|
});
|
||||||
var node_4 = sibling(node_2, 2);
|
var node_4 = sibling(node_2, 2);
|
||||||
var consequent_3 = ($$anchor) => {
|
var consequent_3 = ($$anchor) => {
|
||||||
var div_13 = root_6$4();
|
var div_13 = root_6$3();
|
||||||
var div_14 = child(div_13);
|
var div_14 = child(div_13);
|
||||||
var p = sibling(child(div_14), 2);
|
var p = sibling(child(div_14), 2);
|
||||||
var strong = sibling(child(p));
|
var strong = sibling(child(p));
|
||||||
@ -6231,12 +6255,12 @@ var root_1$6 = /* @__PURE__ */ from_html(`<div class="loading svelte-13s7gu4">Lo
|
|||||||
var root_2$4 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-13s7gu4"> </div>`);
|
var root_2$4 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-13s7gu4"> </div>`);
|
||||||
var root_4$4 = /* @__PURE__ */ from_html(`<button class="btn btn-primary mt-3">+ New Entry</button>`);
|
var root_4$4 = /* @__PURE__ */ from_html(`<button class="btn btn-primary mt-3">+ New Entry</button>`);
|
||||||
var root_3$4 = /* @__PURE__ */ from_html(`<div class="empty-state svelte-13s7gu4"><p class="empty-icon svelte-13s7gu4"> </p> <p class="empty-text svelte-13s7gu4"> </p> <p class="empty-hint svelte-13s7gu4"> </p> <!></div>`);
|
var root_3$4 = /* @__PURE__ */ from_html(`<div class="empty-state svelte-13s7gu4"><p class="empty-icon svelte-13s7gu4"> </p> <p class="empty-text svelte-13s7gu4"> </p> <p class="empty-hint svelte-13s7gu4"> </p> <!></div>`);
|
||||||
var root_6$3 = /* @__PURE__ */ from_html(`matching "<strong> </strong>"`, 1);
|
var root_6$2 = /* @__PURE__ */ from_html(`matching "<strong> </strong>"`, 1);
|
||||||
var root_7$4 = /* @__PURE__ */ from_html(`<th style="width: 60px" class="svelte-13s7gu4"></th>`);
|
var root_7$4 = /* @__PURE__ */ from_html(`<th style="width: 60px" class="svelte-13s7gu4"></th>`);
|
||||||
var root_9$1 = /* @__PURE__ */ from_html(`<span class="drag-handle svelte-13s7gu4" aria-hidden="true">⠿</span>`);
|
var root_9$1 = /* @__PURE__ */ from_html(`<span class="drag-handle svelte-13s7gu4" aria-hidden="true">⠿</span>`);
|
||||||
var root_10$1 = /* @__PURE__ */ from_html(`<td class="svelte-13s7gu4"><button class="btn btn-ghost btn-sm restore-btn svelte-13s7gu4" title="Restore entry">↩️</button></td>`);
|
var root_10$1 = /* @__PURE__ */ from_html(`<td class="svelte-13s7gu4"><button class="btn btn-ghost btn-sm restore-btn svelte-13s7gu4" title="Restore entry">↩️</button></td>`);
|
||||||
var root_8$2 = /* @__PURE__ */ from_html(`<tr><td class="svelte-13s7gu4"><!> <span class="entry-title svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-username svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-url truncate svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-notes truncate svelte-13s7gu4"> </span></td><!></tr>`);
|
var root_8$3 = /* @__PURE__ */ from_html(`<tr><td class="svelte-13s7gu4"><!> <span class="entry-title svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-username svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-url truncate svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-notes truncate svelte-13s7gu4"> </span></td><!></tr>`);
|
||||||
var root_5$3 = /* @__PURE__ */ from_html(`<div class="results-info svelte-13s7gu4"><span class="text-sm text-muted"> <!></span></div> <table class="entries-table svelte-13s7gu4"><thead><tr><th class="svelte-13s7gu4">Title</th><th class="svelte-13s7gu4">Username</th><th class="svelte-13s7gu4">URL</th><th class="svelte-13s7gu4">Notes</th><!></tr></thead><tbody></tbody></table>`, 1);
|
var root_5$4 = /* @__PURE__ */ from_html(`<div class="results-info svelte-13s7gu4"><span class="text-sm text-muted"> <!></span></div> <table class="entries-table svelte-13s7gu4"><thead><tr><th class="svelte-13s7gu4">Title</th><th class="svelte-13s7gu4">Username</th><th class="svelte-13s7gu4">URL</th><th class="svelte-13s7gu4">Notes</th><!></tr></thead><tbody></tbody></table>`, 1);
|
||||||
var root$5 = /* @__PURE__ */ from_html(`<div class="entry-list"><!></div>`);
|
var root$5 = /* @__PURE__ */ from_html(`<div class="entry-list"><!></div>`);
|
||||||
function EntryList($$anchor, $$props) {
|
function EntryList($$anchor, $$props) {
|
||||||
push($$props, true);
|
push($$props, true);
|
||||||
@ -6319,13 +6343,13 @@ function EntryList($$anchor, $$props) {
|
|||||||
append($$anchor, div_3);
|
append($$anchor, div_3);
|
||||||
};
|
};
|
||||||
var alternate = ($$anchor) => {
|
var alternate = ($$anchor) => {
|
||||||
var fragment = root_5$3();
|
var fragment = root_5$4();
|
||||||
var div_4 = first_child(fragment);
|
var div_4 = first_child(fragment);
|
||||||
var span = child(div_4);
|
var span = child(div_4);
|
||||||
var text_4 = child(span);
|
var text_4 = child(span);
|
||||||
var node_2 = sibling(text_4);
|
var node_2 = sibling(text_4);
|
||||||
var consequent_4 = ($$anchor) => {
|
var consequent_4 = ($$anchor) => {
|
||||||
var fragment_1 = root_6$3();
|
var fragment_1 = root_6$2();
|
||||||
var strong = sibling(first_child(fragment_1));
|
var strong = sibling(first_child(fragment_1));
|
||||||
var text_5 = child(strong, true);
|
var text_5 = child(strong, true);
|
||||||
reset(strong);
|
reset(strong);
|
||||||
@ -6352,7 +6376,7 @@ function EntryList($$anchor, $$props) {
|
|||||||
reset(thead);
|
reset(thead);
|
||||||
var tbody = sibling(thead);
|
var tbody = sibling(thead);
|
||||||
each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => {
|
each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => {
|
||||||
var tr_1 = root_8$2();
|
var tr_1 = root_8$3();
|
||||||
var td = child(tr_1);
|
var td = child(tr_1);
|
||||||
var node_4 = child(td);
|
var node_4 = child(td);
|
||||||
var consequent_6 = ($$anchor) => {
|
var consequent_6 = ($$anchor) => {
|
||||||
@ -6438,14 +6462,14 @@ var root_1$5 = /* @__PURE__ */ from_html(`<div class="toast svelte-dssgjx"> </di
|
|||||||
var root_2$3 = /* @__PURE__ */ from_html(`<div class="loading svelte-dssgjx">Loading...</div>`);
|
var root_2$3 = /* @__PURE__ */ from_html(`<div class="loading svelte-dssgjx">Loading...</div>`);
|
||||||
var root_3$3 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-dssgjx"> </div>`);
|
var root_3$3 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-dssgjx"> </div>`);
|
||||||
var root_4$3 = /* @__PURE__ */ from_html(`<div class="empty-state svelte-dssgjx">Entry not found</div>`);
|
var root_4$3 = /* @__PURE__ */ from_html(`<div class="empty-state svelte-dssgjx">Entry not found</div>`);
|
||||||
var root_6$2 = /* @__PURE__ */ from_html(`<button class="btn btn-primary btn-sm">↩️ Restore</button> <button class="btn btn-danger btn-sm">🗑 Delete Forever</button>`, 1);
|
var root_6$1 = /* @__PURE__ */ from_html(`<button class="btn btn-primary btn-sm">↩️ Restore</button> <button class="btn btn-danger btn-sm">🗑 Delete Forever</button>`, 1);
|
||||||
var root_7$3 = /* @__PURE__ */ from_html(`<button class="btn btn-ghost btn-sm">✏️ Edit</button> <button class="btn btn-danger btn-sm">🗑 Move to Trash</button>`, 1);
|
var root_7$3 = /* @__PURE__ */ from_html(`<button class="btn btn-ghost btn-sm">✏️ Edit</button> <button class="btn btn-danger btn-sm">🗑 Move to Trash</button>`, 1);
|
||||||
var root_8$1 = /* @__PURE__ */ from_html(`<div class="detail-field"><span class="field-label svelte-dssgjx">Username</span> <div class="field-value svelte-dssgjx"><span> </span> <button class="btn btn-ghost btn-sm copy-btn svelte-dssgjx" title="Copy username">📋</button></div></div>`);
|
var root_8$2 = /* @__PURE__ */ from_html(`<div class="detail-field"><span class="field-label svelte-dssgjx">Username</span> <div class="field-value svelte-dssgjx"><span> </span> <button class="btn btn-ghost btn-sm copy-btn svelte-dssgjx" title="Copy username">📋</button></div></div>`);
|
||||||
var root_9 = /* @__PURE__ */ from_html(`<div class="detail-field"><span class="field-label svelte-dssgjx">URL</span> <div class="field-value svelte-dssgjx"><a target="_blank" rel="noopener noreferrer" class="svelte-dssgjx"> </a> <button class="btn btn-ghost btn-sm copy-btn svelte-dssgjx" title="Copy URL">📋</button></div></div>`);
|
var root_9 = /* @__PURE__ */ from_html(`<div class="detail-field"><span class="field-label svelte-dssgjx">URL</span> <div class="field-value svelte-dssgjx"><a target="_blank" rel="noopener noreferrer" class="svelte-dssgjx"> </a> <button class="btn btn-ghost btn-sm copy-btn svelte-dssgjx" title="Copy URL">📋</button></div></div>`);
|
||||||
var root_10 = /* @__PURE__ */ from_html(`<div class="detail-field"><span class="field-label svelte-dssgjx">Notes</span> <div class="field-value notes svelte-dssgjx"> </div></div>`);
|
var root_10 = /* @__PURE__ */ from_html(`<div class="detail-field"><span class="field-label svelte-dssgjx">Notes</span> <div class="field-value notes svelte-dssgjx"> </div></div>`);
|
||||||
var root_11 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-dssgjx" role="presentation"><div class="modal svelte-dssgjx" role="dialog" aria-modal="true" aria-label="Move to trash confirmation" tabindex="-1"><h3 class="svelte-dssgjx">Move to Trash</h3> <p class="svelte-dssgjx">Move "<strong> </strong>" to the trash? You can restore it later.</p> <div class="modal-actions svelte-dssgjx"><button class="btn btn-danger"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
var root_11 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-dssgjx" role="presentation"><div class="modal svelte-dssgjx" role="dialog" aria-modal="true" aria-label="Move to trash confirmation" tabindex="-1"><h3 class="svelte-dssgjx">Move to Trash</h3> <p class="svelte-dssgjx">Move "<strong> </strong>" to the trash? You can restore it later.</p> <div class="modal-actions svelte-dssgjx"><button class="btn btn-danger"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
||||||
var root_12 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-dssgjx" role="presentation"><div class="modal svelte-dssgjx" role="dialog" aria-modal="true" aria-label="Permanent delete confirmation" tabindex="-1"><h3 class="svelte-dssgjx">Delete Forever</h3> <p class="svelte-dssgjx">Permanently delete "<strong> </strong>"? This cannot be undone.</p> <div class="modal-actions svelte-dssgjx"><button class="btn btn-danger"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
var root_12 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-dssgjx" role="presentation"><div class="modal svelte-dssgjx" role="dialog" aria-modal="true" aria-label="Permanent delete confirmation" tabindex="-1"><h3 class="svelte-dssgjx">Delete Forever</h3> <p class="svelte-dssgjx">Permanently delete "<strong> </strong>"? This cannot be undone.</p> <div class="modal-actions svelte-dssgjx"><button class="btn btn-danger"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
||||||
var root_5$2 = /* @__PURE__ */ from_html(`<div class="detail-card svelte-dssgjx"><div class="detail-header svelte-dssgjx"><h2 class="svelte-dssgjx"> </h2> <div class="header-actions svelte-dssgjx"><!></div></div> <div class="detail-fields svelte-dssgjx"><!> <div class="detail-field"><span class="field-label svelte-dssgjx">Password</span> <div class="field-value svelte-dssgjx"><span> </span> <button class="btn btn-ghost btn-sm" title="Toggle visibility"> </button> <button class="btn btn-ghost btn-sm copy-btn svelte-dssgjx" title="Copy password">📋</button></div></div> <!> <!></div> <div class="detail-meta svelte-dssgjx"><span class="text-xs text-muted"> </span> <span class="text-xs text-muted"> </span></div></div> <!> <!>`, 1);
|
var root_5$3 = /* @__PURE__ */ from_html(`<div class="detail-card svelte-dssgjx"><div class="detail-header svelte-dssgjx"><h2 class="svelte-dssgjx"> </h2> <div class="header-actions svelte-dssgjx"><!></div></div> <div class="detail-fields svelte-dssgjx"><!> <div class="detail-field"><span class="field-label svelte-dssgjx">Password</span> <div class="field-value svelte-dssgjx"><span> </span> <button class="btn btn-ghost btn-sm" title="Toggle visibility"> </button> <button class="btn btn-ghost btn-sm copy-btn svelte-dssgjx" title="Copy password">📋</button></div></div> <!> <!></div> <div class="detail-meta svelte-dssgjx"><span class="text-xs text-muted"> </span> <span class="text-xs text-muted"> </span></div></div> <!> <!>`, 1);
|
||||||
var root$4 = /* @__PURE__ */ from_html(`<div class="entry-detail"><!> <!></div>`);
|
var root$4 = /* @__PURE__ */ from_html(`<div class="entry-detail"><!> <!></div>`);
|
||||||
function EntryDetail($$anchor, $$props) {
|
function EntryDetail($$anchor, $$props) {
|
||||||
push($$props, true);
|
push($$props, true);
|
||||||
@ -6549,7 +6573,7 @@ function EntryDetail($$anchor, $$props) {
|
|||||||
append($$anchor, root_4$3());
|
append($$anchor, root_4$3());
|
||||||
};
|
};
|
||||||
var alternate_1 = ($$anchor) => {
|
var alternate_1 = ($$anchor) => {
|
||||||
var fragment = root_5$2();
|
var fragment = root_5$3();
|
||||||
var div_5 = first_child(fragment);
|
var div_5 = first_child(fragment);
|
||||||
var div_6 = child(div_5);
|
var div_6 = child(div_5);
|
||||||
var h2 = child(div_6);
|
var h2 = child(div_6);
|
||||||
@ -6558,7 +6582,7 @@ function EntryDetail($$anchor, $$props) {
|
|||||||
var div_7 = sibling(h2, 2);
|
var div_7 = sibling(h2, 2);
|
||||||
var node_2 = child(div_7);
|
var node_2 = child(div_7);
|
||||||
var consequent_4 = ($$anchor) => {
|
var consequent_4 = ($$anchor) => {
|
||||||
var fragment_1 = root_6$2();
|
var fragment_1 = root_6$1();
|
||||||
var button = first_child(fragment_1);
|
var button = first_child(fragment_1);
|
||||||
var button_1 = sibling(button, 2);
|
var button_1 = sibling(button, 2);
|
||||||
delegated("click", button, () => $$props.onEdit(get(entry).id));
|
delegated("click", button, () => $$props.onEdit(get(entry).id));
|
||||||
@ -6582,7 +6606,7 @@ function EntryDetail($$anchor, $$props) {
|
|||||||
var div_8 = sibling(div_6, 2);
|
var div_8 = sibling(div_6, 2);
|
||||||
var node_3 = child(div_8);
|
var node_3 = child(div_8);
|
||||||
var consequent_5 = ($$anchor) => {
|
var consequent_5 = ($$anchor) => {
|
||||||
var div_9 = root_8$1();
|
var div_9 = root_8$2();
|
||||||
var div_10 = sibling(child(div_9), 2);
|
var div_10 = sibling(child(div_9), 2);
|
||||||
var span = child(div_10);
|
var span = child(div_10);
|
||||||
var text_4 = child(span, true);
|
var text_4 = child(span, true);
|
||||||
@ -6741,7 +6765,7 @@ delegate(["click"]);
|
|||||||
//#region src/components/EntryForm.svelte
|
//#region src/components/EntryForm.svelte
|
||||||
var root_1$4 = /* @__PURE__ */ from_html(`<div class="loading svelte-pafazm">Loading...</div>`);
|
var root_1$4 = /* @__PURE__ */ from_html(`<div class="loading svelte-pafazm">Loading...</div>`);
|
||||||
var root_3$2 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-pafazm"> </div>`);
|
var root_3$2 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-pafazm"> </div>`);
|
||||||
var root_5$1 = /* @__PURE__ */ from_html(`<div class="validation-error svelte-pafazm"> </div>`);
|
var root_5$2 = /* @__PURE__ */ from_html(`<div class="validation-error svelte-pafazm"> </div>`);
|
||||||
var root_4$2 = /* @__PURE__ */ from_html(`<div class="validation-errors svelte-pafazm"></div>`);
|
var root_4$2 = /* @__PURE__ */ from_html(`<div class="validation-errors svelte-pafazm"></div>`);
|
||||||
var root_7$2 = /* @__PURE__ */ from_html(`<option> </option>`);
|
var root_7$2 = /* @__PURE__ */ from_html(`<option> </option>`);
|
||||||
var root_2$2 = /* @__PURE__ */ from_html(`<!> <form class="form-card svelte-pafazm"><!> <div class="form-group"><label for="title">Title *</label> <input id="title" type="text" placeholder="e.g. GitHub, Gmail"/></div> <div class="form-group"><label for="username">Username / Email</label> <input id="username" type="text" placeholder="username or email"/></div> <div class="form-group"><label for="password">Password *</label> <div class="password-input-group svelte-pafazm"><input id="password" placeholder="Password" class="svelte-pafazm"/> <button type="button" class="btn btn-ghost btn-sm" title="Toggle visibility"> </button> <button type="button" class="btn btn-ghost btn-sm" title="Generate password">🎲</button></div></div> <div class="form-group"><label for="url">URL</label> <input id="url" type="url" placeholder="https://example.com"/></div> <div class="form-group"><label for="group">Group</label> <select id="group"><option>No group</option><!></select></div> <div class="form-group"><label for="notes">Notes</label> <textarea id="notes" placeholder="Any additional notes..."></textarea></div> <div class="form-actions svelte-pafazm"><button type="submit" class="btn btn-primary"> </button> <button type="button" class="btn btn-ghost">Cancel</button></div></form>`, 1);
|
var root_2$2 = /* @__PURE__ */ from_html(`<!> <form class="form-card svelte-pafazm"><!> <div class="form-group"><label for="title">Title *</label> <input id="title" type="text" placeholder="e.g. GitHub, Gmail"/></div> <div class="form-group"><label for="username">Username / Email</label> <input id="username" type="text" placeholder="username or email"/></div> <div class="form-group"><label for="password">Password *</label> <div class="password-input-group svelte-pafazm"><input id="password" placeholder="Password" class="svelte-pafazm"/> <button type="button" class="btn btn-ghost btn-sm" title="Toggle visibility"> </button> <button type="button" class="btn btn-ghost btn-sm" title="Generate password">🎲</button></div></div> <div class="form-group"><label for="url">URL</label> <input id="url" type="url" placeholder="https://example.com"/></div> <div class="form-group"><label for="group">Group</label> <select id="group"><option>No group</option><!></select></div> <div class="form-group"><label for="notes">Notes</label> <textarea id="notes" placeholder="Any additional notes..."></textarea></div> <div class="form-actions svelte-pafazm"><button type="submit" class="btn btn-primary"> </button> <button type="button" class="btn btn-ghost">Cancel</button></div></form>`, 1);
|
||||||
@ -6776,6 +6800,9 @@ function EntryForm($$anchor, $$props) {
|
|||||||
set(notes, entry.notes || "", true);
|
set(notes, entry.notes || "", true);
|
||||||
set(groupId, entry.groupId || "", true);
|
set(groupId, entry.groupId || "", true);
|
||||||
} else set(error, "Entry not found");
|
} else set(error, "Entry not found");
|
||||||
|
} else {
|
||||||
|
const active = search.activeGroupId;
|
||||||
|
set(groupId, active !== "all" && active !== "trash" ? active : "", true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
set(error, "Failed to load form: " + e.message);
|
set(error, "Failed to load form: " + e.message);
|
||||||
@ -6844,7 +6871,7 @@ function EntryForm($$anchor, $$props) {
|
|||||||
var consequent_2 = ($$anchor) => {
|
var consequent_2 = ($$anchor) => {
|
||||||
var div_3 = root_4$2();
|
var div_3 = root_4$2();
|
||||||
each(div_3, 21, () => get(formErrors), index, ($$anchor, err) => {
|
each(div_3, 21, () => get(formErrors), index, ($$anchor, err) => {
|
||||||
var div_4 = root_5$1();
|
var div_4 = root_5$2();
|
||||||
var text_1 = child(div_4);
|
var text_1 = child(div_4);
|
||||||
reset(div_4);
|
reset(div_4);
|
||||||
template_effect(() => set_text(text_1, `⚠ ${get(err) ?? ""}`));
|
template_effect(() => set_text(text_1, `⚠ ${get(err) ?? ""}`));
|
||||||
@ -6950,17 +6977,28 @@ function EntryForm($$anchor, $$props) {
|
|||||||
delegate(["click"]);
|
delegate(["click"]);
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region src/components/ImportExport.svelte
|
//#region src/components/ImportExport.svelte
|
||||||
var root_1$3 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-17di1i9" role="presentation"><div class="modal svelte-17di1i9" role="dialog" aria-modal="true" aria-label="Export vault" tabindex="-1"><h3 class="svelte-17di1i9">Export Vault</h3> <p class="svelte-17di1i9">All entries and groups will be exported. You'll need the source vault's master password when importing into another vault.</p> <div class="modal-actions svelte-17di1i9"><button class="btn btn-primary"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
var root_2$1 = /* @__PURE__ */ from_html(`<label class="checkbox-label group-checkbox svelte-17di1i9"><input type="checkbox" class="svelte-17di1i9"/> <span class="group-color-dot svelte-17di1i9"></span> <span class="group-name"> </span></label>`);
|
||||||
var root_3$1 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-17di1i9"> </div>`);
|
var root_1$3 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-17di1i9" role="presentation"><div class="modal svelte-17di1i9" role="dialog" aria-modal="true" aria-label="Export vault" tabindex="-1"><h3 class="svelte-17di1i9">Export Vault</h3> <p class="svelte-17di1i9">Select which groups to export. You'll need the source vault's master password when importing into another vault.</p> <div class="group-select-header svelte-17di1i9"><label class="checkbox-label svelte-17di1i9"><input type="checkbox" class="svelte-17di1i9"/> <span>Select all</span></label> <span class="entry-count svelte-17di1i9"> </span></div> <div class="group-select-list svelte-17di1i9"></div> <div class="modal-actions svelte-17di1i9"><button class="btn btn-primary"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
||||||
var root_4$1 = /* @__PURE__ */ from_html(`<div class="success-banner svelte-17di1i9"> <!></div>`);
|
var root_4$1 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-17di1i9"> </div>`);
|
||||||
var root_6$1 = /* @__PURE__ */ from_html(`<p class="svelte-17di1i9">File loaded. Enter the <strong>source vault's master password</strong> to decrypt and re-encrypt entries under your current vault.</p> <div class="form-group svelte-17di1i9"><label for="source-password" class="file-label svelte-17di1i9">Source vault password</label> <input id="source-password" type="password" placeholder="Enter source vault password" autocomplete="current-password" class="svelte-17di1i9"/></div> <div class="import-mode svelte-17di1i9"><label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Merge — add to existing data</span></label> <label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Replace — clear all existing data first</span></label></div> <div class="modal-actions svelte-17di1i9"><button class="btn btn-primary"> </button> <button class="btn btn-ghost">Cancel</button></div>`, 1);
|
var root_5$1 = /* @__PURE__ */ from_html(`<div class="success-banner svelte-17di1i9"> <!></div>`);
|
||||||
var root_7$1 = /* @__PURE__ */ from_html(`<p class="svelte-17di1i9">Select how to handle existing data:</p> <div class="import-mode svelte-17di1i9"><label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Merge — add to existing data</span></label> <label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Replace — clear all existing data first</span></label></div> <div class="form-group svelte-17di1i9"><label for="import-file" class="file-label svelte-17di1i9">Choose JSON file</label> <input id="import-file" type="file" accept=".json,application/json" class="svelte-17di1i9"/></div>`, 1);
|
var root_7$1 = /* @__PURE__ */ from_html(`<p class="svelte-17di1i9">File loaded. Enter the <strong>source vault's master password</strong> to decrypt and re-encrypt entries under your current vault.</p> <div class="form-group svelte-17di1i9"><label for="source-password" class="file-label svelte-17di1i9">Source vault password</label> <input id="source-password" type="password" placeholder="Enter source vault password" autocomplete="current-password" class="svelte-17di1i9"/></div> <div class="import-mode svelte-17di1i9"><label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Merge — add to existing data</span></label> <label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Replace — clear all existing data first</span></label></div> <div class="modal-actions svelte-17di1i9"><button class="btn btn-primary"> </button> <button class="btn btn-ghost">Cancel</button></div>`, 1);
|
||||||
var root_2$1 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-17di1i9" role="presentation"><div class="modal svelte-17di1i9" role="dialog" aria-modal="true" aria-label="Import vault data" tabindex="-1"><h3 class="svelte-17di1i9">Import Vault Data</h3> <!> <!> <div class="modal-actions svelte-17di1i9"><button class="btn btn-ghost">Close</button></div></div></div>`);
|
var root_8$1 = /* @__PURE__ */ from_html(`<p class="svelte-17di1i9">Select how to handle existing data:</p> <div class="import-mode svelte-17di1i9"><label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Merge — add to existing data</span></label> <label class="radio-label svelte-17di1i9"><input type="radio" name="importMode" class="svelte-17di1i9"/> <span class="svelte-17di1i9">Replace — clear all existing data first</span></label></div> <div class="form-group svelte-17di1i9"><label for="import-file" class="file-label svelte-17di1i9">Choose JSON file</label> <input id="import-file" type="file" accept=".json,application/json" class="svelte-17di1i9"/></div>`, 1);
|
||||||
|
var root_3$1 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-17di1i9" role="presentation"><div class="modal svelte-17di1i9" role="dialog" aria-modal="true" aria-label="Import vault data" tabindex="-1"><h3 class="svelte-17di1i9">Import Vault Data</h3> <!> <!> <div class="modal-actions svelte-17di1i9"><button class="btn btn-ghost">Close</button></div></div></div>`);
|
||||||
var root$2 = /* @__PURE__ */ from_html(`<div class="import-export"><button class="btn btn-ghost btn-sm" title="Export">📤 Export</button> <button class="btn btn-ghost btn-sm" title="Import">📥 Import</button> <!> <!></div>`);
|
var root$2 = /* @__PURE__ */ from_html(`<div class="import-export"><button class="btn btn-ghost btn-sm" title="Export">📤 Export</button> <button class="btn btn-ghost btn-sm" title="Import">📥 Import</button> <!> <!></div>`);
|
||||||
function ImportExport($$anchor, $$props) {
|
function ImportExport($$anchor, $$props) {
|
||||||
push($$props, true);
|
push($$props, true);
|
||||||
const binding_group = [];
|
const binding_group = [];
|
||||||
let showExport = /* @__PURE__ */ state(false);
|
let showExport = /* @__PURE__ */ state(false);
|
||||||
|
async function openExportModal() {
|
||||||
|
set(allGroups, [{
|
||||||
|
id: "",
|
||||||
|
name: "Ungrouped",
|
||||||
|
color: "#6b7280"
|
||||||
|
}, ...(await getGroups()).filter((g) => !isTrashGroup(g.id))], true);
|
||||||
|
set(allEntries, await getEntries(), true);
|
||||||
|
set(selectedGroupIds, get(allGroups).map((g) => g.id), true);
|
||||||
|
set(showExport, true);
|
||||||
|
}
|
||||||
let showImport = /* @__PURE__ */ state(false);
|
let showImport = /* @__PURE__ */ state(false);
|
||||||
let importMode = /* @__PURE__ */ state("merge");
|
let importMode = /* @__PURE__ */ state("merge");
|
||||||
let importResult = /* @__PURE__ */ state(null);
|
let importResult = /* @__PURE__ */ state(null);
|
||||||
@ -6970,10 +7008,15 @@ function ImportExport($$anchor, $$props) {
|
|||||||
let exporting = /* @__PURE__ */ state(false);
|
let exporting = /* @__PURE__ */ state(false);
|
||||||
let sourcePassword = /* @__PURE__ */ state("");
|
let sourcePassword = /* @__PURE__ */ state("");
|
||||||
let parsedFileData = /* @__PURE__ */ state(null);
|
let parsedFileData = /* @__PURE__ */ state(null);
|
||||||
|
let allGroups = /* @__PURE__ */ state(proxy([]));
|
||||||
|
let allEntries = /* @__PURE__ */ state(proxy([]));
|
||||||
|
let selectedGroupIds = /* @__PURE__ */ state(proxy([]));
|
||||||
|
let selectAll = /* @__PURE__ */ user_derived(() => get(allGroups).length > 0 && get(allGroups).every((g) => get(selectedGroupIds).includes(g.id)));
|
||||||
|
let exportEntryCount = /* @__PURE__ */ user_derived(() => get(allEntries).filter((e) => get(selectedGroupIds).includes(e.groupId)).length);
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
set(exporting, true);
|
set(exporting, true);
|
||||||
try {
|
try {
|
||||||
set(exportData, await exportAll(), true);
|
set(exportData, await exportSelected(get(selectedGroupIds).length === get(allGroups).length ? null : get(selectedGroupIds)), true);
|
||||||
const json = JSON.stringify(get(exportData), null, 2);
|
const json = JSON.stringify(get(exportData), null, 2);
|
||||||
const blob = new Blob([json], { type: "application/json" });
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@ -6988,6 +7031,14 @@ function ImportExport($$anchor, $$props) {
|
|||||||
}
|
}
|
||||||
set(exporting, false);
|
set(exporting, false);
|
||||||
}
|
}
|
||||||
|
function toggleSelectAll() {
|
||||||
|
if (get(selectAll)) set(selectedGroupIds, [], true);
|
||||||
|
else set(selectedGroupIds, get(allGroups).map((g) => g.id), true);
|
||||||
|
}
|
||||||
|
function toggleGroup(groupId) {
|
||||||
|
if (get(selectedGroupIds).includes(groupId)) set(selectedGroupIds, get(selectedGroupIds).filter((id) => id !== groupId), true);
|
||||||
|
else set(selectedGroupIds, [...get(selectedGroupIds), groupId], true);
|
||||||
|
}
|
||||||
async function handleFileSelect(event) {
|
async function handleFileSelect(event) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@ -7034,19 +7085,51 @@ function ImportExport($$anchor, $$props) {
|
|||||||
var div_1 = root_1$3();
|
var div_1 = root_1$3();
|
||||||
var div_2 = child(div_1);
|
var div_2 = child(div_1);
|
||||||
var div_3 = sibling(child(div_2), 4);
|
var div_3 = sibling(child(div_2), 4);
|
||||||
var button_2 = child(div_3);
|
var label = child(div_3);
|
||||||
var text_1 = child(button_2, true);
|
var input = child(label);
|
||||||
|
remove_input_defaults(input);
|
||||||
|
next(2);
|
||||||
|
reset(label);
|
||||||
|
var span = sibling(label, 2);
|
||||||
|
var text_1 = child(span);
|
||||||
|
reset(span);
|
||||||
|
reset(div_3);
|
||||||
|
var div_4 = sibling(div_3, 2);
|
||||||
|
each(div_4, 21, () => get(allGroups), index, ($$anchor, group) => {
|
||||||
|
var label_1 = root_2$1();
|
||||||
|
var input_1 = child(label_1);
|
||||||
|
remove_input_defaults(input_1);
|
||||||
|
var span_1 = sibling(input_1, 2);
|
||||||
|
var span_2 = sibling(span_1, 2);
|
||||||
|
var text_2 = child(span_2, true);
|
||||||
|
reset(span_2);
|
||||||
|
reset(label_1);
|
||||||
|
template_effect(($0) => {
|
||||||
|
set_checked(input_1, $0);
|
||||||
|
set_style(span_1, `background-color: ${(get(group).color || "#6c63ff") ?? ""}`);
|
||||||
|
set_text(text_2, get(group).name);
|
||||||
|
}, [() => get(selectedGroupIds).includes(get(group).id)]);
|
||||||
|
delegated("change", input_1, () => toggleGroup(get(group).id));
|
||||||
|
append($$anchor, label_1);
|
||||||
|
});
|
||||||
|
reset(div_4);
|
||||||
|
var div_5 = sibling(div_4, 2);
|
||||||
|
var button_2 = child(div_5);
|
||||||
|
var text_3 = child(button_2, true);
|
||||||
reset(button_2);
|
reset(button_2);
|
||||||
var button_3 = sibling(button_2, 2);
|
var button_3 = sibling(button_2, 2);
|
||||||
reset(div_3);
|
reset(div_5);
|
||||||
reset(div_2);
|
reset(div_2);
|
||||||
reset(div_1);
|
reset(div_1);
|
||||||
template_effect(() => {
|
template_effect(() => {
|
||||||
button_2.disabled = get(exporting);
|
set_checked(input, get(selectAll));
|
||||||
set_text(text_1, get(exporting) ? "Exporting..." : "📤 Export JSON");
|
set_text(text_1, `${get(exportEntryCount) ?? ""} entries`);
|
||||||
|
button_2.disabled = get(exporting) || get(selectedGroupIds).length === 0;
|
||||||
|
set_text(text_3, get(exporting) ? "Exporting..." : "📤 Export JSON");
|
||||||
});
|
});
|
||||||
delegated("click", div_1, () => set(showExport, false));
|
delegated("click", div_1, () => set(showExport, false));
|
||||||
delegated("click", div_2, (e) => e.stopPropagation());
|
delegated("click", div_2, (e) => e.stopPropagation());
|
||||||
|
delegated("change", input, toggleSelectAll);
|
||||||
delegated("click", button_2, handleExport);
|
delegated("click", button_2, handleExport);
|
||||||
delegated("click", button_3, () => set(showExport, false));
|
delegated("click", button_3, () => set(showExport, false));
|
||||||
append($$anchor, div_1);
|
append($$anchor, div_1);
|
||||||
@ -7056,79 +7139,43 @@ function ImportExport($$anchor, $$props) {
|
|||||||
});
|
});
|
||||||
var node_1 = sibling(node, 2);
|
var node_1 = sibling(node, 2);
|
||||||
var consequent_5 = ($$anchor) => {
|
var consequent_5 = ($$anchor) => {
|
||||||
var div_4 = root_2$1();
|
var div_6 = root_3$1();
|
||||||
var div_5 = child(div_4);
|
var div_7 = child(div_6);
|
||||||
var node_2 = sibling(child(div_5), 2);
|
var node_2 = sibling(child(div_7), 2);
|
||||||
var consequent_1 = ($$anchor) => {
|
var consequent_1 = ($$anchor) => {
|
||||||
var div_6 = root_3$1();
|
var div_8 = root_4$1();
|
||||||
var text_2 = child(div_6, true);
|
var text_4 = child(div_8, true);
|
||||||
reset(div_6);
|
reset(div_8);
|
||||||
template_effect(() => set_text(text_2, get(importError)));
|
template_effect(() => set_text(text_4, get(importError)));
|
||||||
append($$anchor, div_6);
|
append($$anchor, div_8);
|
||||||
};
|
};
|
||||||
if_block(node_2, ($$render) => {
|
if_block(node_2, ($$render) => {
|
||||||
if (get(importError)) $$render(consequent_1);
|
if (get(importError)) $$render(consequent_1);
|
||||||
});
|
});
|
||||||
var node_3 = sibling(node_2, 2);
|
var node_3 = sibling(node_2, 2);
|
||||||
var consequent_3 = ($$anchor) => {
|
var consequent_3 = ($$anchor) => {
|
||||||
var div_7 = root_4$1();
|
var div_9 = root_5$1();
|
||||||
var text_3 = child(div_7);
|
var text_5 = child(div_9);
|
||||||
var node_4 = sibling(text_3);
|
var node_4 = sibling(text_5);
|
||||||
var consequent_2 = ($$anchor) => {
|
var consequent_2 = ($$anchor) => {
|
||||||
var text_4 = text();
|
var text_6 = text();
|
||||||
template_effect(() => set_text(text_4, `(${get(importResult).skipped ?? ""} skipped)`));
|
template_effect(() => set_text(text_6, `(${get(importResult).skipped ?? ""} skipped)`));
|
||||||
append($$anchor, text_4);
|
append($$anchor, text_6);
|
||||||
};
|
};
|
||||||
if_block(node_4, ($$render) => {
|
if_block(node_4, ($$render) => {
|
||||||
if (get(importResult).skipped > 0) $$render(consequent_2);
|
if (get(importResult).skipped > 0) $$render(consequent_2);
|
||||||
});
|
});
|
||||||
reset(div_7);
|
reset(div_9);
|
||||||
template_effect(() => set_text(text_3, `✓ Imported ${get(importResult).imported.entries ?? ""} entries and ${get(importResult).imported.groups ?? ""} groups `));
|
template_effect(() => set_text(text_5, `✓ Imported ${get(importResult).imported.entries ?? ""} entries and ${get(importResult).imported.groups ?? ""} groups `));
|
||||||
append($$anchor, div_7);
|
append($$anchor, div_9);
|
||||||
};
|
};
|
||||||
var consequent_4 = ($$anchor) => {
|
var consequent_4 = ($$anchor) => {
|
||||||
var fragment_1 = root_6$1();
|
var fragment_1 = root_7$1();
|
||||||
var div_8 = sibling(first_child(fragment_1), 2);
|
var div_10 = sibling(first_child(fragment_1), 2);
|
||||||
var input = sibling(child(div_8), 2);
|
var input_2 = sibling(child(div_10), 2);
|
||||||
remove_input_defaults(input);
|
|
||||||
reset(div_8);
|
|
||||||
var div_9 = sibling(div_8, 2);
|
|
||||||
var label = child(div_9);
|
|
||||||
var input_1 = child(label);
|
|
||||||
remove_input_defaults(input_1);
|
|
||||||
input_1.value = input_1.__value = "merge";
|
|
||||||
next(2);
|
|
||||||
reset(label);
|
|
||||||
var label_1 = sibling(label, 2);
|
|
||||||
var input_2 = child(label_1);
|
|
||||||
remove_input_defaults(input_2);
|
remove_input_defaults(input_2);
|
||||||
input_2.value = input_2.__value = "replace";
|
|
||||||
next(2);
|
|
||||||
reset(label_1);
|
|
||||||
reset(div_9);
|
|
||||||
var div_10 = sibling(div_9, 2);
|
|
||||||
var button_4 = child(div_10);
|
|
||||||
var text_5 = child(button_4, true);
|
|
||||||
reset(button_4);
|
|
||||||
var button_5 = sibling(button_4, 2);
|
|
||||||
reset(div_10);
|
reset(div_10);
|
||||||
template_effect(() => {
|
var div_11 = sibling(div_10, 2);
|
||||||
button_4.disabled = get(importing);
|
|
||||||
set_text(text_5, get(importing) ? "Importing..." : "📥 Import");
|
|
||||||
});
|
|
||||||
bind_value(input, () => get(sourcePassword), ($$value) => set(sourcePassword, $$value));
|
|
||||||
bind_group(binding_group, [], input_1, () => get(importMode), ($$value) => set(importMode, $$value));
|
|
||||||
bind_group(binding_group, [], input_2, () => get(importMode), ($$value) => set(importMode, $$value));
|
|
||||||
delegated("click", button_4, handleImportSubmit);
|
|
||||||
delegated("click", button_5, () => {
|
|
||||||
set(parsedFileData, null);
|
|
||||||
set(sourcePassword, "");
|
|
||||||
});
|
|
||||||
append($$anchor, fragment_1);
|
|
||||||
};
|
|
||||||
var alternate = ($$anchor) => {
|
|
||||||
var fragment_2 = root_7$1();
|
|
||||||
var div_11 = sibling(first_child(fragment_2), 2);
|
|
||||||
var label_2 = child(div_11);
|
var label_2 = child(div_11);
|
||||||
var input_3 = child(label_2);
|
var input_3 = child(label_2);
|
||||||
remove_input_defaults(input_3);
|
remove_input_defaults(input_3);
|
||||||
@ -7143,12 +7190,48 @@ function ImportExport($$anchor, $$props) {
|
|||||||
reset(label_3);
|
reset(label_3);
|
||||||
reset(div_11);
|
reset(div_11);
|
||||||
var div_12 = sibling(div_11, 2);
|
var div_12 = sibling(div_11, 2);
|
||||||
var input_5 = sibling(child(div_12), 2);
|
var button_4 = child(div_12);
|
||||||
|
var text_7 = child(button_4, true);
|
||||||
|
reset(button_4);
|
||||||
|
var button_5 = sibling(button_4, 2);
|
||||||
reset(div_12);
|
reset(div_12);
|
||||||
template_effect(() => input_5.disabled = get(importing));
|
template_effect(() => {
|
||||||
|
button_4.disabled = get(importing);
|
||||||
|
set_text(text_7, get(importing) ? "Importing..." : "📥 Import");
|
||||||
|
});
|
||||||
|
bind_value(input_2, () => get(sourcePassword), ($$value) => set(sourcePassword, $$value));
|
||||||
bind_group(binding_group, [], input_3, () => get(importMode), ($$value) => set(importMode, $$value));
|
bind_group(binding_group, [], input_3, () => get(importMode), ($$value) => set(importMode, $$value));
|
||||||
bind_group(binding_group, [], input_4, () => get(importMode), ($$value) => set(importMode, $$value));
|
bind_group(binding_group, [], input_4, () => get(importMode), ($$value) => set(importMode, $$value));
|
||||||
delegated("change", input_5, handleFileSelect);
|
delegated("click", button_4, handleImportSubmit);
|
||||||
|
delegated("click", button_5, () => {
|
||||||
|
set(parsedFileData, null);
|
||||||
|
set(sourcePassword, "");
|
||||||
|
});
|
||||||
|
append($$anchor, fragment_1);
|
||||||
|
};
|
||||||
|
var alternate = ($$anchor) => {
|
||||||
|
var fragment_2 = root_8$1();
|
||||||
|
var div_13 = sibling(first_child(fragment_2), 2);
|
||||||
|
var label_4 = child(div_13);
|
||||||
|
var input_5 = child(label_4);
|
||||||
|
remove_input_defaults(input_5);
|
||||||
|
input_5.value = input_5.__value = "merge";
|
||||||
|
next(2);
|
||||||
|
reset(label_4);
|
||||||
|
var label_5 = sibling(label_4, 2);
|
||||||
|
var input_6 = child(label_5);
|
||||||
|
remove_input_defaults(input_6);
|
||||||
|
input_6.value = input_6.__value = "replace";
|
||||||
|
next(2);
|
||||||
|
reset(label_5);
|
||||||
|
reset(div_13);
|
||||||
|
var div_14 = sibling(div_13, 2);
|
||||||
|
var input_7 = sibling(child(div_14), 2);
|
||||||
|
reset(div_14);
|
||||||
|
template_effect(() => input_7.disabled = get(importing));
|
||||||
|
bind_group(binding_group, [], input_5, () => get(importMode), ($$value) => set(importMode, $$value));
|
||||||
|
bind_group(binding_group, [], input_6, () => get(importMode), ($$value) => set(importMode, $$value));
|
||||||
|
delegated("change", input_7, handleFileSelect);
|
||||||
append($$anchor, fragment_2);
|
append($$anchor, fragment_2);
|
||||||
};
|
};
|
||||||
if_block(node_3, ($$render) => {
|
if_block(node_3, ($$render) => {
|
||||||
@ -7156,25 +7239,25 @@ function ImportExport($$anchor, $$props) {
|
|||||||
else if (get(parsedFileData)) $$render(consequent_4, 1);
|
else if (get(parsedFileData)) $$render(consequent_4, 1);
|
||||||
else $$render(alternate, -1);
|
else $$render(alternate, -1);
|
||||||
});
|
});
|
||||||
var div_13 = sibling(node_3, 2);
|
var div_15 = sibling(node_3, 2);
|
||||||
var button_6 = child(div_13);
|
var button_6 = child(div_15);
|
||||||
reset(div_13);
|
reset(div_15);
|
||||||
reset(div_5);
|
reset(div_7);
|
||||||
reset(div_4);
|
reset(div_6);
|
||||||
delegated("click", div_4, () => set(showImport, false));
|
delegated("click", div_6, () => set(showImport, false));
|
||||||
delegated("click", div_5, (e) => e.stopPropagation());
|
delegated("click", div_7, (e) => e.stopPropagation());
|
||||||
delegated("click", button_6, () => {
|
delegated("click", button_6, () => {
|
||||||
set(showImport, false);
|
set(showImport, false);
|
||||||
set(importResult, null);
|
set(importResult, null);
|
||||||
set(importError, "");
|
set(importError, "");
|
||||||
});
|
});
|
||||||
append($$anchor, div_4);
|
append($$anchor, div_6);
|
||||||
};
|
};
|
||||||
if_block(node_1, ($$render) => {
|
if_block(node_1, ($$render) => {
|
||||||
if (get(showImport)) $$render(consequent_5);
|
if (get(showImport)) $$render(consequent_5);
|
||||||
});
|
});
|
||||||
reset(div);
|
reset(div);
|
||||||
delegated("click", button, () => set(showExport, true));
|
delegated("click", button, openExportModal);
|
||||||
delegated("click", button_1, () => set(showImport, true));
|
delegated("click", button_1, () => set(showImport, true));
|
||||||
append($$anchor, div);
|
append($$anchor, div);
|
||||||
pop();
|
pop();
|
||||||
@ -8434,6 +8517,70 @@ label {
|
|||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Group selection for export */
|
||||||
|
.group-select-header.svelte-17di1i9 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-header.svelte-17di1i9 .entry-count:where(.svelte-17di1i9) {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list.svelte-17di1i9 {
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list.svelte-17di1i9::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list.svelte-17di1i9::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list.svelte-17di1i9::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label.svelte-17di1i9 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label.svelte-17di1i9 input[type="checkbox"]:where(.svelte-17di1i9) {
|
||||||
|
accent-color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-checkbox.svelte-17di1i9 {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: background-color 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-checkbox.svelte-17di1i9:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-color-dot.svelte-17di1i9 {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-panel.svelte-1koizbb {
|
.settings-panel.svelte-1koizbb {
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
import { createEntry, updateEntry as updateEntryModel, validateEntry, isTrashGroup } from '../lib/models/schema.js'
|
import { createEntry, updateEntry as updateEntryModel, validateEntry, isTrashGroup } from '../lib/models/schema.js'
|
||||||
import { generatePassword } from '../lib/crypto/crypto.js'
|
import { generatePassword } from '../lib/crypto/crypto.js'
|
||||||
import { app } from '../lib/stores/app.svelte.js'
|
import { app } from '../lib/stores/app.svelte.js'
|
||||||
|
import { search as searchStore } from '../lib/stores/search.svelte.js'
|
||||||
import { autofocus } from '../lib/autofocus.js'
|
import { autofocus } from '../lib/autofocus.js'
|
||||||
|
|
||||||
let { entryId, onSave, onCancel } = $props()
|
let { entryId, onSave, onCancel } = $props()
|
||||||
@ -39,6 +40,10 @@
|
|||||||
} else {
|
} else {
|
||||||
error = 'Entry not found'
|
error = 'Entry not found'
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Default to the currently active group if it's a real group
|
||||||
|
const active = searchStore.activeGroupId
|
||||||
|
groupId = (active !== 'all' && active !== 'trash') ? active : ''
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = 'Failed to load form: ' + e.message
|
error = 'Failed to load form: ' + e.message
|
||||||
|
|||||||
@ -1,9 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import { exportAll, importAll } from '../lib/storage/db.js'
|
import { exportSelected, importAll, getGroups, getEntries } from '../lib/storage/db.js'
|
||||||
import { search as searchStore } from '../lib/stores/search.svelte.js'
|
import { search as searchStore } from '../lib/stores/search.svelte.js'
|
||||||
import { app } from '../lib/stores/app.svelte.js'
|
import { app } from '../lib/stores/app.svelte.js'
|
||||||
|
import { isTrashGroup } from '../lib/models/schema.js'
|
||||||
|
|
||||||
let showExport = $state(false)
|
let showExport = $state(false)
|
||||||
|
|
||||||
|
async function openExportModal() {
|
||||||
|
const groups = (await getGroups()).filter(g => !isTrashGroup(g.id))
|
||||||
|
allGroups = [{ id: '', name: 'Ungrouped', color: '#6b7280' }, ...groups]
|
||||||
|
allEntries = await getEntries()
|
||||||
|
selectedGroupIds = allGroups.map(g => g.id)
|
||||||
|
showExport = true
|
||||||
|
}
|
||||||
let showImport = $state(false)
|
let showImport = $state(false)
|
||||||
let importMode = $state('merge') // 'merge' or 'replace'
|
let importMode = $state('merge') // 'merge' or 'replace'
|
||||||
let importResult = $state(null)
|
let importResult = $state(null)
|
||||||
@ -14,10 +23,21 @@
|
|||||||
let sourcePassword = $state('')
|
let sourcePassword = $state('')
|
||||||
let parsedFileData = $state(null)
|
let parsedFileData = $state(null)
|
||||||
|
|
||||||
|
// Group selection for export
|
||||||
|
let allGroups = $state([])
|
||||||
|
let allEntries = $state([])
|
||||||
|
let selectedGroupIds = $state([])
|
||||||
|
let selectAll = $derived(
|
||||||
|
allGroups.length > 0 && allGroups.every(g => selectedGroupIds.includes(g.id))
|
||||||
|
)
|
||||||
|
let exportEntryCount = $derived(
|
||||||
|
allEntries.filter(e => selectedGroupIds.includes(e.groupId)).length
|
||||||
|
)
|
||||||
|
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
exporting = true
|
exporting = true
|
||||||
try {
|
try {
|
||||||
exportData = await exportAll()
|
exportData = await exportSelected(selectedGroupIds.length === allGroups.length ? null : selectedGroupIds)
|
||||||
const json = JSON.stringify(exportData, null, 2)
|
const json = JSON.stringify(exportData, null, 2)
|
||||||
const blob = new Blob([json], { type: 'application/json' })
|
const blob = new Blob([json], { type: 'application/json' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
@ -33,6 +53,22 @@
|
|||||||
exporting = false
|
exporting = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll() {
|
||||||
|
if (selectAll) {
|
||||||
|
selectedGroupIds = []
|
||||||
|
} else {
|
||||||
|
selectedGroupIds = allGroups.map(g => g.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleGroup(groupId) {
|
||||||
|
if (selectedGroupIds.includes(groupId)) {
|
||||||
|
selectedGroupIds = selectedGroupIds.filter(id => id !== groupId)
|
||||||
|
} else {
|
||||||
|
selectedGroupIds = [...selectedGroupIds, groupId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleFileSelect(event) {
|
async function handleFileSelect(event) {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0]
|
||||||
if (!file) return
|
if (!file) return
|
||||||
@ -86,7 +122,7 @@
|
|||||||
|
|
||||||
<div class="import-export">
|
<div class="import-export">
|
||||||
<!-- Export button -->
|
<!-- Export button -->
|
||||||
<button class="btn btn-ghost btn-sm" onclick={() => showExport = true} title="Export">
|
<button class="btn btn-ghost btn-sm" onclick={openExportModal} title="Export">
|
||||||
📤 Export
|
📤 Export
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -101,9 +137,32 @@
|
|||||||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||||
<div class="modal" role="dialog" aria-modal="true" aria-label="Export vault" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
<div class="modal" role="dialog" aria-modal="true" aria-label="Export vault" tabindex="-1" onclick={(e) => e.stopPropagation()}>
|
||||||
<h3>Export Vault</h3>
|
<h3>Export Vault</h3>
|
||||||
<p>All entries and groups will be exported. You'll need the source vault's master password when importing into another vault.</p>
|
<p>Select which groups to export. You'll need the source vault's master password when importing into another vault.</p>
|
||||||
|
|
||||||
|
<div class="group-select-header">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" checked={selectAll} onchange={toggleSelectAll} />
|
||||||
|
<span>Select all</span>
|
||||||
|
</label>
|
||||||
|
<span class="entry-count">{exportEntryCount} entries</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group-select-list">
|
||||||
|
{#each allGroups as group}
|
||||||
|
<label class="checkbox-label group-checkbox">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedGroupIds.includes(group.id)}
|
||||||
|
onchange={() => toggleGroup(group.id)}
|
||||||
|
/>
|
||||||
|
<span class="group-color-dot" style="background-color: {group.color || '#6c63ff'}"></span>
|
||||||
|
<span class="group-name">{group.name}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-primary" onclick={handleExport} disabled={exporting}>
|
<button class="btn btn-primary" onclick={handleExport} disabled={exporting || selectedGroupIds.length === 0}>
|
||||||
{exporting ? 'Exporting...' : '📤 Export JSON'}
|
{exporting ? 'Exporting...' : '📤 Export JSON'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-ghost" onclick={() => showExport = false}>Cancel</button>
|
<button class="btn btn-ghost" onclick={() => showExport = false}>Cancel</button>
|
||||||
@ -320,4 +379,68 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Group selection for export */
|
||||||
|
.group-select-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-header .entry-count {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list {
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-select-list::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
accent-color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-checkbox {
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: background-color 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-checkbox:hover {
|
||||||
|
background: var(--color-surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-color-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -376,22 +376,42 @@ export async function moveEntryToGroup(entryId, groupId) {
|
|||||||
// ========================
|
// ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export all data (entries + groups + meta) as a JSON object.
|
* Export data (entries + groups + meta) as a JSON object.
|
||||||
* Entries remain encrypted with the source vault's key. The import function
|
* Entries remain encrypted with the source vault's key. The import function
|
||||||
* requires the source vault's master password to decrypt and re-encrypt
|
* requires the source vault's master password to decrypt and re-encrypt
|
||||||
* entries under the target vault's key.
|
* entries under the target vault's key.
|
||||||
*
|
*
|
||||||
|
* @param {string[]} [groupIds] - Array of group IDs to export. If null/empty, exports everything.
|
||||||
|
* Include '' to export ungrouped entries.
|
||||||
* @returns {Promise<Object>}
|
* @returns {Promise<Object>}
|
||||||
*/
|
*/
|
||||||
export async function exportAll() {
|
export async function exportSelected(groupIds) {
|
||||||
const db = await getDb()
|
const db = await getDb()
|
||||||
const entries = await db.getAll('entries')
|
const allEntries = await db.getAll('entries')
|
||||||
const groups = await db.getAll('groups')
|
const allGroups = await db.getAll('groups')
|
||||||
|
|
||||||
const saltRow = await db.get('meta', 'salt')
|
const saltRow = await db.get('meta', 'salt')
|
||||||
const testEncryptedRow = await db.get('meta', 'testEncrypted')
|
const testEncryptedRow = await db.get('meta', 'testEncrypted')
|
||||||
const testPlaintextRow = await db.get('meta', 'testPlaintext')
|
const testPlaintextRow = await db.get('meta', 'testPlaintext')
|
||||||
|
|
||||||
|
// If no groups selected, export everything
|
||||||
|
if (!groupIds || groupIds.length === 0) {
|
||||||
|
return {
|
||||||
|
version: DB_VERSION,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
meta: {
|
||||||
|
salt: saltRow?.value || null,
|
||||||
|
testEncrypted: testEncryptedRow?.value || null,
|
||||||
|
testPlaintext: testPlaintextRow?.value || null,
|
||||||
|
},
|
||||||
|
groups: allGroups,
|
||||||
|
entries: allEntries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = allEntries.filter(e => groupIds.includes(e.groupId))
|
||||||
|
const groups = allGroups.filter(g => groupIds.includes(g.id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: DB_VERSION,
|
version: DB_VERSION,
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
moveToTrash,
|
moveToTrash,
|
||||||
emptyTrash,
|
emptyTrash,
|
||||||
restoreEntry,
|
restoreEntry,
|
||||||
exportAll,
|
exportSelected,
|
||||||
importAll,
|
importAll,
|
||||||
TRASH_GROUP_ID,
|
TRASH_GROUP_ID,
|
||||||
} from '../../../src/lib/storage/db.js'
|
} from '../../../src/lib/storage/db.js'
|
||||||
@ -446,7 +446,7 @@ describe('Export / Import', () => {
|
|||||||
})
|
})
|
||||||
await addEntry(entry)
|
await addEntry(entry)
|
||||||
|
|
||||||
exportData = await exportAll()
|
exportData = await exportSelected(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should export all data', async () => {
|
it('should export all data', async () => {
|
||||||
@ -461,6 +461,56 @@ describe('Export / Import', () => {
|
|||||||
expect(exportData.entries[0].title).toBe('GitHub')
|
expect(exportData.entries[0].title).toBe('GitHub')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should export with specific group IDs', async () => {
|
||||||
|
const group2 = createGroup('Personal')
|
||||||
|
await addGroup(group2)
|
||||||
|
|
||||||
|
const enc = await encrypt('personal-secret', await deriveKey('test', generateSalt()))
|
||||||
|
await addEntry(createEntry({
|
||||||
|
title: 'Netflix',
|
||||||
|
encryptedPassword: enc,
|
||||||
|
groupId: group2.id,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Export only the Work group
|
||||||
|
const workGroupId = exportData.groups.find(g => g.name === 'Work').id
|
||||||
|
const workOnly = await exportSelected([workGroupId])
|
||||||
|
expect(workOnly.groups).toHaveLength(1)
|
||||||
|
expect(workOnly.groups[0].name).toBe('Work')
|
||||||
|
expect(workOnly.entries).toHaveLength(1)
|
||||||
|
expect(workOnly.entries[0].title).toBe('GitHub')
|
||||||
|
|
||||||
|
// Export both groups
|
||||||
|
const allGroups = await getGroups()
|
||||||
|
const bothGroupIds = [workGroupId, group2.id]
|
||||||
|
const both = await exportSelected(bothGroupIds)
|
||||||
|
expect(both.entries).toHaveLength(2)
|
||||||
|
expect(both.groups).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should export with empty array (full export)', async () => {
|
||||||
|
const emptyExport = await exportSelected([])
|
||||||
|
expect(emptyExport.entries).toHaveLength(1)
|
||||||
|
const nonTrashGroups = emptyExport.groups.filter(g => g.id !== TRASH_GROUP_ID)
|
||||||
|
expect(nonTrashGroups).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should export only ungrouped entries with empty string groupId', async () => {
|
||||||
|
const enc = await encrypt('ungrouped-secret', await deriveKey('test', generateSalt()))
|
||||||
|
await addEntry(createEntry({
|
||||||
|
title: 'Ungrouped Entry',
|
||||||
|
encryptedPassword: enc,
|
||||||
|
groupId: '',
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ungroupedExport = await exportSelected([''])
|
||||||
|
const ungroupedEntries = ungroupedExport.entries.filter(e => e.groupId === '')
|
||||||
|
expect(ungroupedEntries).toHaveLength(1)
|
||||||
|
expect(ungroupedEntries[0].title).toBe('Ungrouped Entry')
|
||||||
|
// Should not include the Work group's entry
|
||||||
|
expect(ungroupedExport.entries).not.toEqual(expect.arrayContaining([expect.objectContaining({ title: 'GitHub' })]))
|
||||||
|
})
|
||||||
|
|
||||||
it('should import with merge mode', async () => {
|
it('should import with merge mode', async () => {
|
||||||
await clearAllData()
|
await clearAllData()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user