diff --git a/package-lock.json b/package-lock.json index 1ddd3ae..64d0849 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "quickmove", - "version": "2.5.0", + "version": "2.6.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 7324b8a..9da515d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "quickmove", "description": "Quick Folder Move", - "version": "2.5.0", + "version": "2.6.0", "private": true, "author": "Philipp Kewisch ", "license": "MPL-2.0", diff --git a/src/background.js b/src/background.js index 6215943..cf33a12 100644 --- a/src/background.js +++ b/src/background.js @@ -63,6 +63,23 @@ async function applyTags(tag) { await Promise.all(ops); } +async function updateSpecificFolders(origFolder, newFolder) { + let { defaultFolders } = await browser.storage.local.get({ defaultFolders: [] }); + + let foundFolder = defaultFolders.find((folder) => { + return folder.accountId == origFolder.accountId && folder.path == origFolder.path; + }); + + if (foundFolder) { + foundFolder.accountId = newFolder.accountId; + foundFolder.path = newFolder.path; + await browser.storage.local.set({ defaultFolders }); + } +} + +browser.folders.onRenamed.addListener(updateSpecificFolders); +browser.folders.onMoved.addListener(updateSpecificFolders); + browser.runtime.onInstalled.addListener(({ reason, previousVersion }) => { if (previousVersion && previousVersion.startsWith("1.")) { browser.quickmove.migrateShortcut(); diff --git a/src/popup/foldernode.js b/src/common/foldernode.js similarity index 98% rename from src/popup/foldernode.js rename to src/common/foldernode.js index 17e61d6..9c78eae 100644 --- a/src/popup/foldernode.js +++ b/src/common/foldernode.js @@ -93,7 +93,7 @@ export class FolderNode extends BaseNode { let parts = path.split("/"); let node = this; // eslint-disable-line consistent-this for (let part of parts) { - node = node.child(part); + node = node.child(part, create); } return node; } diff --git a/src/manifest.json b/src/manifest.json index 07cdbfc..da7c2c5 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extensionName__", "description": "__MSG_extensionDescription__", - "version": "2.5.0", + "version": "2.6.0", "default_locale": "en", "author": "Philipp Kewisch", "browser_specific_settings": { diff --git a/src/options/options.css b/src/options/options.css index 92ca580..158eeb5 100644 --- a/src/options/options.css +++ b/src/options/options.css @@ -3,12 +3,31 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch */ +@media (prefers-color-scheme: light) { + :root { + --menu-color: #18181b; + --menu-background-color: white; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --menu-color: #f4f4f5; + --menu-background-color: #18181b; + } +} + * { box-sizing: border-box; margin: 0; padding: 0; } +html { + margin: 0; + padding: 0; +} + body { font-family: sans-serif; padding: 20px; @@ -17,6 +36,8 @@ body { grid-template-columns: auto 1fr; align-items: center; gap: 1em 0.4em; + background-color: var(--menu-background-color); + color: var(--menu-color); } .preference { @@ -63,3 +84,44 @@ select { border-radius: 5px; background: #fff; } + +folder-list { + max-height: 20em; +} +fieldset { + width: 100%; + min-height: 25em; + grid-column: span 2; + padding: 10px; +} + +.overflow { + position: relative; + height: 3em; + width: 100%; +} + +.overflow folder-list { + position: absolute; + top: 0; + left: 0; + width: 100%; +} + +#folder-picker { + width: 100%; + background-color: var(--menu-background-color); +} + + +.panel { + display: none; + margin-top: 10px; +} +.panel.selected { + display: block; +} + +#defaultFolderSetting label { + margin-inline-end: 10px; +} diff --git a/src/options/options.html b/src/options/options.html index df29cd2..71ebdb0 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -7,8 +7,9 @@ - + +
@@ -35,15 +36,34 @@
-
- - -
+
+ Default Folders +
+ + + + + + +
+
+
+ +
+ +
+
+ + +
+
+
+
diff --git a/src/options/options.js b/src/options/options.js index 57dac78..517252d 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -3,14 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch */ -const DEFAULT_PREFERENCES = { - layout: "auto", - markAsRead: true, - maxRecentFolders: 15, - showFolderPath: false, - useLegacyShortcuts: false, - skipArchive: true -}; +import { AccountNode } from "../common/foldernode.js"; +import { DEFAULT_PREFERENCES, getValidatedDefaultFolders } from "../common/util.js"; async function restore_options() { let prefs = await browser.storage.local.get(DEFAULT_PREFERENCES); @@ -21,11 +15,11 @@ async function restore_options() { continue; } - if (elem.type == "checkbox") { - elem.checked = prefs[key]; - } else if (elem.getAttribute("type") == "radio") { - let item = document.querySelector(`input[type='radio'][name='${elem.id}'][value='${prefs[key]}']`); + if (!elem.type && elem.dataset.type == "radio") { + let item = document.querySelector(`input[type='radio'][name='${key}'][value='${prefs[key]}']`); item.checked = true; + } else if (elem.type == "checkbox") { + elem.checked = prefs[key]; } else { elem.value = prefs[key]; } @@ -76,6 +70,55 @@ function setup_localization() { } } -document.addEventListener("DOMContentLoaded", setup_localization); -document.addEventListener("DOMContentLoaded", setup_listeners); -document.addEventListener("DOMContentLoaded", restore_options); +async function setup_defaultfolders() { + let accounts = await browser.accounts.list(); + let accountNodes = accounts.map(account => new AccountNode(account, skipArchive)); + let folders = accountNodes.reduce((acc, node) => acc.concat([...node]), []); + let defaultFolders = await getValidatedDefaultFolders(accountNodes); + + let defaultFolderList = document.getElementById("default-folders"); + defaultFolderList.accounts = accounts; + defaultFolderList.initItems(defaultFolders, null, true); + + let defaultFolderSet = new Set(defaultFolders); + + let folderPicker = document.getElementById("folder-picker"); + folderPicker.accounts = accounts; + folderPicker.initItems(folders, [], true); + + folderPicker.addEventListener("item-selected", (event) => { + defaultFolderSet.add(event.detail); + let allItems = [...defaultFolderSet]; + + defaultFolderList.allItems = allItems; + folderPicker.searchValue = ""; + + let storageData = allItems.map(item => ({ accountId: item.accountId, path: item.path })); + browser.storage.local.set({ defaultFolders: storageData }); + }); + + defaultFolderList.addEventListener("item-deleted", (event) => { + defaultFolderSet.delete(event.detail); + let allItems = [...defaultFolderSet]; + + defaultFolderList.allItems = allItems; + + let storageData = allItems.map(item => ({ accountId: item.accountId, path: item.path })); + browser.storage.local.set({ defaultFolders: storageData }); + }); + + document.getElementById("defaultFolderSetting").addEventListener("change", (event) => { + document.querySelector(".panel.selected")?.classList.remove("selected"); + document.querySelector(`.panel[data-value="${event.target.value}"]`).classList.add("selected"); + }); + + let currentValue = document.querySelector("#defaultFolderSetting input:checked")?.value; + if (currentValue) { + document.querySelector(`.panel[data-value="${currentValue}"]`).classList.add("selected"); + } +} + +document.addEventListener("DOMContentLoaded", setup_localization, { once: true }); +document.addEventListener("DOMContentLoaded", setup_listeners, { once: true }); +document.addEventListener("DOMContentLoaded", restore_options, { once: true }); +document.addEventListener("DOMContentLoaded", setup_defaultfolders, { once: true }); diff --git a/src/popup/baseitemlist.js b/src/popup/baseitemlist.js index f5a19a0..79439ce 100644 --- a/src/popup/baseitemlist.js +++ b/src/popup/baseitemlist.js @@ -55,6 +55,22 @@ export default class BaseItemList extends HTMLElement { border-color: #7AACFE; } + :host(:not([delete])) .item > .delete { + display: none; + } + + :host([popup]) .list-body { + box-shadow: 0px 2px 8px 0px rgba(0,0,0,0.75); + margin: 0 10px 10px; + } + + @media (prefers-color-scheme: dark) { + :host([popup]) .list-body { + box-shadow: 0px 2px 8px 0px rgba(179, 179, 179, 0.6); + } + } + + .search-header { display: flex; flex-direction: column; @@ -70,7 +86,6 @@ export default class BaseItemList extends HTMLElement { display: none; } - .search-input { background-color: var(--item-list-background); color: var(--item-list-color); @@ -86,8 +101,12 @@ export default class BaseItemList extends HTMLElement { .list-body { display: flex; flex-direction: column; - padding: 5px 0; + padding: 2px; overflow-y: auto; + border-radius: 3px; + } + .list-body:-moz-only-whitespace { + display: none; } .list-body:focus { @@ -142,6 +161,10 @@ export default class BaseItemList extends HTMLElement { height: 16px; margin-inline-end: 5px; } + + .item > .delete { + margin: 0 5px; + } `; } static get headerItemTemplateContent() { @@ -162,6 +185,7 @@ export default class BaseItemList extends HTMLElement {
+ `; @@ -196,23 +220,35 @@ export default class BaseItemList extends HTMLElement { } connectedCallback() { - this.shadowRoot.querySelector(".search-input").addEventListener("input", this.searchInputCallbackRaw); - this.shadowRoot.querySelector(".search-input").addEventListener("keydown", this.searchKeyDownCallback); - this.shadowRoot.querySelector(".search-input").addEventListener("keyup", this.searchKeyUpCallback); - this.shadowRoot.querySelector(".list-body").addEventListener("click", this.itemListClick); - this.shadowRoot.querySelector(".list-body").addEventListener("keydown", this.itemListKeyDown); - this.shadowRoot.querySelector(".list-body").addEventListener("mouseover", this.itemListSelect); - this.shadowRoot.querySelector(".list-body").addEventListener("mouseleave", this.itemListSelectLeave); + let searchInput = this.search; + let listBody = this.shadowRoot.querySelector(".list-body"); + + searchInput.placeholder = this.getAttribute("placeholder") || ""; + + searchInput.addEventListener("input", this.searchInputCallbackRaw); + listBody.addEventListener("click", this.itemListClick); + if (!this.getAttribute("readonly")) { + searchInput.addEventListener("keydown", this.searchKeyDownCallback); + searchInput.addEventListener("keyup", this.searchKeyUpCallback); + listBody.addEventListener("keydown", this.itemListKeyDown); + listBody.addEventListener("mouseover", this.itemListSelect); + listBody.addEventListener("mouseleave", this.itemListSelectLeave); + } } disconnectedCallback() { - this.shadowRoot.querySelector(".search-input").removeEventListener("input", this.searchInputCallbackRaw); - this.shadowRoot.querySelector(".search-input").removeEventListener("keydown", this.searchKeyDownCallback); - this.shadowRoot.querySelector(".search-input").removeEventListener("keyup", this.searchKeyCallback); - this.shadowRoot.querySelector(".list-body").removeEventListener("click", this.itemListClick); - this.shadowRoot.querySelector(".list-body").removeEventListener("keydown", this.itemListKeyDown); - this.shadowRoot.querySelector(".list-body").removeEventListener("mouseover", this.itemListSelect); - this.shadowRoot.querySelector(".list-body").removeEventListener("mouseleave", this.itemListSelectLeave); + let searchInput = this.search; + let listBody = this.shadowRoot.querySelector(".list-body"); + + searchInput.removeEventListener("input", this.searchInputCallbackRaw); + listBody.removeEventListener("click", this.itemListClick); + if (!this.getAttribute("readonly")) { + searchInput.removeEventListener("keydown", this.searchKeyDownCallback); + searchInput.removeEventListener("keyup", this.searchKeyCallback); + listBody.removeEventListener("keydown", this.itemListKeyDown); + listBody.removeEventListener("mouseover", this.itemListSelect); + listBody.removeEventListener("mouseleave", this.itemListSelectLeave); + } } get search() { @@ -299,7 +335,13 @@ export default class BaseItemList extends HTMLElement { } itemListClick(event) { - this.dispatchSelect(event.target.closest(".item")); + if (this.getAttribute("delete") && event.target.classList.contains("delete")) { + let listItem = event.target.closest(".item"); + let customEvent = new CustomEvent("item-deleted", { detail: listItem.item }); + this.dispatchEvent(customEvent); + } else if (!this.getAttribute("readonly")) { + this.dispatchSelect(event.target.closest(".item")); + } } dispatchSelect(item) { @@ -433,7 +475,7 @@ export default class BaseItemList extends HTMLElement { this._addItem(item, BaseItemList.MODE_SEARCH); } } - } else if (this.defaultItems?.length) { + } else if (this.defaultItems) { for (let item of this.defaultItems) { this._addItem(item, BaseItemList.MODE_DEFAULT); } diff --git a/src/popup/folderlist.js b/src/popup/folderlist.js index 8fcb6f3..89758b9 100644 --- a/src/popup/folderlist.js +++ b/src/popup/folderlist.js @@ -71,7 +71,8 @@ class TBFolderList extends BaseItemList { } _addItem(folder, mode) { - let depth = mode == BaseItemList.MODE_ALL ? (item.path.match(/\//g) || []).length - 1 : 0; + // let depth = mode == BaseItemList.MODE_ALL ? (folder.path.match(/\//g) || []).length - 1 : 0; + let depth = 0; let template = this.shadowRoot.querySelector(".item-template"); let body = this.shadowRoot.querySelector(".list-body"); @@ -144,5 +145,4 @@ class TBFolderList extends BaseItemList { this.repopulate(); } } - customElements.define("folder-list", TBFolderList); diff --git a/src/popup/popup.css b/src/popup/popup.css index 6c926f2..d904306 100644 --- a/src/popup/popup.css +++ b/src/popup/popup.css @@ -31,12 +31,13 @@ body, html { min-width: 42em; padding: 0; margin: 0; - overflow: auto; } body { padding: 8px; max-height: 600px; + display: flex; + flex-direction: column; } html[compact] body, html[compact] { diff --git a/src/popup/popup.js b/src/popup/popup.js index c470ddd..a0d1a7f 100644 --- a/src/popup/popup.js +++ b/src/popup/popup.js @@ -3,7 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. * Portions Copyright (C) Philipp Kewisch */ -import { AccountNode } from "./foldernode.js"; +import { AccountNode } from "../common/foldernode.js"; +import { getValidatedDefaultFolders } from "../common/util.js"; const ALL_ACTIONS = ["move", "copy", "goto", "tag"]; @@ -19,24 +20,24 @@ function switchList(action) { let tagList = document.getElementById("tag-list"); if (action == "tag") { - tagList.style.display = "block"; + tagList.style.display = "revert-layer"; folderList.style.display = "none"; return tagList; } else { - folderList.style.display = "block"; + folderList.style.display = "revert-layer"; tagList.style.display = "none"; return folderList; } } async function load() { - let { maxRecentFolders, showFolderPath, skipArchive, layout } = await browser.storage.local.get({ maxRecentFolders: 15, showFolderPath: true, layout: "auto", skipArchive: true }); + let { maxRecentFolders, showFolderPath, skipArchive, layout, defaultFolderSetting } = await browser.storage.local.get({ maxRecentFolders: 15, showFolderPath: true, layout: "auto", skipArchive: true, defaultFolderSetting: "recent" }); if (layout == "wide" || (layout == "auto" && window.outerWidth > 1400)) { document.documentElement.removeAttribute("compact"); document.getElementById("folder-list").removeAttribute("compact"); } - document.body.style.display = "block"; + document.body.style.display = "revert-layer"; setup_localization(); @@ -65,19 +66,27 @@ async function load() { let accounts = await browser.accounts.list(); let [currentWindow] = await browser.mailTabs.query({ currentWindow: true, active: true }); - let currentAccount = currentWindow.displayedFolder.accountId; - let currentAccountIndex = accounts.findIndex(account => account.id === currentAccount); + let currentAccountId = currentWindow.displayedFolder?.accountId; + let currentAccountIndex = accounts.findIndex(account => account.id === currentAccountId); if (currentAccountIndex >= 0) { accounts.unshift(...accounts.splice(currentAccountIndex, 1)); } let accountNodes = accounts.map(account => new AccountNode(account, skipArchive)); let folders = accountNodes.reduce((acc, node) => acc.concat([...node]), []); - let recent = await browser.quickmove.query({ recent: true, limit: maxRecentFolders, canFileMessages: true }); + let defaultFolders; + + if (defaultFolderSetting == "recent") { + defaultFolders = await browser.quickmove.query({ recent: true, limit: maxRecentFolders, canFileMessages: true }); + } else if (defaultFolderSetting == "specific") { + defaultFolders = await getValidatedDefaultFolders(accountNodes); + } else { + defaultFolders = null; + } let folderList = document.getElementById("folder-list"); folderList.accounts = accounts; - folderList.initItems(folders, recent, showFolderPath); + folderList.initItems(folders, defaultFolders, showFolderPath); folderList.ignoreFocus = true; folderList.addEventListener("item-selected", async (event) => { let operation = document.querySelector("input[name='action']:checked").value; @@ -95,7 +104,7 @@ async function load() { let tags = await browser.messages.listTags(); let tagList = document.getElementById("tag-list"); tagList.ignoreFocus = true; - tagList.initItems(tags, []); + tagList.initItems(tags, null); tagList.addEventListener("item-selected", async (event) => { await browser.runtime.sendMessage({ action: "processSelectedMessages", tag: event.detail.key, operation: "tag" }); window.close();