tools / files / separator-page / index.html
index.html
8d5ecd8e 2022-01-20 Pavel Safronov | 1 | <!DOCTYPE html> |
| 2 | <html> | |
| 3 | ||
f86ea817 2025-07-22 Pavel Safronov | 4 | <head> |
| 5 | <meta charset="utf-8" /> | |
| 6 | <title>Separator</title> | |
9d6bfeb8 2025-07-23 Pavel Safronov | 7 | <link rel="icon" id="favicon" /> |
f86ea817 2025-07-22 Pavel Safronov | 8 | <script src="https://cdn.jsdelivr.net/npm/showdown/dist/showdown.min.js"></script> |
483a7647 2026-10-27 Pavel Safronov | 9 | <script src="https://apps.fuzzyworld.net/tools/separator-page/lz-string.js"></script> |
f86ea817 2025-07-22 Pavel Safronov | 10 | <style> |
| 11 | body { | |
| 12 | font-family: Arial, sans-serif; | |
| 13 | margin: 0; | |
| 14 | padding: 0; | |
| 15 | display: flex; | |
| 16 | flex-direction: column; | |
| 17 | align-items: center; | |
| 18 | justify-content: center; | |
78f9b326 2025-07-22 Pavel Safronov | 19 | height: 100%; |
f86ea817 2025-07-22 Pavel Safronov | 20 | background-color: #f9f9f9; |
| 21 | } | |
| 22 | ||
| 23 | .container { | |
| 24 | width: 90%; | |
| 25 | max-width: 600px; | |
| 26 | background: #fff; | |
| 27 | border: 1px solid #ddd; | |
| 28 | border-radius: 8px; | |
| 29 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
| 30 | padding: 16px; | |
| 31 | } | |
| 32 | ||
| 33 | .header { | |
| 34 | display: flex; | |
| 35 | align-items: center; | |
| 36 | gap: 12px; | |
| 37 | margin-bottom: 8px; | |
| 38 | } | |
| 39 | ||
| 40 | .icon-preview { | |
| 41 | width: 32px; | |
| 42 | height: 32px; | |
| 43 | border: 1px solid #ddd; | |
| 44 | border-radius: 4px; | |
78f9b326 2025-07-22 Pavel Safronov | 45 | background-size: contain; |
| 46 | background-repeat: no-repeat; | |
| 47 | background-position: center; | |
f86ea817 2025-07-22 Pavel Safronov | 48 | } |
| 49 | ||
| 50 | .title { | |
| 51 | font-size: 1.5em; | |
| 52 | font-weight: bold; | |
| 53 | width: 100%; | |
| 54 | outline: none; | |
| 55 | border: none; | |
| 56 | background: none; | |
| 57 | } | |
| 58 | ||
| 59 | .title[contenteditable="true"] { | |
| 60 | border: 1px dashed #aaa; | |
| 61 | padding: 4px; | |
| 62 | border-radius: 4px; | |
| 63 | } | |
| 64 | ||
| 65 | .description-container { | |
| 66 | position: relative; | |
| 67 | } | |
| 68 | ||
| 69 | .description { | |
| 70 | font-size: 1em; | |
| 71 | line-height: 1.5; | |
| 72 | color: #555; | |
| 73 | outline: none; | |
| 74 | width: 100%; | |
| 75 | display: none; | |
| 76 | height: max-content; | |
| 77 | } | |
| 78 | ||
| 79 | .description-rendered { | |
| 80 | font-size: 1em; | |
| 81 | line-height: 1.5; | |
| 82 | color: #555; | |
| 83 | width: 100%; | |
| 84 | } | |
| 85 | ||
| 86 | .description[contenteditable="true"] { | |
| 87 | border: 1px dashed #aaa; | |
| 88 | padding: 8px; | |
| 89 | border-radius: 4px; | |
| 90 | display: block; | |
| 91 | } | |
| 92 | ||
| 93 | .toolbar { | |
| 94 | margin-top: 16px; | |
| 95 | display: flex; | |
| 96 | flex-direction: column; | |
| 97 | gap: 8px; | |
| 98 | } | |
| 99 | ||
| 100 | .info { | |
| 101 | font-size: 0.9em; | |
| 102 | color: #666; | |
| 103 | text-align: center; | |
| 104 | margin-top: 12px; | |
| 105 | } | |
| 106 | ||
| 107 | .drop-area { | |
| 108 | border: 2px dashed #aaa; | |
| 109 | padding: 8px; | |
| 110 | text-align: center; | |
| 111 | color: #666; | |
| 112 | font-size: 0.9em; | |
| 113 | border-radius: 4px; | |
| 114 | } | |
| 115 | ||
| 116 | .drop-area.dragover { | |
| 117 | background-color: #eef; | |
| 118 | border-color: #666; | |
| 119 | } | |
| 120 | ||
| 121 | .custom-icon-input { | |
| 122 | display: flex; | |
| 123 | gap: 8px; | |
| 124 | align-items: center; | |
| 125 | } | |
| 126 | ||
| 127 | .custom-icon-input input { | |
| 128 | flex-grow: 1; | |
| 129 | } | |
| 130 | ||
| 131 | .edit-button { | |
| 132 | position: absolute; | |
| 133 | top: 0; | |
| 134 | right: 0; | |
| 135 | background: #ddd; | |
| 136 | border: none; | |
| 137 | padding: 4px; | |
| 138 | cursor: pointer; | |
| 139 | border-radius: 4px; | |
| 140 | } | |
| 141 | ||
| 142 | .edit-button:hover { | |
| 143 | background: #ccc; | |
| 144 | } | |
| 145 | ||
| 146 | .previous-icons { | |
| 147 | display: flex; | |
| 148 | flex-wrap: wrap; | |
| 149 | gap: 8px; | |
| 150 | margin-top: 8px; | |
| 151 | } | |
| 152 | ||
| 153 | .previous-icon { | |
| 154 | width: 32px; | |
| 155 | height: 32px; | |
| 156 | border: 1px solid #ddd; | |
| 157 | border-radius: 4px; | |
| 158 | cursor: pointer; | |
| 159 | background-size: cover; | |
| 160 | background-position: center; | |
| 161 | transition: transform 0.2s; | |
| 162 | } | |
| 163 | ||
| 164 | .previous-icon:hover { | |
| 165 | transform: scale(1.1); | |
| 166 | border-color: #999; | |
| 167 | } | |
| 168 | ||
| 169 | .previous-icons-container { | |
| 170 | margin: 16px 0; | |
| 171 | } | |
| 172 | ||
| 173 | .previous-icons-container h3 { | |
| 174 | margin: 0 0 8px 0; | |
| 175 | font-size: 1.1em; | |
| 176 | color: #333; | |
| 177 | } | |
| 178 | </style> | |
| 179 | </head> | |
| 180 | ||
| 181 | <body> | |
| 182 | ||
| 183 | <div class="container"> | |
| 184 | <div class="header"> | |
| 185 | <div id="icon-preview" class="icon-preview" title="Icon Preview"></div> | |
| 186 | <div id="title" class="title" contenteditable="true">Separator</div> | |
| 187 | </div> | |
8d5ecd8e 2022-01-20 Pavel Safronov | 188 | |
f86ea817 2025-07-22 Pavel Safronov | 189 | <hr /> |
8d5ecd8e 2022-01-20 Pavel Safronov | 190 | |
f86ea817 2025-07-22 Pavel Safronov | 191 | <div class="description-container"> |
| 192 | <textarea id="description" class="description" placeholder="Write your Markdown here..."># Info | |
8d5ecd8e 2022-01-20 Pavel Safronov | 193 | |
14b80d45 2025-07-22 Pavel Safronov | 194 | This is a separator page! |
| 195 | ## TODO | |
| 196 | ||
| 197 | 1. Read this page | |
| 198 | 1. Understand this page | |
| 199 | 1. The page name, icon, and content are editable | |
900d6ca9 2025-07-22 Pavel Safronov | 200 | 1. To edit the description, click the "Edit" button |
| 201 | 1. Enter Markdown text | |
14b80d45 2025-07-22 Pavel Safronov | 202 | 1. Click "Done" button to return to "view mode" |
| 203 | 1. Markdown contents will be rendered as HTML | |
| 204 | 1. Content links will always open in a new tab | |
| 205 | 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. | |
| 206 | 1. Your data is stored in local storage, under the tab id. | |
| 207 | 1. Local storage is accessible by any other script that's executing on the separator page (or on the same domain), so | |
900d6ca9 2025-07-22 Pavel Safronov | 208 | technically *some* scripts can get your local separator page info |
14b80d45 2025-07-22 Pavel Safronov | 209 | </textarea> |
f86ea817 2025-07-22 Pavel Safronov | 210 | <div id="description-rendered" class="description-rendered"></div> |
| 211 | <button id="edit-description" class="edit-button">Edit</button> | |
| 212 | </div> | |
8d5ecd8e 2022-01-20 Pavel Safronov | 213 | |
f86ea817 2025-07-22 Pavel Safronov | 214 | <hr /> |
8d5ecd8e 2022-01-20 Pavel Safronov | 215 | |
f86ea817 2025-07-22 Pavel Safronov | 216 | <div class="previous-icons-container"> |
d981c94f 2025-07-22 Pavel Safronov | 217 | <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> |
| 218 | <h3 style="margin: 0;">Previous Icons (shift-click to remove)</h3> | |
| 219 | <div style="display: flex; gap: 8px;"> | |
14b80d45 2025-07-22 Pavel Safronov | 220 | <button id="reset-icons" |
| 221 | style="padding: 4px 8px; font-size: 0.8em; background: #ddd; border: none; border-radius: 4px; cursor: pointer;">Reset</button> | |
d981c94f 2025-07-22 Pavel Safronov | 222 | </div> |
| 223 | </div> | |
f86ea817 2025-07-22 Pavel Safronov | 224 | <div class="previous-icons" id="previous-icons"> |
| 225 | <!-- Previous icons will be dynamically added here --> | |
| 226 | </div> | |
| 227 | </div> | |
8d5ecd8e 2022-01-20 Pavel Safronov | 228 | |
f86ea817 2025-07-22 Pavel Safronov | 229 | <div class="toolbar"> |
| 230 | <div class="drop-area" id="drop-area">Drop a PNG here or paste an image URL</div> | |
| 231 | <div class="custom-icon-input"> | |
| 232 | <input id="icon-url-input" type="text" placeholder="Paste an image URL here" /> | |
| 233 | <button id="apply-icon-button">Apply</button> | |
| 234 | </div> | |
| 235 | </div> | |
8d5ecd8e 2022-01-20 Pavel Safronov | 236 | |
f86ea817 2025-07-22 Pavel Safronov | 237 | <div class="info"> |
| 238 | Need icons? Check out <a href="https://flaticon.com" target="_blank">Flaticon</a>. | |
| 239 | <br /> | |
5fdab834 2025-07-23 Pavel Safronov | 240 | The default icons came from |
| 241 | <a href="https://www.flaticon.com/authors/icongeek26" target="_blank">icongeek26</a>'s | |
| 242 | <a href="https://www.flaticon.com/search?author_id=296&style_id=1001&type=standard" target="_blank"> | |
| 243 | Outline collection</a>. | |
b4a56733 2025-10-21 Pavel Safronov | 244 | <br /> |
483a7647 2026-10-27 Pavel Safronov | 245 | <a href="https://code.fuzzyworld.net/tools/tree/separator-page/" target="_blank">Source repo</a>. |
f86ea817 2025-07-22 Pavel Safronov | 246 | </div> |
| 247 | </div> | |
8d5ecd8e 2022-01-20 Pavel Safronov | 248 | |
f86ea817 2025-07-22 Pavel Safronov | 249 | <script> |
| 250 | const converter = new showdown.Converter(); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 251 | |
f86ea817 2025-07-22 Pavel Safronov | 252 | /** @type {string} */ |
| 253 | const tabId = new URLSearchParams(window.location.search).get("tab") || Math.floor(Date.now() / 1000).toString(); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 254 | |
f86ea817 2025-07-22 Pavel Safronov | 255 | /** @type {string} */ |
| 256 | const localStorageKey = `separator-tab-${tabId}`; | |
8d5ecd8e 2022-01-20 Pavel Safronov | 257 | |
f86ea817 2025-07-22 Pavel Safronov | 258 | /** @type {HTMLElement} */ |
| 259 | const iconPreview = document.getElementById("icon-preview"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 260 | |
f86ea817 2025-07-22 Pavel Safronov | 261 | /** @type {HTMLElement} */ |
| 262 | const titleElement = document.getElementById("title"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 263 | |
f86ea817 2025-07-22 Pavel Safronov | 264 | /** @type {HTMLTextAreaElement} */ |
| 265 | const descriptionElement = document.getElementById("description"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 266 | |
f86ea817 2025-07-22 Pavel Safronov | 267 | /** @type {HTMLElement} */ |
| 268 | const descriptionRendered = document.getElementById("description-rendered"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 269 | |
f86ea817 2025-07-22 Pavel Safronov | 270 | /** @type {HTMLButtonElement} */ |
| 271 | const editDescriptionButton = document.getElementById("edit-description"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 272 | |
f86ea817 2025-07-22 Pavel Safronov | 273 | /** @type {HTMLInputElement} */ |
| 274 | const iconUrlInput = document.getElementById("icon-url-input"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 275 | |
f86ea817 2025-07-22 Pavel Safronov | 276 | /** @type {HTMLButtonElement} */ |
| 277 | const applyIconButton = document.getElementById("apply-icon-button"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 278 | |
f86ea817 2025-07-22 Pavel Safronov | 279 | /** @type {HTMLDivElement} */ |
| 280 | const dropArea = document.getElementById("drop-area"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 281 | |
f86ea817 2025-07-22 Pavel Safronov | 282 | /** @type {HTMLLinkElement} */ |
| 283 | const faviconElement = document.getElementById("favicon"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 284 | |
f86ea817 2025-07-22 Pavel Safronov | 285 | /** @type {HTMLDivElement} */ |
| 286 | const previousIconsContainer = document.getElementById("previous-icons"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 287 | |
14b80d45 2025-07-22 Pavel Safronov | 288 | /** @type {HTMLButtonElement} */ |
| 289 | const resetIconsButton = document.getElementById("reset-icons"); | |
| 290 | ||
f86ea817 2025-07-22 Pavel Safronov | 291 | // Global storage key for previous icons (shared across all tabs) |
| 292 | const previousIconsKey = "separator-previous-icons"; | |
| 293 | const maxPreviousIcons = 100; // Limit to 100 recent icons | |
8d5ecd8e 2022-01-20 Pavel Safronov | 294 | |
14b80d45 2025-07-22 Pavel Safronov | 295 | // Default icons from the icons folder |
| 296 | const defaultIcons = [ | |
78f9b326 2025-07-22 Pavel Safronov | 297 | "./icons/template-icon-0.png", |
14b80d45 2025-07-22 Pavel Safronov | 298 | "./icons/template-icon-1.png", |
| 299 | "./icons/template-icon-2.png", | |
| 300 | "./icons/template-icon-3.png", | |
| 301 | "./icons/template-icon-4.png", | |
| 302 | "./icons/template-icon-5.png", | |
| 303 | "./icons/template-icon-6.png", | |
| 304 | "./icons/template-icon-7.png", | |
| 305 | "./icons/template-icon-8.png", | |
| 306 | "./icons/template-icon-9.png", | |
| 307 | "./icons/template-icon-10.png", | |
| 308 | "./icons/template-icon-11.png", | |
| 309 | "./icons/template-icon-12.png", | |
| 310 | "./icons/template-icon-13.png" | |
| 311 | ]; | |
| 312 | ||
c4975b4a 2026-10-27 Pavel Safronov | 313 | // Import a tab from the old address (pavelsafronov.gitlab.io). Browsers keep storage per site, so the |
| 314 | // redirect page there reads this tab's data and passes it along as #migrate=<URI-encoded JSON>. | |
| 315 | // The payload is only trusted when we arrive from that site, because the description is rendered as HTML. | |
| 316 | function handleMigrationData() { | |
| 317 | const prefix = "#migrate="; | |
| 318 | if (!window.location.hash.startsWith(prefix)) { | |
| 319 | return; | |
| 320 | } | |
| 321 | const cleanUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`; | |
| 322 | try { | |
| 323 | const cameFromOldSite = document.referrer.startsWith("https://pavelsafronov.gitlab.io/"); | |
| 324 | if (!cameFromOldSite) { | |
| 325 | console.warn('Ignoring migration data: not from the old site. Referrer:', document.referrer); | |
| 326 | return; | |
| 327 | } | |
| 328 | const payload = JSON.parse(decodeURIComponent(window.location.hash.substring(prefix.length))); | |
| 329 | const toCurrentSite = (icon) => { | |
| 330 | // Built-in icons were stored as absolute URLs on the old site, where they no longer exist. | |
| 331 | const match = typeof icon === "string" && icon.match(/\/separator-page\/icons\/(template-icon-\d+\.png)$/); | |
| 332 | return match ? new URL(`icons/${match[1]}`, window.location.href).href : icon; | |
| 333 | }; | |
| 334 | ||
| 335 | // Keep the user's icon history too, behind whatever this site already has. | |
| 336 | if (Array.isArray(payload.previousIcons)) { | |
| 337 | const existing = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); | |
| 338 | const merged = [...existing, ...payload.previousIcons.map(toCurrentSite)]; | |
| 339 | const unique = [...new Set(merged)].slice(0, maxPreviousIcons); | |
| 340 | localStorage.setItem(previousIconsKey, JSON.stringify(unique)); | |
| 341 | } | |
| 342 | ||
| 343 | // Never overwrite a tab that already exists here. | |
| 344 | const data = payload.data; | |
| 345 | if (data && String(payload.tab) === tabId && !localStorage.getItem(localStorageKey)) { | |
| 346 | data.icon = toCurrentSite(data.icon); | |
| 347 | localStorage.setItem(localStorageKey, JSON.stringify(data)); | |
| 348 | console.debug('Imported tab from the old site:', data); | |
| 349 | } | |
| 350 | } catch (error) { | |
| 351 | console.warn('Failed to import migration data:', error); | |
| 352 | } finally { | |
| 353 | history.replaceState(null, "", cleanUrl); | |
| 354 | } | |
| 355 | } | |
| 356 | ||
| 357 | handleMigrationData(); | |
| 358 | ||
f86ea817 2025-07-22 Pavel Safronov | 359 | // Load saved data |
| 360 | const savedData = JSON.parse(localStorage.getItem(localStorageKey) || "null"); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 361 | |
e88c23cc 2025-07-23 Pavel Safronov | 362 | // Function to handle legacy hash-based data |
| 363 | function handleLegacyHashData() { | |
| 364 | console.debug('Checking for legacy hash data...'); | |
| 365 | const hash = window.location.hash; | |
| 366 | if (hash && hash.length > 1) { | |
| 367 | try { | |
| 368 | const base64String = hash.substring(1); // Remove the '#' character | |
| 369 | const decompressed = LZString.decompressFromBase64(base64String); | |
| 370 | if (decompressed) { | |
| 371 | const legacyData = JSON.parse(decompressed); | |
| 372 | console.debug('Loaded legacy hash data:', legacyData); | |
| 373 | ||
| 374 | // Extract name and description from legacy format | |
| 375 | const convertedData = { | |
| 376 | title: legacyData.name || "Separator", | |
| 377 | description: legacyData.description || "", | |
| 378 | icon: null // Will be set below | |
| 379 | }; | |
| 380 | ||
| 381 | // Use the first previous icon or first default icon as fallback | |
| 382 | const previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); | |
| 383 | const fallbackIcon = previousIcons.length > 0 ? previousIcons[0] : defaultIcons[0]; | |
| 384 | convertedData.icon = fallbackIcon; | |
| 385 | ||
| 386 | // Clear the hash and update URL | |
| 387 | const newUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`; | |
| 388 | history.replaceState(null, "", newUrl); | |
| 389 | ||
| 390 | // Save the converted data to localStorage | |
| 391 | localStorage.setItem(localStorageKey, JSON.stringify(convertedData)); | |
| 392 | ||
| 393 | return convertedData; | |
| 394 | } | |
| 395 | } catch (error) { | |
| 396 | console.warn('Failed to parse legacy hash data:', error); | |
| 397 | } | |
| 398 | } | |
| 399 | console.debug('No legacy hash data found.'); | |
| 400 | return null; | |
| 401 | } | |
| 402 | ||
| 403 | // Check for legacy hash data FIRST, before any URL manipulation | |
| 404 | const legacyData = handleLegacyHashData(); | |
| 405 | ||
78f9b326 2025-07-22 Pavel Safronov | 406 | const searchParams = new URLSearchParams(window.location.search); |
| 407 | if (!searchParams.has("tab")) { | |
f86ea817 2025-07-22 Pavel Safronov | 408 | const newUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`; |
| 409 | history.replaceState(null, "", newUrl); | |
| 410 | } | |
8d5ecd8e 2022-01-20 Pavel Safronov | 411 | |
78f9b326 2025-07-22 Pavel Safronov | 412 | if (searchParams.has("name")) { |
| 413 | const name = searchParams.get("name"); | |
| 414 | if (name) { | |
| 415 | titleElement.innerText = name; | |
| 416 | document.title = name; | |
| 417 | } | |
| 418 | } | |
e88c23cc 2025-07-23 Pavel Safronov | 419 | if (legacyData) { |
| 420 | renderAll(legacyData); | |
| 421 | console.debug('Initialized with legacy data:', legacyData); | |
| 422 | } else if (savedData) { | |
f86ea817 2025-07-22 Pavel Safronov | 423 | renderAll(savedData); |
| 424 | console.debug('Loaded saved data:', savedData); | |
| 425 | } else { | |
0e04e2d5 2025-07-23 Pavel Safronov | 426 | handleIconChange(defaultIcons[0]); // Set the first default icon as the current icon |
f86ea817 2025-07-22 Pavel Safronov | 427 | saveTabData(); |
| 428 | } | |
8d5ecd8e 2022-01-20 Pavel Safronov | 429 | |
f86ea817 2025-07-22 Pavel Safronov | 430 | // Initialize previous icons display |
14b80d45 2025-07-22 Pavel Safronov | 431 | initializeDefaultIcons(); |
f86ea817 2025-07-22 Pavel Safronov | 432 | renderPreviousIcons(); |
8d5ecd8e 2022-01-20 Pavel Safronov | 433 | |
14b80d45 2025-07-22 Pavel Safronov | 434 | function initializeDefaultIcons() { |
| 435 | const existingIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); | |
| 436 | ||
| 437 | // If no icons exist, populate with defaults | |
| 438 | if (existingIcons.length === 0) { | |
| 439 | localStorage.setItem(previousIconsKey, JSON.stringify(defaultIcons)); | |
| 440 | console.log('Initialized with default icons'); | |
db52355d 2025-07-23 Pavel Safronov | 441 | handleIconChange(defaultIcons[0]); // Set the first default icon as the current icon |
14b80d45 2025-07-22 Pavel Safronov | 442 | } |
| 443 | } | |
| 444 | ||
| 445 | function resetToDefaultIcons() { | |
89807632 2025-07-23 Pavel Safronov | 446 | const confirmMessage = "Are you sure you want to reset to default icons?\n\nThis will permanently delete ALL your previous icons and replace them with the original template icons.\n\nThis action cannot be undone."; |
9d6bfeb8 2025-07-23 Pavel Safronov | 447 | |
89807632 2025-07-23 Pavel Safronov | 448 | if (!confirm(confirmMessage)) { |
| 449 | return; // User cancelled, do nothing | |
| 450 | } | |
| 451 | ||
14b80d45 2025-07-22 Pavel Safronov | 452 | localStorage.setItem(previousIconsKey, JSON.stringify(defaultIcons)); |
| 453 | renderPreviousIcons(); | |
| 454 | ||
| 455 | // Show feedback | |
| 456 | const originalText = resetIconsButton.textContent; | |
| 457 | resetIconsButton.textContent = "Reset!"; | |
| 458 | resetIconsButton.style.background = "#4CAF50"; | |
| 459 | setTimeout(() => { | |
| 460 | resetIconsButton.textContent = originalText; | |
| 461 | resetIconsButton.style.background = "#ddd"; | |
| 462 | }, 2000); | |
| 463 | ||
| 464 | console.log('Reset to default icons'); | |
| 465 | } | |
| 466 | ||
f86ea817 2025-07-22 Pavel Safronov | 467 | function updateFavicon(iconUrl) { |
| 468 | console.debug('Updating favicon with URL:', iconUrl); | |
| 469 | faviconElement.href = iconUrl; | |
| 470 | } | |
8d5ecd8e 2022-01-20 Pavel Safronov | 471 | |
f86ea817 2025-07-22 Pavel Safronov | 472 | function savePreviousIcon(iconUrl) { |
| 473 | // Skip default favicon and data URLs for data:image/x-icon | |
| 474 | if (iconUrl.startsWith('data:image/x-icon')) { | |
| 475 | return; | |
| 476 | } | |
8d5ecd8e 2022-01-20 Pavel Safronov | 477 | |
f86ea817 2025-07-22 Pavel Safronov | 478 | let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); |
8d5ecd8e 2022-01-20 Pavel Safronov | 479 | |
f86ea817 2025-07-22 Pavel Safronov | 480 | // Remove the icon if it already exists to avoid duplicates |
| 481 | previousIcons = previousIcons.filter(icon => icon !== iconUrl); | |
8d5ecd8e 2022-01-20 Pavel Safronov | 482 | |
f86ea817 2025-07-22 Pavel Safronov | 483 | // Add the new icon to the beginning |
| 484 | previousIcons.unshift(iconUrl); | |
37a3afd5 2022-08-29 Pavel Safronov | 485 | |
f86ea817 2025-07-22 Pavel Safronov | 486 | // Limit to maxPreviousIcons |
| 487 | if (previousIcons.length > maxPreviousIcons) { | |
| 488 | previousIcons = previousIcons.slice(0, maxPreviousIcons); | |
| 489 | } | |
| 490 | ||
| 491 | localStorage.setItem(previousIconsKey, JSON.stringify(previousIcons)); | |
| 492 | renderPreviousIcons(); | |
| 493 | } | |
| 494 | ||
| 495 | function removePreviousIcon(iconUrl) { | |
| 496 | let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); | |
| 497 | previousIcons = previousIcons.filter(icon => icon !== iconUrl); | |
| 498 | localStorage.setItem(previousIconsKey, JSON.stringify(previousIcons)); | |
| 499 | renderPreviousIcons(); | |
| 500 | } | |
| 501 | ||
| 502 | function renderPreviousIcons() { | |
| 503 | const previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); | |
| 504 | previousIconsContainer.innerHTML = ""; | |
| 505 | ||
89807632 2025-07-23 Pavel Safronov | 506 | previousIcons.forEach((iconUrl, index) => { |
f86ea817 2025-07-22 Pavel Safronov | 507 | const iconElement = document.createElement("div"); |
| 508 | iconElement.className = "previous-icon"; | |
| 509 | iconElement.style.backgroundImage = `url('${iconUrl}')`; | |
78f9b326 2025-07-22 Pavel Safronov | 510 | iconElement.title = `Click to use • Shift+Click to remove • Ctrl+Click to download`; |
| 511 | iconElement.addEventListener("click", async (event) => { | |
f86ea817 2025-07-22 Pavel Safronov | 512 | if (event.shiftKey) { |
| 513 | removePreviousIcon(iconUrl); | |
78f9b326 2025-07-22 Pavel Safronov | 514 | } else if (event.ctrlKey || event.metaKey) { |
| 515 | // Download the individual icon | |
| 516 | event.preventDefault(); | |
| 517 | const filename = `icon-${index + 1}.png`; | |
| 518 | await downloadImageAsFile(iconUrl, filename); | |
f86ea817 2025-07-22 Pavel Safronov | 519 | } else { |
| 520 | handleIconChange(iconUrl); | |
| 521 | } | |
| 522 | }); | |
| 523 | previousIconsContainer.appendChild(iconElement); | |
| 524 | }); | |
| 525 | } | |
| 526 | ||
d981c94f 2025-07-22 Pavel Safronov | 527 | async function downloadImageAsFile(url, filename) { |
| 528 | try { | |
| 529 | // If it's a data URL, we can download it directly | |
| 530 | if (url.startsWith('data:')) { | |
| 531 | const link = document.createElement('a'); | |
| 532 | link.href = url; | |
| 533 | link.download = filename; | |
| 534 | document.body.appendChild(link); | |
| 535 | link.click(); | |
| 536 | document.body.removeChild(link); | |
| 537 | return true; | |
| 538 | } | |
| 539 | ||
| 540 | // For external URLs, we need to fetch and convert | |
| 541 | const response = await fetch(url); | |
| 542 | const blob = await response.blob(); | |
| 543 | ||
| 544 | // Convert to PNG if it's not already | |
| 545 | const canvas = document.createElement('canvas'); | |
| 546 | const ctx = canvas.getContext('2d'); | |
| 547 | const img = new Image(); | |
| 548 | ||
| 549 | return new Promise((resolve) => { | |
| 550 | img.onload = () => { | |
| 551 | canvas.width = img.width; | |
| 552 | canvas.height = img.height; | |
| 553 | ctx.drawImage(img, 0, 0); | |
| 554 | ||
| 555 | canvas.toBlob((pngBlob) => { | |
| 556 | const link = document.createElement('a'); | |
| 557 | link.href = URL.createObjectURL(pngBlob); | |
| 558 | link.download = filename; | |
| 559 | document.body.appendChild(link); | |
| 560 | link.click(); | |
| 561 | document.body.removeChild(link); | |
| 562 | URL.revokeObjectURL(link.href); | |
| 563 | resolve(true); | |
| 564 | }, 'image/png'); | |
| 565 | }; | |
| 566 | ||
| 567 | img.onerror = () => { | |
| 568 | console.warn(`Failed to load image: ${url}`); | |
| 569 | resolve(false); | |
| 570 | }; | |
| 571 | ||
| 572 | img.crossOrigin = 'anonymous'; | |
| 573 | img.src = URL.createObjectURL(blob); | |
| 574 | }); | |
| 575 | } catch (error) { | |
| 576 | console.warn(`Failed to download ${url}:`, error); | |
| 577 | return false; | |
| 578 | } | |
| 579 | } | |
| 580 | ||
f86ea817 2025-07-22 Pavel Safronov | 581 | function saveTabData() { |
| 582 | const data = { | |
| 583 | id: tabId, | |
| 584 | icon: faviconElement.href, | |
| 585 | title: titleElement.innerText, | |
| 586 | description: descriptionElement.value, | |
| 587 | }; | |
| 588 | console.debug('Saving tab data to localStorage:', data); | |
| 589 | renderAll(data); | |
| 590 | localStorage.setItem(localStorageKey, JSON.stringify(data)); | |
| 591 | } | |
| 592 | ||
| 593 | function renderAll(data) { | |
| 594 | iconPreview.style.backgroundImage = `url('${data.icon}')`; | |
| 595 | if (titleElement.innerText !== data.title) { | |
| 596 | titleElement.innerText = data.title; | |
| 597 | } | |
| 598 | document.title = titleElement.innerText || "Separator"; | |
| 599 | renderDescription(data.description); | |
| 600 | updateFavicon(data.icon); | |
| 601 | } | |
| 602 | ||
| 603 | function renderDescription(description) { | |
| 604 | descriptionElement.value = description; | |
| 605 | descriptionElement.style.height = ""; | |
| 606 | descriptionElement.style.height = `${descriptionElement.scrollHeight}px`; | |
| 607 | const html = converter.makeHtml(description); | |
| 608 | descriptionRendered.innerHTML = html; | |
89528c89 2025-07-30 Pavel Safronov | 609 | // update all rendered links to open in a new tab |
| 610 | const links = descriptionRendered.querySelectorAll("a"); | |
| 611 | links.forEach(link => { | |
| 612 | link.setAttribute("target", "_blank"); | |
| 613 | link.setAttribute("rel", "noopener noreferrer"); | |
| 614 | }); | |
f86ea817 2025-07-22 Pavel Safronov | 615 | } |
| 616 | ||
| 617 | function handleIconChange(iconUrl) { | |
| 618 | iconPreview.style.backgroundImage = `url('${iconUrl}')`; | |
| 619 | updateFavicon(iconUrl); | |
| 620 | savePreviousIcon(iconUrl); | |
| 621 | saveTabData(); | |
| 622 | } | |
| 623 | ||
| 624 | // Toggle edit mode for description | |
| 625 | editDescriptionButton.addEventListener("click", () => { | |
| 626 | const isEditing = descriptionElement.style.display === "block"; | |
| 627 | ||
| 628 | console.debug('Toggling edit mode:', isEditing ? 'Done' : 'Edit'); | |
| 629 | descriptionElement.style.display = isEditing ? "none" : "block"; | |
| 630 | descriptionRendered.style.display = isEditing ? "block" : "none"; | |
| 631 | editDescriptionButton.innerText = isEditing ? "Edit" : "Done"; | |
| 632 | ||
| 633 | // renderDescription(); | |
| 634 | saveTabData(); | |
| 635 | }); | |
| 636 | ||
| 637 | // Drop area handling | |
| 638 | dropArea.addEventListener("dragover", (event) => { | |
| 639 | event.preventDefault(); | |
| 640 | dropArea.classList.add("dragover"); | |
| 641 | }); | |
| 642 | ||
| 643 | dropArea.addEventListener("dragleave", () => { | |
| 644 | dropArea.classList.remove("dragover"); | |
| 645 | }); | |
| 646 | ||
| 647 | dropArea.addEventListener("drop", (event) => { | |
| 648 | event.preventDefault(); | |
| 649 | dropArea.classList.remove("dragover"); | |
| 650 | const file = event.dataTransfer.files[0]; | |
| 651 | if (file && file.type.startsWith("image/")) { | |
| 652 | const reader = new FileReader(); | |
| 653 | reader.onload = () => handleIconChange(reader.result); | |
| 654 | reader.readAsDataURL(file); | |
| 655 | } | |
| 656 | }); | |
| 657 | ||
| 658 | // Clipboard paste handling | |
| 659 | document.addEventListener("paste", (event) => { | |
| 660 | const clipboardItems = event.clipboardData.items; | |
| 661 | for (const item of clipboardItems) { | |
| 662 | if (item.type.startsWith("image/")) { | |
| 663 | const blob = item.getAsFile(); | |
| 664 | const reader = new FileReader(); | |
| 665 | reader.onload = () => handleIconChange(reader.result); | |
| 666 | reader.readAsDataURL(blob); | |
| 667 | break; | |
| 668 | } | |
| 669 | } | |
| 670 | }); | |
| 671 | ||
| 672 | // Apply icon from URL | |
| 673 | applyIconButton.addEventListener("click", () => { | |
| 674 | const iconUrl = iconUrlInput.value.trim(); | |
| 675 | if (iconUrl) { | |
| 676 | handleIconChange(iconUrl); | |
| 677 | } | |
| 678 | }); | |
| 679 | ||
14b80d45 2025-07-22 Pavel Safronov | 680 | // Reset icons to defaults |
| 681 | resetIconsButton.addEventListener("click", resetToDefaultIcons); | |
d981c94f 2025-07-22 Pavel Safronov | 682 | |
f86ea817 2025-07-22 Pavel Safronov | 683 | // Title and description event listeners |
| 684 | titleElement.addEventListener("input", () => { | |
| 685 | saveTabData(); | |
| 686 | }); | |
| 687 | </script> | |
| 688 | ||
| 689 | </body> | |
8d5ecd8e 2022-01-20 Pavel Safronov | 690 | |
f86ea817 2025-07-22 Pavel Safronov | 691 | </html> |