fuzzyworld

tools / files / separator-page / index.html

index.html

Blame: the commit that last changed each line · file · history

8d5ecd8e 2022-01-20 Pavel Safronov1<!DOCTYPE html>
2<html>
3
f86ea817 2025-07-22 Pavel Safronov4<head>
5 <meta charset="utf-8" />
6 <title>Separator</title>
9d6bfeb8 2025-07-23 Pavel Safronov7 <link rel="icon" id="favicon" />
f86ea817 2025-07-22 Pavel Safronov8 <script src="https://cdn.jsdelivr.net/npm/showdown/dist/showdown.min.js"></script>
483a7647 2026-10-27 Pavel Safronov9 <script src="https://apps.fuzzyworld.net/tools/separator-page/lz-string.js"></script>
f86ea817 2025-07-22 Pavel Safronov10 <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 Safronov19 height: 100%;
f86ea817 2025-07-22 Pavel Safronov20 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 Safronov45 background-size: contain;
46 background-repeat: no-repeat;
47 background-position: center;
f86ea817 2025-07-22 Pavel Safronov48 }
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 Safronov188
f86ea817 2025-07-22 Pavel Safronov189 <hr />
8d5ecd8e 2022-01-20 Pavel Safronov190
f86ea817 2025-07-22 Pavel Safronov191 <div class="description-container">
192 <textarea id="description" class="description" placeholder="Write your Markdown here..."># Info
8d5ecd8e 2022-01-20 Pavel Safronov193
14b80d45 2025-07-22 Pavel Safronov194This is a separator page!
195## TODO
196
1971. Read this page
1981. Understand this page
1991. The page name, icon, and content are editable
900d6ca9 2025-07-22 Pavel Safronov2001. To edit the description, click the "Edit" button
2011. Enter Markdown text
14b80d45 2025-07-22 Pavel Safronov2021. Click "Done" button to return to "view mode"
2031. Markdown contents will be rendered as HTML
2041. Content links will always open in a new tab
2051. 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.
2061. Your data is stored in local storage, under the tab id.
2071. 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 Safronov208technically *some* scripts can get your local separator page info
14b80d45 2025-07-22 Pavel Safronov209 </textarea>
f86ea817 2025-07-22 Pavel Safronov210 <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 Safronov213
f86ea817 2025-07-22 Pavel Safronov214 <hr />
8d5ecd8e 2022-01-20 Pavel Safronov215
f86ea817 2025-07-22 Pavel Safronov216 <div class="previous-icons-container">
d981c94f 2025-07-22 Pavel Safronov217 <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 Safronov220 <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 Safronov222 </div>
223 </div>
f86ea817 2025-07-22 Pavel Safronov224 <div class="previous-icons" id="previous-icons">
225 <!-- Previous icons will be dynamically added here -->
226 </div>
227 </div>
8d5ecd8e 2022-01-20 Pavel Safronov228
f86ea817 2025-07-22 Pavel Safronov229 <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 Safronov236
f86ea817 2025-07-22 Pavel Safronov237 <div class="info">
238 Need icons? Check out <a href="https://flaticon.com" target="_blank">Flaticon</a>.
239 <br />
5fdab834 2025-07-23 Pavel Safronov240 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 Safronov244 <br />
483a7647 2026-10-27 Pavel Safronov245 <a href="https://code.fuzzyworld.net/tools/tree/separator-page/" target="_blank">Source repo</a>.
f86ea817 2025-07-22 Pavel Safronov246 </div>
247 </div>
8d5ecd8e 2022-01-20 Pavel Safronov248
f86ea817 2025-07-22 Pavel Safronov249 <script>
250 const converter = new showdown.Converter();
8d5ecd8e 2022-01-20 Pavel Safronov251
f86ea817 2025-07-22 Pavel Safronov252 /** @type {string} */
253 const tabId = new URLSearchParams(window.location.search).get("tab") || Math.floor(Date.now() / 1000).toString();
8d5ecd8e 2022-01-20 Pavel Safronov254
f86ea817 2025-07-22 Pavel Safronov255 /** @type {string} */
256 const localStorageKey = `separator-tab-${tabId}`;
8d5ecd8e 2022-01-20 Pavel Safronov257
f86ea817 2025-07-22 Pavel Safronov258 /** @type {HTMLElement} */
259 const iconPreview = document.getElementById("icon-preview");
8d5ecd8e 2022-01-20 Pavel Safronov260
f86ea817 2025-07-22 Pavel Safronov261 /** @type {HTMLElement} */
262 const titleElement = document.getElementById("title");
8d5ecd8e 2022-01-20 Pavel Safronov263
f86ea817 2025-07-22 Pavel Safronov264 /** @type {HTMLTextAreaElement} */
265 const descriptionElement = document.getElementById("description");
8d5ecd8e 2022-01-20 Pavel Safronov266
f86ea817 2025-07-22 Pavel Safronov267 /** @type {HTMLElement} */
268 const descriptionRendered = document.getElementById("description-rendered");
8d5ecd8e 2022-01-20 Pavel Safronov269
f86ea817 2025-07-22 Pavel Safronov270 /** @type {HTMLButtonElement} */
271 const editDescriptionButton = document.getElementById("edit-description");
8d5ecd8e 2022-01-20 Pavel Safronov272
f86ea817 2025-07-22 Pavel Safronov273 /** @type {HTMLInputElement} */
274 const iconUrlInput = document.getElementById("icon-url-input");
8d5ecd8e 2022-01-20 Pavel Safronov275
f86ea817 2025-07-22 Pavel Safronov276 /** @type {HTMLButtonElement} */
277 const applyIconButton = document.getElementById("apply-icon-button");
8d5ecd8e 2022-01-20 Pavel Safronov278
f86ea817 2025-07-22 Pavel Safronov279 /** @type {HTMLDivElement} */
280 const dropArea = document.getElementById("drop-area");
8d5ecd8e 2022-01-20 Pavel Safronov281
f86ea817 2025-07-22 Pavel Safronov282 /** @type {HTMLLinkElement} */
283 const faviconElement = document.getElementById("favicon");
8d5ecd8e 2022-01-20 Pavel Safronov284
f86ea817 2025-07-22 Pavel Safronov285 /** @type {HTMLDivElement} */
286 const previousIconsContainer = document.getElementById("previous-icons");
8d5ecd8e 2022-01-20 Pavel Safronov287
14b80d45 2025-07-22 Pavel Safronov288 /** @type {HTMLButtonElement} */
289 const resetIconsButton = document.getElementById("reset-icons");
290
f86ea817 2025-07-22 Pavel Safronov291 // 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 Safronov294
14b80d45 2025-07-22 Pavel Safronov295 // Default icons from the icons folder
296 const defaultIcons = [
78f9b326 2025-07-22 Pavel Safronov297 "./icons/template-icon-0.png",
14b80d45 2025-07-22 Pavel Safronov298 "./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 Safronov313 // 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 Safronov359 // Load saved data
360 const savedData = JSON.parse(localStorage.getItem(localStorageKey) || "null");
8d5ecd8e 2022-01-20 Pavel Safronov361
e88c23cc 2025-07-23 Pavel Safronov362 // 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 Safronov406 const searchParams = new URLSearchParams(window.location.search);
407 if (!searchParams.has("tab")) {
f86ea817 2025-07-22 Pavel Safronov408 const newUrl = `${window.location.origin}${window.location.pathname}?tab=${tabId}`;
409 history.replaceState(null, "", newUrl);
410 }
8d5ecd8e 2022-01-20 Pavel Safronov411
78f9b326 2025-07-22 Pavel Safronov412 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 Safronov419 if (legacyData) {
420 renderAll(legacyData);
421 console.debug('Initialized with legacy data:', legacyData);
422 } else if (savedData) {
f86ea817 2025-07-22 Pavel Safronov423 renderAll(savedData);
424 console.debug('Loaded saved data:', savedData);
425 } else {
0e04e2d5 2025-07-23 Pavel Safronov426 handleIconChange(defaultIcons[0]); // Set the first default icon as the current icon
f86ea817 2025-07-22 Pavel Safronov427 saveTabData();
428 }
8d5ecd8e 2022-01-20 Pavel Safronov429
f86ea817 2025-07-22 Pavel Safronov430 // Initialize previous icons display
14b80d45 2025-07-22 Pavel Safronov431 initializeDefaultIcons();
f86ea817 2025-07-22 Pavel Safronov432 renderPreviousIcons();
8d5ecd8e 2022-01-20 Pavel Safronov433
14b80d45 2025-07-22 Pavel Safronov434 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 Safronov441 handleIconChange(defaultIcons[0]); // Set the first default icon as the current icon
14b80d45 2025-07-22 Pavel Safronov442 }
443 }
444
445 function resetToDefaultIcons() {
89807632 2025-07-23 Pavel Safronov446 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 Safronov447
89807632 2025-07-23 Pavel Safronov448 if (!confirm(confirmMessage)) {
449 return; // User cancelled, do nothing
450 }
451
14b80d45 2025-07-22 Pavel Safronov452 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 Safronov467 function updateFavicon(iconUrl) {
468 console.debug('Updating favicon with URL:', iconUrl);
469 faviconElement.href = iconUrl;
470 }
8d5ecd8e 2022-01-20 Pavel Safronov471
f86ea817 2025-07-22 Pavel Safronov472 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 Safronov477
f86ea817 2025-07-22 Pavel Safronov478 let previousIcons = JSON.parse(localStorage.getItem(previousIconsKey) || "[]");
8d5ecd8e 2022-01-20 Pavel Safronov479
f86ea817 2025-07-22 Pavel Safronov480 // Remove the icon if it already exists to avoid duplicates
481 previousIcons = previousIcons.filter(icon => icon !== iconUrl);
8d5ecd8e 2022-01-20 Pavel Safronov482
f86ea817 2025-07-22 Pavel Safronov483 // Add the new icon to the beginning
484 previousIcons.unshift(iconUrl);
37a3afd5 2022-08-29 Pavel Safronov485
f86ea817 2025-07-22 Pavel Safronov486 // 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 Safronov506 previousIcons.forEach((iconUrl, index) => {
f86ea817 2025-07-22 Pavel Safronov507 const iconElement = document.createElement("div");
508 iconElement.className = "previous-icon";
509 iconElement.style.backgroundImage = `url('${iconUrl}')`;
78f9b326 2025-07-22 Pavel Safronov510 iconElement.title = `Click to use • Shift+Click to remove • Ctrl+Click to download`;
511 iconElement.addEventListener("click", async (event) => {
f86ea817 2025-07-22 Pavel Safronov512 if (event.shiftKey) {
513 removePreviousIcon(iconUrl);
78f9b326 2025-07-22 Pavel Safronov514 } 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 Safronov519 } else {
520 handleIconChange(iconUrl);
521 }
522 });
523 previousIconsContainer.appendChild(iconElement);
524 });
525 }
526
d981c94f 2025-07-22 Pavel Safronov527 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 Safronov581 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 Safronov609 // 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 Safronov615 }
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 Safronov680 // Reset icons to defaults
681 resetIconsButton.addEventListener("click", resetToDefaultIcons);
d981c94f 2025-07-22 Pavel Safronov682
f86ea817 2025-07-22 Pavel Safronov683 // Title and description event listeners
684 titleElement.addEventListener("input", () => {
685 saveTabData();
686 });
687 </script>
688
689</body>
8d5ecd8e 2022-01-20 Pavel Safronov690
f86ea817 2025-07-22 Pavel Safronov691</html>