tools / commit
Merge branch 'v2' into 'main'
separator-page/icons/template-icon-1.png | Bin 0 -> 23061 bytes separator-page/icons/template-icon-10.png | Bin 0 -> 28966 bytes separator-page/icons/template-icon-11.png | Bin 0 -> 39781 bytes separator-page/icons/template-icon-12.png | Bin 0 -> 30222 bytes separator-page/icons/template-icon-13.png | Bin 0 -> 19720 bytes separator-page/icons/template-icon-2.png | Bin 0 -> 14143 bytes separator-page/icons/template-icon-3.png | Bin 0 -> 16440 bytes separator-page/icons/template-icon-4.png | Bin 0 -> 37455 bytes separator-page/icons/template-icon-5.png | Bin 0 -> 24424 bytes separator-page/icons/template-icon-6.png | Bin 0 -> 26763 bytes separator-page/icons/template-icon-7.png | Bin 0 -> 31415 bytes separator-page/icons/template-icon-8.png | Bin 0 -> 21859 bytes separator-page/icons/template-icon-9.png | Bin 0 -> 26307 bytes separator-page/index.html | 842 ++++++++++++++++++++---------- 14 files changed, 571 insertions(+), 271 deletions(-) diff --git a/separator-page/icons/template-icon-1.png b/separator-page/icons/template-icon-1.png new file mode 100644 index 0000000..6f3825b Binary files /dev/null and b/separator-page/icons/template-icon-1.png differ diff --git a/separator-page/icons/template-icon-10.png b/separator-page/icons/template-icon-10.png new file mode 100644 index 0000000..e96db50 Binary files /dev/null and b/separator-page/icons/template-icon-10.png differ diff --git a/separator-page/icons/template-icon-11.png b/separator-page/icons/template-icon-11.png new file mode 100644 index 0000000..0029ab6 Binary files /dev/null and b/separator-page/icons/template-icon-11.png differ diff --git a/separator-page/icons/template-icon-12.png b/separator-page/icons/template-icon-12.png new file mode 100644 index 0000000..1d644e2 Binary files /dev/null and b/separator-page/icons/template-icon-12.png differ diff --git a/separator-page/icons/template-icon-13.png b/separator-page/icons/template-icon-13.png new file mode 100644 index 0000000..b1517ac Binary files /dev/null and b/separator-page/icons/template-icon-13.png differ diff --git a/separator-page/icons/template-icon-2.png b/separator-page/icons/template-icon-2.png new file mode 100644 index 0000000..e78e157 Binary files /dev/null and b/separator-page/icons/template-icon-2.png differ diff --git a/separator-page/icons/template-icon-3.png b/separator-page/icons/template-icon-3.png new file mode 100644 index 0000000..ca665b1 Binary files /dev/null and b/separator-page/icons/template-icon-3.png differ diff --git a/separator-page/icons/template-icon-4.png b/separator-page/icons/template-icon-4.png new file mode 100644 index 0000000..10eaaf3 Binary files /dev/null and b/separator-page/icons/template-icon-4.png differ diff --git a/separator-page/icons/template-icon-5.png b/separator-page/icons/template-icon-5.png new file mode 100644 index 0000000..48753c1 Binary files /dev/null and b/separator-page/icons/template-icon-5.png differ diff --git a/separator-page/icons/template-icon-6.png b/separator-page/icons/template-icon-6.png new file mode 100644 index 0000000..95ef782 Binary files /dev/null and b/separator-page/icons/template-icon-6.png differ diff --git a/separator-page/icons/template-icon-7.png b/separator-page/icons/template-icon-7.png new file mode 100644 index 0000000..922e89e Binary files /dev/null and b/separator-page/icons/template-icon-7.png differ diff --git a/separator-page/icons/template-icon-8.png b/separator-page/icons/template-icon-8.png new file mode 100644 index 0000000..eedb323 Binary files /dev/null and b/separator-page/icons/template-icon-8.png differ diff --git a/separator-page/icons/template-icon-9.png b/separator-page/icons/template-icon-9.png new file mode 100644 index 0000000..1a81b6e Binary files /dev/null and b/separator-page/icons/template-icon-9.png differ diff --git a/separator-page/index.html b/separator-page/index.html index c72df94..2dc61d3 100644 --- a/separator-page/index.html +++ b/separator-page/index.html @@ -1,320 +1,620 @@ <!DOCTYPE html> <html> - <head> - <meta charset="utf-8" /> - <title>Separator</title> - <link rel="icon" - href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP//AAD//wAA/n8AAP5/AAD+fwAA/n8AAOAHAADgBwAA/n8AAP5/AAD+fwAA/n8AAP//AAD//wAA" /> - <script language="javascript" - src="https://pavelsafronov.gitlab.io/tools/separator-page/lz-string.js"></script> - - <!-- https://github.com/showdownjs/showdown --> - <script language="javascript" - src="https://rawgit.com/showdownjs/showdown/develop/dist/showdown.min.js"></script> - <script> - /** @typedef {Object} State - * @property {string} name - * @property {string} description - */ - - /** @type {State} */ - const defaultState = { - name: "Separator", - description: `# Info +<head> + <meta charset="utf-8" /> + <title>Separator</title> + <link rel="icon" id="favicon" + href="data:image/x-icon;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//wAA//8AAP//AAD//wAA/n8AAP5/AAD+fwAA/n8AAOAHAADgBwAA/n8AAP5/AAD+fwAA/n8AAP//AAD//wAA" /> + <script src="https://cdn.jsdelivr.net/npm/showdown/dist/showdown.min.js"></script> + <style> + body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; + background-color: #f9f9f9; + } + + .container { + width: 90%; + max-width: 600px; + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 16px; + } + + .header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; + } + + .icon-preview { + width: 32px; + height: 32px; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + background-size: cover; + } + + .title { + font-size: 1.5em; + font-weight: bold; + width: 100%; + outline: none; + border: none; + background: none; + } + + .title[contenteditable="true"] { + border: 1px dashed #aaa; + padding: 4px; + border-radius: 4px; + } + + .description-container { + position: relative; + } + + .description { + font-size: 1em; + line-height: 1.5; + color: #555; + outline: none; + width: 100%; + display: none; + height: max-content; + } + + .description-rendered { + font-size: 1em; + line-height: 1.5; + color: #555; + width: 100%; + } + + .description[contenteditable="true"] { + border: 1px dashed #aaa; + padding: 8px; + border-radius: 4px; + display: block; + } + + .toolbar { + margin-top: 16px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .info { + font-size: 0.9em; + color: #666; + text-align: center; + margin-top: 12px; + } + + .drop-area { + border: 2px dashed #aaa; + padding: 8px; + text-align: center; + color: #666; + font-size: 0.9em; + border-radius: 4px; + } + + .drop-area.dragover { + background-color: #eef; + border-color: #666; + } + + .custom-icon-input { + display: flex; + gap: 8px; + align-items: center; + } + + .custom-icon-input input { + flex-grow: 1; + } + + .edit-button { + position: absolute; + top: 0; + right: 0; + background: #ddd; + border: none; + padding: 4px; + cursor: pointer; + border-radius: 4px; + } + + .edit-button:hover { + background: #ccc; + } + + .previous-icons { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; + } + + .previous-icon { + width: 32px; + height: 32px; + border: 1px solid #ddd; + border-radius: 4px; + cursor: pointer; + background-size: cover; + background-position: center; + transition: transform 0.2s; + } + + .previous-icon:hover { + transform: scale(1.1); + border-color: #999; + } + + .previous-icons-container { + margin: 16px 0; + } + + .previous-icons-container h3 { + margin: 0 0 8px 0; + font-size: 1.1em; + color: #333; + } + </style> +</head> + +<body> + + <div class="container"> + <div class="header"> + <div id="icon-preview" class="icon-preview" title="Icon Preview"></div> + <div id="title" class="title" contenteditable="true">Separator</div> + </div> -This is a separator page! + <hr /> + + <div class="description-container"> + <textarea id="description" class="description" placeholder="Write your Markdown here..."># Info +This is a separator page! ## TODO 1. Read this page 1. Understand this page -1. The page name and page content are both editable -1. To enter "edit mode", click on the "Edit" button OR **double-click on title or description element** -1. Double-click on either element again to return to "view mode" +1. The page name, icon, and content are editable +1. To enter "edit mode", click on the "Edit" button +1. Click "Done" button to return to "view mode" 1. Replace the contents on this page to whatever you want a new separator page to contain 1. Markdown contents will be rendered as HTML 1. Content links will always open in a new tab -1. Click "Save as Default" button to save current page as default template -1. If you messed up this page, click on "Hard Reset" to go back to this default message -1. The name and the description are stored as zipped content in the url, so copy-pasting the url in a new tab will exactly reproduce this page -1. Your data is stored in the hash fragment of the url, and [this data is not sent to the server](https://stackoverflow.com/questions/14462218/is-the-url-fragment-identifier-sent-to-the-server), so your notes are never sent anywhere -1. But your notes **do** end up in the browser's history, so don't keep anything very important here -1. The default page state (default title and contents) is kept in [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), so this information also never leaves your computer -1. Local storage is accessible by any other script that's executing on the separator page (or on the same domain), so technically *some* scripts can get your local "default separator page" state` - }; - const maxDataLength = 64 * 1024; - let currentState = defaultState; - - /** @type {HTMLElement} */ - let nameSpan; - /** @type {HTMLInputElement} */ - let nameInput; - /** @type {HTMLElement} */ - let descriptionSpan; - /** @type {HTMLElement} */ - let descriptionShowSpan; - - const lsKey = "pavel-safronov-separator-page"; - const storedDefaultJson = localStorage.getItem(lsKey); - if (isNullOrEmpty(storedDefaultJson)) { - localStorage.setItem(lsKey, JSON.stringify(currentState)); - } else { - /** @type {State} */ - let newState; - try { - newState = JSON.parse(storedDefaultJson); - } catch { - console.error({ message: "Could not parse stored default", storedDefaultJson }); - } - if (newState) { - currentState = newState; - } - } +1. All the information about a tab is stored under a specific id in local storage. Copy-pasting the url in a new tab will work, as long as you're in the same browser. +1. Your data is stored in local storage, under the tab id. +1. Local storage is accessible by any other script that's executing on the separator page (or on the same domain), so +technically *some* scripts can get your local "default separator page" state + </textarea> + <div id="description-rendered" class="description-rendered"></div> + <button id="edit-description" class="edit-button">Edit</button> + </div> - // MD converter - var shodownConverter = new showdown.Converter(); + <hr /> + + <div class="previous-icons-container"> + <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> + <h3 style="margin: 0;">Previous Icons (shift-click to remove)</h3> + <div style="display: flex; gap: 8px;"> + <button id="reset-icons" + style="padding: 4px 8px; font-size: 0.8em; background: #ddd; border: none; border-radius: 4px; cursor: pointer;">Reset</button> + <button id="export-icons" + style="padding: 4px 8px; font-size: 0.8em; background: #ddd; border: none; border-radius: 4px; cursor: pointer;">Export + PNG</button> + </div> + </div> + <div class="previous-icons" id="previous-icons"> + <!-- Previous icons will be dynamically added here --> + </div> + </div> - /** - * @param {string} text - */ - function isNullOrEmpty(text) { - return (!text || text.length === 0); - } + <div class="toolbar"> + <div class="drop-area" id="drop-area">Drop a PNG here or paste an image URL</div> + <div class="custom-icon-input"> + <input id="icon-url-input" type="text" placeholder="Paste an image URL here" /> + <button id="apply-icon-button">Apply</button> + </div> + </div> - /** - * @param {State} partialState - */ - function update(partialState) { - const path = window.location.pathname; + <div class="info"> + Need icons? Check out <a href="https://flaticon.com" target="_blank">Flaticon</a>. + <br /> + The default icons came from <a href="https://www.flaticon.com/authors/icongeek26" + target="_blank">icongeek26</a> + </div> + </div> - if (partialState) { - const newState = { - ...currentState, - ...partialState - }; - currentState = newState; - } - const stateJson = JSON.stringify(currentState); - const stateCompressed = LZString.compressToBase64(stateJson); - const newPath = `${path}#${stateCompressed}`; - if (newPath.length >= maxDataLength) { - alert(`Url is too long (${newPath.length} characters), need to rethink storing data in the hash. Or not using the Separator Page as a OneNote replacement.`); - } + <script> + const converter = new showdown.Converter(); - try { - history.replaceState(currentState, "", newPath); - } catch (ex) { - alert(`Not able to save contents: ${ex}`); - } + /** @type {string} */ + const tabId = new URLSearchParams(window.location.search).get("tab") || Math.floor(Date.now() / 1000).toString(); - let { name, description } = currentState; - // set name - document.title = isNullOrEmpty(name) ? "---" : name; - if (nameSpan.innerHTML !== name) { - nameSpan.innerHTML = name; - } - if (nameInput.value !== name) { - nameInput.value = name; - } + /** @type {string} */ + const localStorageKey = `separator-tab-${tabId}`; - // set description - const descriptionToUse = description !== null && description.length > 0 ? description : "**No content specified, double-click to edit**"; - descriptionShowSpan.innerHTML = shodownConverter.makeHtml(descriptionToUse); - descriptionSpan.value = isNullOrEmpty(description) ? "" : description; + /** @type {HTMLElement} */ + const iconPreview = document.getElementById("icon-preview"); - // update all href links to open in new tabs - const links = descriptionShowSpan.getElementsByTagName("a"); - for (let i = 0; i < links.length; i++) { - const link = links.item(i); - link.target = "_blank"; - } - } + /** @type {HTMLElement} */ + const titleElement = document.getElementById("title"); - /** - * @param {HTMLElement} a - * @param {HTMLElement} b - */ - function flipHideShow(a, b) { - const isAHidden = a.className === "hidden"; - a.className = isAHidden ? "" : "hidden"; - b.className = isAHidden ? "hidden" : ""; - } + /** @type {HTMLTextAreaElement} */ + const descriptionElement = document.getElementById("description"); - window.onpopstate = (state) => { - update(state); - }; - window.onload = () => { - /** @type {State} */ - let state = history.state; - - const search = window.location.search; - if (search) { - const urlParams = new URLSearchParams(search); - const name = urlParams.get("name"); - if (name) { - state = { name }; - } - } + /** @type {HTMLElement} */ + const descriptionRendered = document.getElementById("description-rendered"); - const hash = window.location.hash; - if (hash) { - const compressed = hash.substring(1); - var json = LZString.decompressFromBase64(compressed); - state = JSON.parse(json); - } + /** @type {HTMLButtonElement} */ + const editDescriptionButton = document.getElementById("edit-description"); - // name input - nameInput = document.getElementById("name-input"); - nameInput.onkeyup = () => update({ name: nameInput.value }); - - // name field - nameSpan = document.getElementById("name"); - - // description field - descriptionSpan = document.getElementById("description"); - descriptionSpan.onkeyup = () => update({ description: descriptionSpan.value }); - - // description html field - descriptionShowSpan = document.getElementById("description-show"); - - const saveButton = document.getElementById("save"); - const resetButton = document.getElementById("reset"); - const editButton = document.getElementById("edit"); - const doneButton = document.getElementById("done"); - - // hook up double-click switching - const flipEditState = () => { - flipHideShow(descriptionShowSpan, descriptionSpan); - flipHideShow(nameInput, nameSpan); - flipHideShow(editButton, doneButton); - }; - descriptionShowSpan.addEventListener("dblclick", flipEditState); - descriptionSpan.addEventListener("dblclick", flipEditState); - nameInput.addEventListener("dblclick", () => flipEditState); - nameSpan.addEventListener("dblclick", () => flipEditState); - - saveButton.onclick = () => { - if (confirm("Save current title and description as default starting values for separator page?")) { - localStorage.setItem(lsKey, JSON.stringify(currentState)); - } - }; - resetButton.onclick = () => { - if (confirm("Set default title and description values to original values?")) { - localStorage.setItem(lsKey, JSON.stringify(defaultState)); - update(defaultState); - } - }; - editButton.onclick = () => { - flipEditState(); - } - done.onclick = () => { - flipEditState(); - } + /** @type {HTMLInputElement} */ + const iconUrlInput = document.getElementById("icon-url-input"); - // render everything for the first time - update(state); - }; + /** @type {HTMLButtonElement} */ + const applyIconButton = document.getElementById("apply-icon-button"); - </script> - <style> - #description { - color: gray; - font-size: small; - width: 100%; - height: 100%; - box-sizing: border-box; - } + /** @type {HTMLDivElement} */ + const dropArea = document.getElementById("drop-area"); - #name { - width: 100%; - font-size: xx-large; - } + /** @type {HTMLLinkElement} */ + const faviconElement = document.getElementById("favicon"); - .container { - height: 100%; - } + /** @type {HTMLDivElement} */ + const previousIconsContainer = document.getElementById("previous-icons"); - .info { - color: gray; - font-size: small; + /** @type {HTMLButtonElement} */ + const exportIconsButton = document.getElementById("export-icons"); + + /** @type {HTMLButtonElement} */ + const resetIconsButton = document.getElementById("reset-icons"); + + // Global storage key for previous icons (shared across all tabs) + const previousIconsKey = "separator-previous-icons"; + const maxPreviousIcons = 100; // Limit to 100 recent icons + + // Default icons from the icons folder + const defaultIcons = [ + "./icons/template-icon-1.png", + "./icons/template-icon-2.png", + "./icons/template-icon-3.png", + "./icons/template-icon-4.png", + "./icons/template-icon-5.png", + "./icons/template-icon-6.png", + "./icons/template-icon-7.png", + "./icons/template-icon-8.png", + "./icons/template-icon-9.png", + "./icons/template-icon-10.png", + "./icons/template-icon-11.png", + "./icons/template-icon-12.png", + "./icons/template-icon-13.png" + ]; + + // Load saved data + const savedData = JSON.parse(localStorage.getItem(localStorageKey) || "null"); + + if (!new URLSearchParams(window.location.search).has("tab")) { + const newUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`; + history.replaceState(null, "", newUrl); + } + + if (savedData) { + renderAll(savedData); + console.debug('Loaded saved data:', savedData); + } else { + saveTabData(); + } + + // Initialize previous icons display + initializeDefaultIcons(); + renderPreviousIcons(); + + function initializeDefaultIcons() { + const existingIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); + + // If no icons exist, populate with defaults + if (existingIcons.length === 0) { + localStorage.setItem(previousIconsKey, JSON.stringify(defaultIcons)); + console.log('Initialized with default icons'); } + } + + function resetToDefaultIcons() { + localStorage.setItem(previousIconsKey, JSON.stringify(defaultIcons)); + renderPreviousIcons(); + + // Show feedback + const originalText = resetIconsButton.textContent; + resetIconsButton.textContent = "Reset!"; + resetIconsButton.style.background = "#4CAF50"; + setTimeout(() => { + resetIconsButton.textContent = originalText; + resetIconsButton.style.background = "#ddd"; + }, 2000); + + console.log('Reset to default icons'); + } + + function updateFavicon(iconUrl) { + console.debug('Updating favicon with URL:', iconUrl); + faviconElement.href = iconUrl; + } + + function savePreviousIcon(iconUrl) { + // Skip default favicon and data URLs for data:image/x-icon + if (iconUrl.startsWith('data:image/x-icon')) { + return; + } + + let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); + + // Remove the icon if it already exists to avoid duplicates + previousIcons = previousIcons.filter(icon => icon !== iconUrl); - .left { - float: left; + // Add the new icon to the beginning + previousIcons.unshift(iconUrl); + + // Limit to maxPreviousIcons + if (previousIcons.length > maxPreviousIcons) { + previousIcons = previousIcons.slice(0, maxPreviousIcons); } - .right { - float: right; + localStorage.setItem(previousIconsKey, JSON.stringify(previousIcons)); + renderPreviousIcons(); + } + + function removePreviousIcon(iconUrl) { + let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); + previousIcons = previousIcons.filter(icon => icon !== iconUrl); + localStorage.setItem(previousIconsKey, JSON.stringify(previousIcons)); + renderPreviousIcons(); + } + + function renderPreviousIcons() { + const previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); + previousIconsContainer.innerHTML = ""; + + previousIcons.forEach(iconUrl => { + const iconElement = document.createElement("div"); + iconElement.className = "previous-icon"; + iconElement.style.backgroundImage = `url('${iconUrl}')`; + iconElement.title = `Click to use • Shift+Click to remove`; + iconElement.addEventListener("click", (event) => { + if (event.shiftKey) { + removePreviousIcon(iconUrl); + } else { + handleIconChange(iconUrl); + } + }); + previousIconsContainer.appendChild(iconElement); + }); + } + + async function downloadImageAsFile(url, filename) { + try { + // If it's a data URL, we can download it directly + if (url.startsWith('data:')) { + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + return true; + } + + // For external URLs, we need to fetch and convert + const response = await fetch(url); + const blob = await response.blob(); + + // Convert to PNG if it's not already + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + return new Promise((resolve) => { + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + + canvas.toBlob((pngBlob) => { + const link = document.createElement('a'); + link.href = URL.createObjectURL(pngBlob); + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + resolve(true); + }, 'image/png'); + }; + + img.onerror = () => { + console.warn(`Failed to load image: ${url}`); + resolve(false); + }; + + img.crossOrigin = 'anonymous'; + img.src = URL.createObjectURL(blob); + }); + } catch (error) { + console.warn(`Failed to download ${url}:`, error); + return false; } + } + + async function exportIcons() { + const previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); - .hidden { - display: none; + if (previousIcons.length === 0) { + alert('No icons to export!'); + return; } - </style> - </head> + // Update button to show progress + const originalText = exportIconsButton.textContent; + exportIconsButton.textContent = "Exporting..."; + exportIconsButton.style.background = "#ffa500"; + exportIconsButton.disabled = true; - <body> + let successCount = 0; - <div class="container"> + for (let i = 0; i < previousIcons.length; i++) { + const iconUrl = previousIcons[i]; + const filename = `template-icon-${i + 1}.png`; - <div class="container"> + const success = await downloadImageAsFile(iconUrl, filename); + if (success) { + successCount++; + } - <input autofocus - id="name-input" - title="Name" - class="hidden" - size="100" /> + // Small delay between downloads to prevent overwhelming the browser + await new Promise(resolve => setTimeout(resolve, 100)); + } - <span id="name" - title="Tab title - double-click to edit"> - Separator - </span> + // Show completion feedback + exportIconsButton.textContent = `Exported ${successCount}/${previousIcons.length}`; + exportIconsButton.style.background = successCount === previousIcons.length ? "#4CAF50" : "#ff9800"; + + setTimeout(() => { + exportIconsButton.textContent = originalText; + exportIconsButton.style.background = "#ddd"; + exportIconsButton.disabled = false; + }, 3000); + } + + function saveTabData() { + const data = { + id: tabId, + icon: faviconElement.href, + title: titleElement.innerText, + description: descriptionElement.value, + }; + console.debug('Saving tab data to localStorage:', data); + renderAll(data); + localStorage.setItem(localStorageKey, JSON.stringify(data)); + } + + function renderAll(data) { + iconPreview.style.backgroundImage = `url('${data.icon}')`; + if (titleElement.innerText !== data.title) { + titleElement.innerText = data.title; + } + document.title = titleElement.innerText || "Separator"; + renderDescription(data.description); + updateFavicon(data.icon); + } + + function renderDescription(description) { + descriptionElement.value = description; + descriptionElement.style.height = ""; + descriptionElement.style.height = `${descriptionElement.scrollHeight}px`; + const html = converter.makeHtml(description); + descriptionRendered.innerHTML = html; + } + + function handleIconChange(iconUrl) { + iconPreview.style.backgroundImage = `url('${iconUrl}')`; + updateFavicon(iconUrl); + savePreviousIcon(iconUrl); + saveTabData(); + } + + // Toggle edit mode for description + editDescriptionButton.addEventListener("click", () => { + const isEditing = descriptionElement.style.display === "block"; + + console.debug('Toggling edit mode:', isEditing ? 'Done' : 'Edit'); + descriptionElement.style.display = isEditing ? "none" : "block"; + descriptionRendered.style.display = isEditing ? "block" : "none"; + editDescriptionButton.innerText = isEditing ? "Edit" : "Done"; + + // renderDescription(); + saveTabData(); + }); + + // Drop area handling + dropArea.addEventListener("dragover", (event) => { + event.preventDefault(); + dropArea.classList.add("dragover"); + }); + + dropArea.addEventListener("dragleave", () => { + dropArea.classList.remove("dragover"); + }); + + dropArea.addEventListener("drop", (event) => { + event.preventDefault(); + dropArea.classList.remove("dragover"); + const file = event.dataTransfer.files[0]; + if (file && file.type.startsWith("image/")) { + const reader = new FileReader(); + reader.onload = () => handleIconChange(reader.result); + reader.readAsDataURL(file); + } + }); + + // Clipboard paste handling + document.addEventListener("paste", (event) => { + const clipboardItems = event.clipboardData.items; + for (const item of clipboardItems) { + if (item.type.startsWith("image/")) { + const blob = item.getAsFile(); + const reader = new FileReader(); + reader.onload = () => handleIconChange(reader.result); + reader.readAsDataURL(blob); + break; + } + } + }); - <span class="info"> - Tab title - </span> - </div> + // Apply icon from URL + applyIconButton.addEventListener("click", () => { + const iconUrl = iconUrlInput.value.trim(); + if (iconUrl) { + handleIconChange(iconUrl); + } + }); - <div class="container"> + // Export icons as PNG files + exportIconsButton.addEventListener("click", exportIcons); - <textarea rows="30" - id="description" - title="content" - class="hidden"></textarea> + // Reset icons to defaults + resetIconsButton.addEventListener("click", resetToDefaultIcons); - <span id="description-show" - title="Description - double-click to edit"></span> - </div> - </div> + console.debug("here!"); + // Title and description event listeners + titleElement.addEventListener("input", () => { + console.debug('Title updated to:', titleElement.innerText); + saveTabData(); + }); - <div class="container"> - - <div class="left"> - <button type="button" - id="edit"> - Edit - </button> - <button type="button" - id="done" - class="hidden"> - Done - </button> - <button type="button" - id="save"> - Save as Default - </button> - </div> + descriptionElement.addEventListener("input", () => { + // console.debug('Description updated to:', descriptionElement.value); + }); + </script> - <div class="right"> - <button type="button" - id="reset"> - Hard Reset - </button> - </div> - </div> - </div> - </body> +</body> -</html> +</html> \ No newline at end of file