tools / files / separator-page / index.html
index.html
| 1 | <!DOCTYPE html> |
| 2 | <html> |
| 3 | |
| 4 | <head> |
| 5 | <meta charset="utf-8" /> |
| 6 | <title>Separator</title> |
| 7 | <link rel="icon" id="favicon" /> |
| 8 | <script src="https://cdn.jsdelivr.net/npm/showdown/dist/showdown.min.js"></script> |
| 9 | <script src="https://apps.fuzzyworld.net/tools/separator-page/lz-string.js"></script> |
| 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; |
| 19 | height: 100%; |
| 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; |
| 45 | background-size: contain; |
| 46 | background-repeat: no-repeat; |
| 47 | background-position: center; |
| 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> |
| 188 | |
| 189 | <hr /> |
| 190 | |
| 191 | <div class="description-container"> |
| 192 | <textarea id="description" class="description" placeholder="Write your Markdown here..."># Info |
| 193 | |
| 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 |
| 200 | 1. To edit the description, click the "Edit" button |
| 201 | 1. Enter Markdown text |
| 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 |
| 208 | technically *some* scripts can get your local separator page info |
| 209 | </textarea> |
| 210 | <div id="description-rendered" class="description-rendered"></div> |
| 211 | <button id="edit-description" class="edit-button">Edit</button> |
| 212 | </div> |
| 213 | |
| 214 | <hr /> |
| 215 | |
| 216 | <div class="previous-icons-container"> |
| 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;"> |
| 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> |
| 222 | </div> |
| 223 | </div> |
| 224 | <div class="previous-icons" id="previous-icons"> |
| 225 | <!-- Previous icons will be dynamically added here --> |
| 226 | </div> |
| 227 | </div> |
| 228 | |
| 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> |
| 236 | |
| 237 | <div class="info"> |
| 238 | Need icons? Check out <a href="https://flaticon.com" target="_blank">Flaticon</a>. |
| 239 | <br /> |
| 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>. |
| 244 | <br /> |
| 245 | <a href="https://code.fuzzyworld.net/tools/tree/separator-page/" target="_blank">Source repo</a>. |
| 246 | </div> |
| 247 | </div> |
| 248 | |
| 249 | <script> |
| 250 | const converter = new showdown.Converter(); |
| 251 | |
| 252 | /** @type {string} */ |
| 253 | const tabId = new URLSearchParams(window.location.search).get("tab") || Math.floor(Date.now() / 1000).toString(); |
| 254 | |
| 255 | /** @type {string} */ |
| 256 | const localStorageKey = `separator-tab-${tabId}`; |
| 257 | |
| 258 | /** @type {HTMLElement} */ |
| 259 | const iconPreview = document.getElementById("icon-preview"); |
| 260 | |
| 261 | /** @type {HTMLElement} */ |
| 262 | const titleElement = document.getElementById("title"); |
| 263 | |
| 264 | /** @type {HTMLTextAreaElement} */ |
| 265 | const descriptionElement = document.getElementById("description"); |
| 266 | |
| 267 | /** @type {HTMLElement} */ |
| 268 | const descriptionRendered = document.getElementById("description-rendered"); |
| 269 | |
| 270 | /** @type {HTMLButtonElement} */ |
| 271 | const editDescriptionButton = document.getElementById("edit-description"); |
| 272 | |
| 273 | /** @type {HTMLInputElement} */ |
| 274 | const iconUrlInput = document.getElementById("icon-url-input"); |
| 275 | |
| 276 | /** @type {HTMLButtonElement} */ |
| 277 | const applyIconButton = document.getElementById("apply-icon-button"); |
| 278 | |
| 279 | /** @type {HTMLDivElement} */ |
| 280 | const dropArea = document.getElementById("drop-area"); |
| 281 | |
| 282 | /** @type {HTMLLinkElement} */ |
| 283 | const faviconElement = document.getElementById("favicon"); |
| 284 | |
| 285 | /** @type {HTMLDivElement} */ |
| 286 | const previousIconsContainer = document.getElementById("previous-icons"); |
| 287 | |
| 288 | /** @type {HTMLButtonElement} */ |
| 289 | const resetIconsButton = document.getElementById("reset-icons"); |
| 290 | |
| 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 |
| 294 | |
| 295 | // Default icons from the icons folder |
| 296 | const defaultIcons = [ |
| 297 | "./icons/template-icon-0.png", |
| 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 | |
| 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 | |
| 359 | // Load saved data |
| 360 | const savedData = JSON.parse(localStorage.getItem(localStorageKey) || "null"); |
| 361 | |
| 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 | |
| 406 | const searchParams = new URLSearchParams(window.location.search); |
| 407 | if (!searchParams.has("tab")) { |
| 408 | const newUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`; |
| 409 | history.replaceState(null, "", newUrl); |
| 410 | } |
| 411 | |
| 412 | if (searchParams.has("name")) { |
| 413 | const name = searchParams.get("name"); |
| 414 | if (name) { |
| 415 | titleElement.innerText = name; |
| 416 | document.title = name; |
| 417 | } |
| 418 | } |
| 419 | if (legacyData) { |
| 420 | renderAll(legacyData); |
| 421 | console.debug('Initialized with legacy data:', legacyData); |
| 422 | } else if (savedData) { |
| 423 | renderAll(savedData); |
| 424 | console.debug('Loaded saved data:', savedData); |
| 425 | } else { |
| 426 | handleIconChange(defaultIcons[0]); // Set the first default icon as the current icon |
| 427 | saveTabData(); |
| 428 | } |
| 429 | |
| 430 | // Initialize previous icons display |
| 431 | initializeDefaultIcons(); |
| 432 | renderPreviousIcons(); |
| 433 | |
| 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'); |
| 441 | handleIconChange(defaultIcons[0]); // Set the first default icon as the current icon |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | function resetToDefaultIcons() { |
| 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."; |
| 447 | |
| 448 | if (!confirm(confirmMessage)) { |
| 449 | return; // User cancelled, do nothing |
| 450 | } |
| 451 | |
| 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 | |
| 467 | function updateFavicon(iconUrl) { |
| 468 | console.debug('Updating favicon with URL:', iconUrl); |
| 469 | faviconElement.href = iconUrl; |
| 470 | } |
| 471 | |
| 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 | } |
| 477 | |
| 478 | let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]"); |
| 479 | |
| 480 | // Remove the icon if it already exists to avoid duplicates |
| 481 | previousIcons = previousIcons.filter(icon => icon !== iconUrl); |
| 482 | |
| 483 | // Add the new icon to the beginning |
| 484 | previousIcons.unshift(iconUrl); |
| 485 | |
| 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 | |
| 506 | previousIcons.forEach((iconUrl, index) => { |
| 507 | const iconElement = document.createElement("div"); |
| 508 | iconElement.className = "previous-icon"; |
| 509 | iconElement.style.backgroundImage = `url('${iconUrl}')`; |
| 510 | iconElement.title = `Click to use • Shift+Click to remove • Ctrl+Click to download`; |
| 511 | iconElement.addEventListener("click", async (event) => { |
| 512 | if (event.shiftKey) { |
| 513 | removePreviousIcon(iconUrl); |
| 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); |
| 519 | } else { |
| 520 | handleIconChange(iconUrl); |
| 521 | } |
| 522 | }); |
| 523 | previousIconsContainer.appendChild(iconElement); |
| 524 | }); |
| 525 | } |
| 526 | |
| 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 | |
| 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; |
| 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 | }); |
| 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 | |
| 680 | // Reset icons to defaults |
| 681 | resetIconsButton.addEventListener("click", resetToDefaultIcons); |
| 682 | |
| 683 | // Title and description event listeners |
| 684 | titleElement.addEventListener("input", () => { |
| 685 | saveTabData(); |
| 686 | }); |
| 687 | </script> |
| 688 | |
| 689 | </body> |
| 690 | |
| 691 | </html> |