From 94853d33bf9db0fc3ccd663ec5cae0566c4a390e Mon Sep 17 00:00:00 2001 From: rodrick-mpofu Date: Tue, 25 Mar 2025 02:24:20 -0400 Subject: [PATCH 1/8] Implement link shortcut feature (refs #682) --- mods/mods_available/web-shortcuts/index.js | 240 ++++ .../web-shortcuts/manifest.json | 11 + mods/mods_available/web-shortcuts/module.js | 41 + .../mods_available/web-shortcuts/package.json | 11 + .../web-shortcuts/public/main.js | 197 +++ package-lock.json | 305 +++-- package.json | 2 + src/gui/src/helpers/item_icon.js | 56 + src/gui/src/helpers/new_context_menu_item.js | 1061 ++++++++++++++++- src/gui/src/helpers/open_item.js | 456 +++++++ src/gui/src/lib/mime.js | 12 + 11 files changed, 2322 insertions(+), 70 deletions(-) create mode 100644 mods/mods_available/web-shortcuts/index.js create mode 100644 mods/mods_available/web-shortcuts/manifest.json create mode 100644 mods/mods_available/web-shortcuts/module.js create mode 100644 mods/mods_available/web-shortcuts/package.json create mode 100644 mods/mods_available/web-shortcuts/public/main.js diff --git a/mods/mods_available/web-shortcuts/index.js b/mods/mods_available/web-shortcuts/index.js new file mode 100644 index 000000000..630149ddb --- /dev/null +++ b/mods/mods_available/web-shortcuts/index.js @@ -0,0 +1,240 @@ +/** + * Web Shortcuts Mod for Puter + * + * This mod adds a "Create Web Shortcut" option to the context menu + * that allows users to create .weblink files that open websites. + */ + +const BaseService = require("../../../src/backend/src/services/BaseService"); + +class WebShortcutsService extends BaseService { + async _init() { + const svc_puterHomepage = this.services.get('puter-homepage'); + svc_puterHomepage.register_script('/web-shortcuts/main.js'); + } +} + +// Function to extract URL from text (handles pasted URLs) +function extractURL(text) { + // Remove any warning messages that might be in the pasted content + const cleanText = text.replace(/⚠️Warning⚠️.*?(?=http)/s, '').trim(); + + // Try to find a URL in the text + const urlRegex = /(https?:\/\/[^\s]+)/g; + const matches = cleanText.match(urlRegex); + + if (matches && matches.length > 0) { + return matches[0]; + } + + return text; +} + +// Function to validate URL +function isValidURL(url) { + try { + new URL(url); + return true; + } catch (error) { + return false; + } +} + +// Function to create a web shortcut +async function createWebShortcut(targetPath, url = null) { + try { + // If no URL provided, prompt the user + if (!url) { + let userInput = prompt('Enter or paste the URL for the web shortcut:', 'https://example.com'); + if (!userInput) { + console.log('User cancelled URL input'); + return; + } + url = extractURL(userInput); + } + + // Ensure URL has protocol + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + // Validate URL + if (!isValidURL(url)) { + console.error('Invalid URL:', url); + alert('Invalid URL. Please enter a valid URL.'); + return; + } + + // Get the website title (for the shortcut name) + const validUrl = new URL(url); + const siteName = validUrl.hostname; + + // Get the favicon + const faviconUrl = `https://www.google.com/s2/favicons?domain=${validUrl.origin}&sz=64`; + + // Create a JSON file that will store the shortcut data + const shortcutData = { + url: validUrl.href, + icon: faviconUrl, + type: 'web_shortcut' + }; + + // Create the shortcut file + const shortcutFileName = `${siteName}.weblink`; + + // Get the target path (default to desktop if not provided) + const desktopPath = window.desktop_path || '/Desktop'; + const targetDirectory = targetPath || desktopPath; + + // Write the file + const result = await window.puter.fs.write( + targetDirectory + '/' + shortcutFileName, + JSON.stringify(shortcutData), + { dedupeName: true } + ); + + console.log('Web shortcut created:', result); + } catch (error) { + console.error('Error creating web shortcut:', error); + alert('Error creating web shortcut: ' + error.message); + } +} + +// Handle URL drops on desktop +window.addEventListener('dragover', function(e) { + // Check if we're dragging over the desktop + if (!$(e.target).closest('.desktop').length) return; + + // Check if we have text/uri-list or text/plain data + if (e.dataTransfer.types.includes('text/uri-list') || + e.dataTransfer.types.includes('text/plain')) { + e.preventDefault(); + e.stopPropagation(); + } +}); + +window.addEventListener('drop', async function(e) { + // Check if we're dropping on the desktop + if (!$(e.target).closest('.desktop').length) return; + + // Check if we have text/uri-list or text/plain data + if (e.dataTransfer.types.includes('text/uri-list')) { + e.preventDefault(); + e.stopPropagation(); + + const url = e.dataTransfer.getData('text/uri-list'); + if (isValidURL(url)) { + await createWebShortcut(window.desktop_path, url); + } + } else if (e.dataTransfer.types.includes('text/plain')) { + e.preventDefault(); + e.stopPropagation(); + + const text = e.dataTransfer.getData('text/plain'); + const url = extractURL(text); + if (isValidURL(url)) { + await createWebShortcut(window.desktop_path, url); + } + } +}); + +// Handle URL pastes on desktop +window.addEventListener('paste', async function(e) { + // Check if we're pasting on the desktop + if (!$(e.target).closest('.desktop').length) return; + + const text = e.clipboardData.getData('text/plain'); + const url = extractURL(text); + + if (isValidURL(url)) { + e.preventDefault(); + e.stopPropagation(); + await createWebShortcut(window.desktop_path, url); + } +}); + +// Add "Create Web Shortcut" to the desktop context menu +window.addEventListener('ctxmenu-will-open', function(e) { + const options = e.detail.options; + + // Only add to desktop context menu or directory context menus + if (!options || !options.items) return; + + // Check if this is a desktop or directory context menu + const isDesktopOrDirMenu = options.items.some(item => + (item.html === 'New Folder' || item.html === i18n('new_folder')) || + (item.html === 'Paste' || item.html === i18n('paste')) + ); + + if (isDesktopOrDirMenu) { + // Find the position to insert our menu item (after "New Folder") + let insertIndex = options.items.findIndex(item => + item.html === 'New Folder' || item.html === i18n('new_folder') + ); + + if (insertIndex === -1) { + // If "New Folder" not found, insert at the beginning + insertIndex = 0; + } else { + // Insert after "New Folder" + insertIndex += 1; + } + + // Get the target path + let targetPath; + if (options.parent_element) { + const $parentElement = $(options.parent_element); + if ($parentElement.hasClass('item-container')) { + targetPath = $parentElement.attr('data-path'); + } else if ($parentElement.hasClass('item') && $parentElement.attr('data-is_dir') === '1') { + targetPath = $parentElement.attr('data-path'); + } + } + + // Insert our menu item + options.items.splice(insertIndex, 0, { + html: 'Create Web Shortcut', + icon: '', + onClick: function() { + createWebShortcut(targetPath); + } + }); + } +}); + +// Add "Create Web Shortcut" to the "New" submenu in the desktop context menu +const originalUIContextMenu = window.UIContextMenu; +window.UIContextMenu = function(options) { + if (options && options.items) { + // Find the "New" submenu + const newItemIndex = options.items.findIndex(item => + (item.html === 'New' || item.html === i18n('new')) && + Array.isArray(item.items) + ); + + if (newItemIndex !== -1 && options.items[newItemIndex].items) { + // Add our item to the "New" submenu + options.items[newItemIndex].items.push({ + html: 'Web Shortcut', + icon: '', + onClick: function() { + // Get the target path + let targetPath; + if (options.parent_element) { + const $parentElement = $(options.parent_element); + if ($parentElement.hasClass('item-container')) { + targetPath = $parentElement.attr('data-path'); + } else if ($parentElement.hasClass('item') && $parentElement.attr('data-is_dir') === '1') { + targetPath = $parentElement.attr('data-path'); + } + } + createWebShortcut(targetPath); + } + }); + } + } + + return originalUIContextMenu(options); +}; + +module.exports = WebShortcutsService; \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/manifest.json b/mods/mods_available/web-shortcuts/manifest.json new file mode 100644 index 000000000..b6ff9617b --- /dev/null +++ b/mods/mods_available/web-shortcuts/manifest.json @@ -0,0 +1,11 @@ +{ + "name": "web-shortcuts", + "version": "1.0.0", + "description": "Create web shortcuts on the desktop", + "author": "Puter", + "license": "MIT", + "dependencies": ["fs", "path"], + "client": { + "scripts": ["/mods/web-shortcuts"] + } +} \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/module.js b/mods/mods_available/web-shortcuts/module.js new file mode 100644 index 000000000..bf8adfcaa --- /dev/null +++ b/mods/mods_available/web-shortcuts/module.js @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const WebShortcutsService = require('./index'); + +module.exports = { + name: 'web-shortcuts', + version: '1.0.0', + description: 'Create web shortcuts on the desktop', + author: 'Puter', + license: 'MIT', + dependencies: ['fs', 'path'], + init: async (puter) => { + const service = new WebShortcutsService(puter); + await service.init(); + return service; + }, + routes: { + '/mods/web-shortcuts': { + GET: (req, res) => { + res.sendFile(__dirname + '/public/main.js'); + } + } + } +}; \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/package.json b/mods/mods_available/web-shortcuts/package.json new file mode 100644 index 000000000..488d32a19 --- /dev/null +++ b/mods/mods_available/web-shortcuts/package.json @@ -0,0 +1,11 @@ +{ + "name": "web-shortcuts", + "version": "1.0.0", + "description": "Create web shortcuts on the desktop", + "author": "Puter", + "license": "MIT", + "dependencies": { + "fs": "*", + "path": "*" + } +} \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/public/main.js b/mods/mods_available/web-shortcuts/public/main.js new file mode 100644 index 000000000..b98e86cd8 --- /dev/null +++ b/mods/mods_available/web-shortcuts/public/main.js @@ -0,0 +1,197 @@ +// Web Shortcuts Mod +(function() { + console.log('[Web Shortcuts] Mod initializing...'); + + // URL validation helper + function isValidURL(str) { + const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain + '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path + '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string + '(\\#[-a-z\\d_]*)?$','i'); // fragment + return !!pattern.test(str); + } + + // Extract URL from text + function extractURL(text) { + // Clean the text + text = text.trim(); + + // If it's already a valid URL, return it + if (isValidURL(text)) { + return text; + } + + // Try to extract a URL + const urlRegex = /(https?:\/\/[^\s]+)/g; + const matches = text.match(urlRegex); + return matches ? matches[0] : null; + } + + // Create web shortcut + async function createWebShortcut(url, name = null) { + console.log('[Web Shortcuts] Creating shortcut with URL:', url); + try { + if (!url) { + url = await window.puter.prompt('Enter URL:'); + if (!url) return; + } + + // Add https:// if no protocol specified + if (!url.startsWith('http://') && !url.startsWith('https://')) { + url = 'https://' + url; + } + + if (!isValidURL(url)) { + console.log('[Web Shortcuts] Invalid URL:', url); + window.puter.alert('Invalid URL'); + return; + } + + // Get domain/hostname and build favicon link + const { hostname } = new URL(url); + const favicon = `https://www.google.com/s2/favicons?domain=${hostname}&sz=64`; + + // Use hostname as default name if none provided + if (!name) { + name = await window.puter.prompt('Enter shortcut name:', hostname); + if (!name) return; + } + + console.log('[Web Shortcuts] Creating shortcut:', { url, name, favicon }); + + const shortcutData = { + url: url, + favicon: favicon, + created: new Date().toISOString(), + type: 'link' + }; + + // Build the path for storing the shortcut + const filePath = `${window.desktop_path}/${name}.weblink`; + + // Write the file + const file = await window.puter.fs.write( + filePath, + JSON.stringify(shortcutData), + { + type: 'link', + icon: favicon + } + ); + + console.log('[Web Shortcuts] File created:', file); + + // Create the UI icon on the desktop + window.UIItem({ + appendTo: $('.desktop.item-container'), + 'data-type': 'link', + uid: file.uid, + path: filePath, + icon: favicon, + name: name, + is_dir: false, + metadata: JSON.stringify(shortcutData) + }); + + window.puter.notify('Web shortcut created successfully'); + } catch (error) { + console.error('[Web Shortcuts] Error creating shortcut:', error); + window.puter.alert('Error creating web shortcut: ' + (error.message || 'Please check the URL and try again')); + } + } + + // Add context menu items + window.addEventListener('DOMContentLoaded', () => { + console.log('[Web Shortcuts] DOM Content Loaded'); + + // Get desktop element + const el_desktop = document.querySelector('.desktop'); + console.log('[Web Shortcuts] Desktop element found:', !!el_desktop); + + if (!el_desktop) return; + + // Handle paste events + el_desktop.addEventListener('paste', (e) => { + console.log('[Web Shortcuts] Paste event on desktop:', e.target === el_desktop); + if (e.target !== el_desktop) return; + const text = e.clipboardData.getData('text'); + if (isValidURL(text)) { + e.preventDefault(); + createWebShortcut(text); + } + }); + + // Handle drop events + el_desktop.addEventListener('drop', (e) => { + console.log('[Web Shortcuts] Drop event on desktop:', e.target === el_desktop); + if (e.target !== el_desktop) return; + e.preventDefault(); + const url = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain'); + if (url && isValidURL(url)) { + createWebShortcut(url); + } + }); + + // Listen for context menu opening + window.addEventListener('ctxmenu-will-open', (e) => { + console.log('[Web Shortcuts] Context menu will open:', e.detail); + const options = e.detail.options; + + // Only modify desktop context menu + if (!options || !options.items) { + console.log('[Web Shortcuts] No menu options found'); + return; + } + + // Check if this is a desktop context menu + const isDesktopMenu = options.items.some(item => + (item.html === 'New Folder' || item.html === window.i18n('new_folder')) || + (item.html === 'Paste' || item.html === window.i18n('paste')) + ); + console.log('[Web Shortcuts] Is desktop menu:', isDesktopMenu); + + if (isDesktopMenu) { + // Find the position to insert our menu item (after "New") + const newIndex = options.items.findIndex(item => + item.html === 'New' || item.html === window.i18n('new') + ); + console.log('[Web Shortcuts] New menu index:', newIndex); + + // Insert our menu item after "New" and before the next divider + if (newIndex !== -1) { + let insertIndex = newIndex + 1; + // Find the next divider + while (insertIndex < options.items.length && options.items[insertIndex] !== '-') { + insertIndex++; + } + + console.log('[Web Shortcuts] Inserting at index:', insertIndex); + + // Insert our item before the divider + options.items.splice(insertIndex, 0, { + html: 'Create Web Shortcut', + icon: '', + onClick: () => createWebShortcut() + }); + + console.log('[Web Shortcuts] Menu items after insertion:', options.items); + } + } + }); + + // Add right-click handler to desktop + el_desktop.addEventListener('contextmenu', (e) => { + console.log('[Web Shortcuts] Context menu event on desktop:', e.target === el_desktop); + // Only handle right-clicks directly on the desktop, not on items + if (e.target !== el_desktop) return; + + // The rest of the context menu handling will be done by the ctxmenu-will-open event + }); + + console.log('[Web Shortcuts] All event listeners attached'); + }); + + console.log('[Web Shortcuts] Mod initialization complete'); +})(); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0544b0765..0e0ccda0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "javascript-time-ago": "^2.5.11", "json-colorizer": "^3.0.1", "open": "^10.1.0", + "openai": "^4.73.1", + "qs": "^6.14.0", "sharp": "^0.33.5", "sharp-bmp": "^0.1.5", "sharp-ico": "^0.1.5", @@ -2115,25 +2117,26 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -2143,9 +2146,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "dev": true, "license": "MIT", "dependencies": { @@ -2156,13 +2159,14 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" @@ -2195,9 +2199,10 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" @@ -8063,9 +8068,10 @@ } }, "node_modules/axios": { - "version": "1.7.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", - "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -8266,6 +8272,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -8506,6 +8527,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -9820,6 +9870,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -9963,12 +10027,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -9986,6 +10048,18 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==" }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -10372,6 +10446,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10976,15 +11065,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -11002,6 +11097,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/getopts": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", @@ -11162,11 +11270,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11241,21 +11350,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -13104,6 +13203,15 @@ "semver": "bin/semver.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -14002,9 +14110,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14074,6 +14186,7 @@ "version": "4.73.1", "resolved": "https://registry.npmjs.org/openai/-/openai-4.73.1.tgz", "integrity": "sha512-nWImDJBcUsqrhy7yJScXB4+iqjzbUEgzfA3un/6UnHFdwWhjX24oztj69Ped/njABfOdLcO/F7CeWTI5dt8Xmg==", + "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -14818,11 +14931,12 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -15683,14 +15797,69 @@ "license": "BSD-2-Clause" }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 817be9657..da378e583 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "javascript-time-ago": "^2.5.11", "json-colorizer": "^3.0.1", "open": "^10.1.0", + "openai": "^4.73.1", + "qs": "^6.14.0", "sharp": "^0.33.5", "sharp-bmp": "^0.1.5", "sharp-ico": "^0.1.5", diff --git a/src/gui/src/helpers/item_icon.js b/src/gui/src/helpers/item_icon.js index 18cf51836..d078c5cb8 100644 --- a/src/gui/src/helpers/item_icon.js +++ b/src/gui/src/helpers/item_icon.js @@ -194,6 +194,62 @@ const item_icon = async (fsentry)=>{ else if(fsentry.name.toLowerCase().endsWith('.xlsx')){ return {image: window.icons['file-xlsx.svg'], type: 'icon'}; } + // *.weblink + else if(fsentry.name.toLowerCase().endsWith('.weblink')){ + let faviconUrl = null; + + // First try to get icon from data attribute + if (fsentry.icon) { + faviconUrl = fsentry.icon; + } + // Then try metadata + else if (fsentry.metadata) { + try { + const metadata = JSON.parse(fsentry.metadata); + if (metadata && metadata.faviconUrl) { + faviconUrl = metadata.faviconUrl; + } else if (metadata && metadata.url) { + // If we have the URL but no favicon, generate the Google favicon URL + const urlObj = new URL(metadata.url); + faviconUrl = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; + } + } catch (e) { + console.error("Error parsing weblink metadata:", e); + } + } + // Finally try content + else if (fsentry.content) { + try { + const content = JSON.parse(fsentry.content); + if (content && content.faviconUrl) { + faviconUrl = content.faviconUrl; + } else if (content && content.url) { + // If we have the URL but no favicon, generate the Google favicon URL + const urlObj = new URL(content.url); + faviconUrl = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; + } + } catch (e) { + console.error("Error parsing weblink content:", e); + } + } + + // If we found a favicon URL, use it + if (faviconUrl) { + return { + image: faviconUrl, + type: 'icon', + onerror: function() { + // If favicon fails to load, switch to default icon + const $icons = $(`img[data-icon="${faviconUrl}"]`); + $icons.attr('src', window.icons['link.svg']); + return window.icons['link.svg']; + } + }; + } + + // Fallback to default link icon + return {image: window.icons['link.svg'], type: 'icon'}; + } // -------------------------------------------------- // Determine icon by set or derived mime type // -------------------------------------------------- diff --git a/src/gui/src/helpers/new_context_menu_item.js b/src/gui/src/helpers/new_context_menu_item.js index b496efaea..2186e5543 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -7,16 +7,266 @@ * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import UIPrompt from '../UI/UIPrompt.js'; +import UIAlert from '../UI/UIAlert.js'; +import refresh_item_container from './refresh_item_container.js'; + +// Initialize global weblink icon persistence system +(function initializeWeblinkIconSystem() { + // Create global cache for favicons if it doesn't exist + if (!window.favicon_cache) { + window.favicon_cache = {}; + } + + // Load cached favicons from localStorage + try { + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith('favicon_')) { + const domain = key.replace('favicon_', ''); + const value = localStorage.getItem(key); + window.favicon_cache[domain] = value; + } + } + } catch (e) { + console.error("Error loading cached favicons:", e); + } + + // Create a global MutationObserver to watch for new weblink items + try { + // Function to apply favicon to a weblink item + const applyFaviconToWeblinkItem = (item) => { + // Check if this is a weblink item + if (!item || !item.classList || !item.classList.contains('item')) return; + + const fileName = item.getAttribute('data-name'); + if (!fileName || !fileName.toLowerCase().endsWith('.weblink')) return; + + console.log("Found weblink item:", fileName); + + // Try to get the favicon URL from various sources + let faviconUrl = null; + + // 1. Check if the item already has a favicon URL stored + faviconUrl = item.getAttribute('data-icon-url') || + item.getAttribute('data-favicon') || + item.getAttribute('data-icon'); + + // 2. If not, try to extract the domain from the item's data + if (!faviconUrl) { + const itemUrl = item.getAttribute('data-url'); + if (itemUrl) { + try { + const urlObj = new URL(itemUrl); + const domain = urlObj.hostname; + + // Check if we have a cached favicon for this domain + faviconUrl = window.favicon_cache[domain]; + + // If not, use the default icon + if (!faviconUrl) { + faviconUrl = window.icons['link.svg']; + } + } catch (e) { + console.error("Error extracting domain from URL:", e); + faviconUrl = window.icons['link.svg']; + } + } + } + + // If we still don't have a favicon URL, use the default icon + if (!faviconUrl) { + faviconUrl = window.icons['link.svg']; + } + + console.log("Using favicon URL for weblink item:", faviconUrl); + + // Apply the favicon to the item + // 1. Find the icon element + const iconElement = item.querySelector('img.item-icon'); + if (iconElement) { + // Set the src attribute + iconElement.src = faviconUrl; + + // Add data attributes to prevent the icon from being changed + iconElement.setAttribute('data-original-icon', faviconUrl); + iconElement.setAttribute('data-icon-locked', 'true'); + + // Add !important to the style to prevent it from being overridden + iconElement.style.cssText = ` + width: 32px !important; + height: 32px !important; + content: url('${faviconUrl}') !important; + background-image: url('${faviconUrl}') !important; + `; + + console.log("Applied favicon to icon element"); + } + + // 2. Set data attributes on the item element + item.setAttribute('data-icon', faviconUrl); + item.setAttribute('data-original-icon', faviconUrl); + item.setAttribute('data-icon-locked', 'true'); + item.setAttribute('data-weblink-icon', faviconUrl); + + // 3. Add a style tag with !important rules for this specific item + const itemId = item.id || item.getAttribute('data-uid') || `weblink-${Date.now()}`; + if (!item.id) { + item.id = itemId; + } + + const styleId = `style-${itemId}`; + let styleTag = document.getElementById(styleId); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = styleId; + document.head.appendChild(styleTag); + } + + styleTag.textContent = ` + #${itemId} img.item-icon, + [data-uid="${itemId}"] img.item-icon { + content: url('${faviconUrl}') !important; + background-image: url('${faviconUrl}') !important; + } + `; + + console.log("Added style tag for weblink item"); + }; + + // Create a MutationObserver to watch for new weblink items + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Check if any new nodes were added + if (mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((node) => { + // Check if this is an element node + if (node.nodeType === Node.ELEMENT_NODE) { + // Check if this is a weblink item + if (node.classList && node.classList.contains('item')) { + const fileName = node.getAttribute('data-name'); + if (fileName && fileName.toLowerCase().endsWith('.weblink')) { + console.log("MutationObserver: Found new weblink item:", fileName); + applyFaviconToWeblinkItem(node); + } + } + + // Also check children for weblink items + const weblinkItems = node.querySelectorAll('.item[data-name$=".weblink"]'); + if (weblinkItems.length > 0) { + console.log("MutationObserver: Found new weblink items in children:", weblinkItems.length); + weblinkItems.forEach(applyFaviconToWeblinkItem); + } + } + }); + } + + // Check if any attributes were modified + if (mutation.type === 'attributes' && + mutation.attributeName === 'src' && + mutation.target.classList && + mutation.target.classList.contains('item-icon')) { + + // Check if this is a weblink item icon + const item = mutation.target.closest('.item[data-name$=".weblink"]'); + if (item) { + const originalIcon = mutation.target.getAttribute('data-original-icon'); + if (originalIcon && mutation.target.src !== originalIcon) { + console.log("MutationObserver: Weblink icon src changed, resetting"); + mutation.target.src = originalIcon; + } + } + } + }); + }); + + // Start observing the document + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['src', 'data-name'] + }); + + // Store the observer in a global variable to prevent garbage collection + window.weblinkIconObserver = observer; + + console.log("Global weblink icon persistence system initialized"); + + // Add global CSS rules to ensure all weblink icons are properly displayed + try { + console.log("Adding global CSS rules for weblink icons"); + + // Create a global style tag for weblink icons if it doesn't exist + let globalStyleTag = document.getElementById('weblink-icons-global-style'); + if (!globalStyleTag) { + globalStyleTag = document.createElement('style'); + globalStyleTag.id = 'weblink-icons-global-style'; + document.head.appendChild(globalStyleTag); + } + + // Add comprehensive CSS rules to ensure icons are displayed correctly + globalStyleTag.textContent = ` + /* Ensure weblink icons are always visible */ + .item[data-name$=".weblink"] .item-icon, + .weblink-item .item-icon, + .item[data-weblink="true"] .item-icon, + .item[data-has-custom-icon="true"] .item-icon { + visibility: visible !important; + opacity: 1 !important; + display: block !important; + } + + /* Ensure persistent icons are not overridden */ + .persistent-icon { + width: 32px !important; + height: 32px !important; + } + + /* Force immediate display of icons */ + .item[data-icon-locked="true"] .item-icon { + visibility: visible !important; + opacity: 1 !important; + display: block !important; + } + `; + + console.log("Global CSS rules added for weblink icons"); + } catch (e) { + console.error("Error adding global CSS rules:", e); + } + + // Apply favicons to existing weblink items + setTimeout(() => { + const existingWeblinkItems = document.querySelectorAll('.item[data-name$=".weblink"]'); + if (existingWeblinkItems.length > 0) { + console.log("Found existing weblink items:", existingWeblinkItems.length); + existingWeblinkItems.forEach(applyFaviconToWeblinkItem); + } + }, 500); + + // Also apply favicons periodically to catch any items that might have been missed + setInterval(() => { + const weblinkItems = document.querySelectorAll('.item[data-name$=".weblink"]'); + if (weblinkItems.length > 0) { + weblinkItems.forEach(applyFaviconToWeblinkItem); + } + }, 5000); + + } catch (e) { + console.error("Error setting up global weblink icon persistence system:", e); + } +})(); /** * Returns a context menu item to create a new folder and a variety of file types. @@ -55,6 +305,813 @@ const new_context_menu_item = function(dirname, append_to_element){ window.create_file({dirname: dirname, append_to_element: append_to_element, name: 'New File.html'}); } }, + // Web Link + { + html: 'Web Link', + icon: ``, + onClick: async function() { + // Prompt user for URL + const url = await UIPrompt({ + message: 'Enter the URL for the web link:', + placeholder: 'https://example.com', + defaultValue: 'https://', + validator: (value) => { + // Simple URL validation + return value.startsWith('http://') || value.startsWith('https://') ? + true : 'Please enter a valid URL starting with http:// or https://'; + } + }); + + if (url) { + try { + // Get a name for the link based on the URL + let linkName = 'New Link'; + try { + // Try to extract a name from the URL + const urlObj = new URL(url); + linkName = urlObj.hostname.replace(/^www\./, ''); + console.log("Extracted link name from URL:", linkName); + } catch (e) { + // If URL parsing fails, use default name + console.error("Error parsing URL:", e); + } + + // Try to fetch and preload the favicon using multiple sources + let faviconUrl = null; + try { + const urlObj = new URL(url); + const domain = urlObj.hostname; + + // Define multiple favicon sources to try + const faviconSources = [ + // Direct favicon.ico from the domain root (most reliable) + `https://${domain}/favicon.ico`, + // DuckDuckGo's favicon service + `https://icons.duckduckgo.com/ip3/${domain}.ico`, + // Google's favicon service with higher resolution + `https://www.google.com/s2/favicons?domain=${domain}&sz=128`, + // Fallback to Google's service with no size parameter + `https://www.google.com/s2/favicons?domain=${domain}`, + // Final fallback to default icon + window.icons['link.svg'] + ]; + + console.log("Attempting to fetch favicon from multiple sources"); + + // Try each source in sequence until one works + for (const source of faviconSources) { + try { + console.log("Trying favicon source:", source); + + // Skip the default icon in the validation check + if (source === window.icons['link.svg']) { + console.log("Using default icon as last resort"); + faviconUrl = source; + break; + } + + // Preload the favicon to ensure it's in the browser cache + const isValid = await new Promise((resolve) => { + const preloadImg = new Image(); + + // Set a small size to force loading + preloadImg.style.width = '1px'; + preloadImg.style.height = '1px'; + preloadImg.style.position = 'absolute'; + preloadImg.style.opacity = '0.01'; + + // Add to DOM to force loading + document.body.appendChild(preloadImg); + + preloadImg.onload = () => { + // Check if the image is a valid favicon (not a placeholder) + // Create a canvas to analyze the image + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + canvas.width = preloadImg.width; + canvas.height = preloadImg.height; + + // Draw the image to the canvas + ctx.drawImage(preloadImg, 0, 0); + + // Get the image data + let imageData; + try { + imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + } catch (e) { + console.warn("Could not get image data (CORS issue):", e); + // If we can't analyze the image due to CORS, assume it's valid + document.body.removeChild(preloadImg); + resolve(true); + return; + } + + // Check if the image is a single letter (Google's fallback) + // This is a heuristic: if most pixels are transparent or the same color, + // it's likely a placeholder with a letter + const data = imageData.data; + let transparentPixels = 0; + let colorCounts = {}; + + for (let i = 0; i < data.length; i += 4) { + // Check for transparency + if (data[i + 3] < 10) { + transparentPixels++; + } else { + // Count colors + const color = `${data[i]},${data[i + 1]},${data[i + 2]}`; + colorCounts[color] = (colorCounts[color] || 0) + 1; + } + } + + // Calculate total pixels + const totalPixels = canvas.width * canvas.height; + + // If more than 90% of pixels are transparent, it's likely not a real favicon + if (transparentPixels / totalPixels > 0.9) { + console.warn("Image is mostly transparent, likely not a real favicon"); + document.body.removeChild(preloadImg); + resolve(false); + return; + } + + // If one color dominates (>80% of non-transparent pixels), it might be a letter + const nonTransparentPixels = totalPixels - transparentPixels; + let dominantColorCount = 0; + + for (const color in colorCounts) { + if (colorCounts[color] > dominantColorCount) { + dominantColorCount = colorCounts[color]; + } + } + + // If one color is dominant and the image is small, it might be a letter + if (dominantColorCount / nonTransparentPixels > 0.8 && + canvas.width <= 64 && canvas.height <= 64) { + console.warn("Image has a dominant color, might be a letter placeholder"); + document.body.removeChild(preloadImg); + resolve(false); + return; + } + + // If we get here, the image is likely a valid favicon + console.log("Image appears to be a valid favicon"); + document.body.removeChild(preloadImg); + resolve(true); + }; + + preloadImg.onerror = () => { + console.warn("Failed to load favicon from source:", source); + if (document.body.contains(preloadImg)) { + document.body.removeChild(preloadImg); + } + resolve(false); + }; + + // Set a timeout in case the image takes too long to load + setTimeout(() => { + if (!preloadImg.complete) { + console.warn("Favicon preload timed out for source:", source); + if (document.body.contains(preloadImg)) { + document.body.removeChild(preloadImg); + } + resolve(false); + } + }, 1500); + + // Start loading the image + preloadImg.src = source; + }); + + if (isValid) { + console.log("Found valid favicon at:", source); + faviconUrl = source; + break; + } + } catch (sourceError) { + console.warn("Error trying favicon source:", sourceError); + // Continue to the next source + } + } + + // If no valid favicon was found, use the default icon + if (!faviconUrl) { + console.log("No valid favicon found, using default icon"); + faviconUrl = window.icons['link.svg']; + } + + // Create a permanent copy of the favicon in the DOM to ensure it's always available + const faviconId = `favicon-${Date.now()}-${Math.floor(Math.random() * 1000000)}`; + const faviconImg = document.createElement('img'); + faviconImg.id = faviconId; + faviconImg.src = faviconUrl; + faviconImg.style.position = 'absolute'; + faviconImg.style.width = '1px'; + faviconImg.style.height = '1px'; + faviconImg.style.opacity = '0.01'; + faviconImg.style.pointerEvents = 'none'; + faviconImg.style.left = '-9999px'; + faviconImg.setAttribute('data-permanent-favicon', 'true'); + faviconImg.setAttribute('data-domain', domain); + document.body.appendChild(faviconImg); + + // Store the favicon in a global cache + if (!window.favicon_cache) { + window.favicon_cache = {}; + } + window.favicon_cache[domain] = faviconUrl; + + // Also store in localStorage for persistence + try { + localStorage.setItem(`favicon_${domain}`, faviconUrl); + } catch (e) { + console.error("Error storing favicon in localStorage:", e); + } + + } catch (e) { + console.error("Error in favicon fetching process:", e); + faviconUrl = window.icons['link.svg']; + } + + // Store the URL and favicon in a comprehensive JSON object + const weblink_content = JSON.stringify({ + url: url, + faviconUrl: faviconUrl, + iconDataUrl: faviconUrl, // For backward compatibility + type: 'weblink', + domain: new URL(url).hostname, + created: Date.now(), + modified: Date.now(), + version: '2.0', + metadata: { + originalUrl: url, + originalFaviconUrl: faviconUrl, + linkName: linkName + } + }); + + console.log("Creating weblink file:", { + dirname: dirname, + name: linkName + '.weblink', + content: weblink_content, + url: url, + faviconUrl: faviconUrl + }); + + // Create the file with favicon - with enhanced metadata and attributes + const item = await window.create_file({ + dirname: dirname, + append_to_element: append_to_element, + name: linkName + '.weblink', + content: weblink_content, + icon: faviconUrl, + type: 'weblink', + metadata: JSON.stringify({ + faviconUrl: faviconUrl, + iconDataUrl: faviconUrl, // Add for backward compatibility + url: url, + domain: new URL(url).hostname, + timestamp: Date.now(), + version: '2.0' // Version to identify enhanced weblinks + }), + // Add comprehensive HTML attributes to force the icon to be visible + html_attributes: { + 'data-has-custom-icon': 'true', + 'data-icon-url': faviconUrl, + 'data-weblink': 'true', + 'data-favicon': faviconUrl, + 'data-original-icon': faviconUrl, + 'data-icon-locked': 'true', + 'data-weblink-icon': faviconUrl, + 'data-url': url, + 'data-domain': new URL(url).hostname, + 'data-icon-version': '2.0', + 'style': `--icon-url: url('${faviconUrl}') !important;` + }, + // Add any additional parameters that might help with visibility + force_refresh: true, + show_immediately: true, + skip_history: false, + priority: 'high', + // Add custom CSS class for targeting + class: 'weblink-item persistent-icon-item' + }); + + // Store the URL and favicon in localStorage for extra reliability + if (item) { + const uid = $(item).attr('data-uid'); + if (uid) { + localStorage.setItem('weblink_' + uid, url); + localStorage.setItem('weblink_icon_' + uid, faviconUrl); + + // Also store by domain for cross-item consistency + try { + const domain = new URL(url).hostname; + localStorage.setItem('favicon_' + domain, faviconUrl); + } catch (e) { + console.error("Error storing domain favicon:", e); + } + } + } + + // If the item was created successfully, ensure the icon is set + if (item) { + const $item = $(item); + + // Apply icon in multiple ways to ensure it's visible and persists + const applyIcon = async () => { + console.log("Applying persistent icon to item:", faviconUrl); + + // APPROACH 1: Replace the entire icon element with a persistent one + const $icon = $item.find('img.item-icon'); + if ($icon.length > 0) { + // Create a completely new image element with enhanced attributes + const newIconElement = document.createElement('img'); + newIconElement.className = 'item-icon persistent-icon'; + newIconElement.src = faviconUrl; + + // Add important styling to prevent overrides + newIconElement.style.cssText = ` + width: 32px !important; + height: 32px !important; + content: url('${faviconUrl}') !important; + background-image: url('${faviconUrl}') !important; + background-size: contain !important; + background-repeat: no-repeat !important; + background-position: center !important; + `; + + // Add data attributes to prevent the icon from being changed + newIconElement.setAttribute('data-icon', faviconUrl); + newIconElement.setAttribute('data-original-icon', faviconUrl); + newIconElement.setAttribute('data-icon-locked', 'true'); + newIconElement.setAttribute('data-weblink-icon', faviconUrl); + newIconElement.setAttribute('data-domain', new URL(url).hostname); + + // Replace the existing icon with our new one + $icon[0].parentNode.replaceChild(newIconElement, $icon[0]); + + // Handle favicon loading error + newIconElement.onerror = function() { + console.warn("Icon failed to load, using fallback"); + this.src = window.icons['link.svg']; + }; + + // Add event listener to prevent the src from being changed + newIconElement.addEventListener('load', function() { + console.log("Icon loaded successfully"); + // Force a repaint to ensure the icon is displayed + this.style.display = 'none'; + void this.offsetHeight; + this.style.display = ''; + }); + } else { + console.warn("No icon element found, creating one"); + const newIcon = document.createElement('img'); + newIcon.className = 'item-icon persistent-icon'; + newIcon.src = faviconUrl; + + // Add important styling + newIcon.style.cssText = ` + width: 32px !important; + height: 32px !important; + content: url('${faviconUrl}') !important; + background-image: url('${faviconUrl}') !important; + background-size: contain !important; + background-repeat: no-repeat !important; + background-position: center !important; + `; + + // Add data attributes + newIcon.setAttribute('data-icon', faviconUrl); + newIcon.setAttribute('data-original-icon', faviconUrl); + newIcon.setAttribute('data-icon-locked', 'true'); + + // Add to the item + $item.prepend(newIcon); + + // Handle errors + newIcon.onerror = function() { + console.warn("Icon failed to load, using fallback"); + this.src = window.icons['link.svg']; + }; + } + + // APPROACH 2: Set item attributes and inline styles + $item.attr({ + 'data-icon': faviconUrl, + 'data-url': url, + 'data-type': 'weblink', + }); + + // Add inline style with !important to force the icon + const currentStyle = $item.attr('style') || ''; + $item.attr('style', currentStyle + + `;--icon-url: url('${faviconUrl}') !important;` + + `background-image: url('${faviconUrl}') !important;`); + + // APPROACH 3: Set background image on all possible containers + const $iconContainer = $item.find('.item-icon-container'); + if ($iconContainer.length > 0) { + $iconContainer.css({ + 'background-image': `url('${faviconUrl}') !important`, + 'background-size': 'contain !important', + 'background-repeat': 'no-repeat !important', + 'background-position': 'center !important' + }); + } + + // APPROACH 4: Add a custom style tag for this specific item + const itemId = $item.attr('id') || `weblink-${Date.now()}`; + if (!$item.attr('id')) { + $item.attr('id', itemId); + } + + const styleId = `style-${itemId}`; + let $styleTag = $(`#${styleId}`); + if ($styleTag.length === 0) { + $styleTag = $(``); + $('head').append($styleTag); + } + + $styleTag.html(` + #${itemId} .item-icon { + background-image: url('${faviconUrl}') !important; + content: url('${faviconUrl}') !important; + } + #${itemId}[data-has-custom-icon="true"] .item-icon-container { + background-image: url('${faviconUrl}') !important; + } + `); + + // Force a DOM reflow to ensure the icon is displayed + void $item[0].offsetHeight; + + // Force multiple refreshes of the desktop view to ensure the file appears + console.log("Forcing multiple refreshes of the desktop view"); + await refresh_item_container(dirname); + + // Add a small delay and refresh again to ensure the file appears + await new Promise(resolve => setTimeout(resolve, 100)); + await refresh_item_container(dirname); + + // Apply the icon again after a short delay to ensure it's visible + setTimeout(() => { + const $iconAfterRefresh = $item.find('img.item-icon'); + if ($iconAfterRefresh.length > 0) { + $iconAfterRefresh.attr('src', faviconUrl); + } + console.log("Icon applied after refresh"); + }, 100); + }; + + // Apply icon immediately + await applyIcon(); + + // And apply again after short delays to ensure it sticks + setTimeout(() => { applyIcon(); }, 300); + setTimeout(() => { applyIcon(); }, 1000); + + // APPROACH 5: Direct DOM manipulation for maximum control + // This is a more aggressive approach that directly modifies the DOM + setTimeout(() => { + try { + // Find all possible icon elements related to this item + const itemId = $item.attr('id'); + const uid = $item.attr('data-uid'); + + // Query selectors to find all possible icon elements + const selectors = [ + `#${itemId} img.item-icon`, + `[data-uid="${uid}"] img.item-icon`, + `[data-uid="${uid}"] .item-icon-container img`, + `.item[data-name="${linkName}.weblink"] img.item-icon` + ]; + + // Try each selector + selectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + if (elements.length > 0) { + console.log(`Found ${elements.length} elements with selector: ${selector}`); + elements.forEach(el => { + // Force the src attribute + el.src = faviconUrl; + // Also set as a background image + el.style.backgroundImage = `url('${faviconUrl}')`; + // Force a repaint + el.style.display = 'none'; + void el.offsetHeight; + el.style.display = ''; + }); + } + }); + + // Also try to find the parent container and set its background + const containers = document.querySelectorAll(`[data-uid="${uid}"] .item-icon-container`); + containers.forEach(container => { + container.style.backgroundImage = `url('${faviconUrl}')`; + }); + + console.log("Direct DOM manipulation completed"); + } catch (e) { + console.error("Error during direct DOM manipulation:", e); + } + }, 500); + + // APPROACH 6: Use MutationObserver to watch for DOM changes + // This will ensure the icon is set correctly even if the DOM changes + try { + const uid = $item.attr('data-uid'); + if (uid) { + console.log("Setting up MutationObserver for item:", uid); + + // Create a MutationObserver to watch for changes to the DOM + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + // Check if any new nodes were added + if (mutation.addedNodes.length > 0) { + // Look for icon elements in the added nodes + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + // Check if this is an icon element + if (node.classList && node.classList.contains('item-icon')) { + console.log("MutationObserver: Found new icon element"); + node.src = faviconUrl; + } + + // Also check children + const icons = node.querySelectorAll('.item-icon'); + if (icons.length > 0) { + console.log("MutationObserver: Found new icon elements in children"); + icons.forEach(icon => { + icon.src = faviconUrl; + }); + } + } + }); + } + + // Check if any attributes were modified + if (mutation.type === 'attributes' && + mutation.attributeName === 'src' && + mutation.target.classList && + mutation.target.classList.contains('item-icon')) { + // If the src attribute of an icon was changed, set it back + if (mutation.target.src !== faviconUrl) { + console.log("MutationObserver: Icon src changed, resetting"); + mutation.target.src = faviconUrl; + } + } + }); + }); + + // Start observing the document + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['src'] + }); + + // Stop observing after 5 seconds to avoid memory leaks + setTimeout(() => { + console.log("Stopping MutationObserver"); + observer.disconnect(); + }, 5000); + } + } catch (e) { + console.error("Error setting up MutationObserver:", e); + } + + console.log("Set icon directly on item:", faviconUrl); + } + + // Store the URL in localStorage for extra reliability + if (item) { + const uid = $(item).attr('data-uid'); + if (uid) { + localStorage.setItem('weblink_' + uid, url); + } + } + + // APPROACH 7: Create a permanent visible element with the icon + // This will ensure the icon is always visible + try { + console.log("Creating permanent visible element with icon"); + + // Find the container where the file should be displayed + let container = null; + if (append_to_element) { + container = append_to_element; + } else { + // Try to find the desktop or current directory container + container = document.querySelector('.desktop, .explorer-container.active, .files-container.active'); + } + + if (container) { + console.log("Found container for permanent element"); + + // Generate a unique ID for this element + const uniqueId = `weblink-${Date.now()}-${Math.floor(Math.random() * 1000000)}`; + + // Create a permanent element with the icon + const permanentElement = document.createElement('div'); + permanentElement.id = uniqueId; + permanentElement.className = 'item weblink-item permanent-item'; + permanentElement.setAttribute('data-name', linkName + '.weblink'); + permanentElement.setAttribute('data-type', 'weblink'); + permanentElement.setAttribute('data-icon', faviconUrl); + permanentElement.setAttribute('data-url', url); + permanentElement.setAttribute('data-permanent', 'true'); + + // Add inline styles to ensure it's always visible + permanentElement.style.display = 'inline-block'; + permanentElement.style.position = 'relative'; + permanentElement.style.margin = '10px'; + permanentElement.style.textAlign = 'center'; + permanentElement.style.verticalAlign = 'top'; + permanentElement.style.width = '80px'; + permanentElement.style.height = '80px'; + permanentElement.style.zIndex = '1000'; // High z-index to ensure visibility + + // Create the icon element + const iconElement = document.createElement('img'); + iconElement.className = 'item-icon'; + iconElement.src = faviconUrl; + iconElement.style.width = '32px'; + iconElement.style.height = '32px'; + iconElement.style.display = 'block'; + iconElement.style.margin = '0 auto 5px auto'; + + // Create the name element + const nameElement = document.createElement('div'); + nameElement.className = 'item-name'; + nameElement.textContent = linkName + '.weblink'; + nameElement.style.fontSize = '12px'; + nameElement.style.wordWrap = 'break-word'; + + // Add the elements to the permanent element + permanentElement.appendChild(iconElement); + permanentElement.appendChild(nameElement); + + // Add the permanent element to the container + container.appendChild(permanentElement); + + // Add a click handler to open the URL + permanentElement.addEventListener('click', () => { + window.open(url, '_blank', 'noopener,noreferrer'); + }); + + // Add a style tag to ensure this element is always visible + const styleTag = document.createElement('style'); + styleTag.id = `style-${uniqueId}`; + styleTag.textContent = ` + #${uniqueId} { + display: inline-block !important; + visibility: visible !important; + opacity: 1 !important; + } + #${uniqueId} .item-icon { + content: url('${faviconUrl}') !important; + background-image: url('${faviconUrl}') !important; + } + `; + document.head.appendChild(styleTag); + + // Set up a MutationObserver to ensure the element is not removed + const observer = new MutationObserver((mutations) => { + // Check if our element was removed + if (!document.getElementById(uniqueId)) { + console.log("Permanent element was removed, re-adding it"); + container.appendChild(permanentElement); + } + + // Also check if the icon was changed + const icon = document.querySelector(`#${uniqueId} .item-icon`); + if (icon && icon.src !== faviconUrl) { + console.log("Icon was changed, resetting it"); + icon.src = faviconUrl; + } + }); + + // Start observing the container + observer.observe(container, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['src', 'style', 'class'] + }); + + // Keep the observer running indefinitely + // Store it in a global variable to prevent garbage collection + window.weblinkObservers = window.weblinkObservers || {}; + window.weblinkObservers[uniqueId] = observer; + + console.log("Permanent element created with ID:", uniqueId); + } + } catch (e) { + console.error("Error creating permanent element:", e); + } + + // Add global CSS rules to ensure all weblink icons are properly displayed + try { + console.log("Adding global CSS rules for weblink icons"); + + // Create a global style tag for weblink icons if it doesn't exist + let globalStyleTag = document.getElementById('weblink-icons-global-style'); + if (!globalStyleTag) { + globalStyleTag = document.createElement('style'); + globalStyleTag.id = 'weblink-icons-global-style'; + document.head.appendChild(globalStyleTag); + } + + // Add comprehensive CSS rules to ensure icons are displayed correctly + globalStyleTag.textContent = ` + /* Ensure weblink icons are always visible */ + .item[data-name$=".weblink"] .item-icon, + .weblink-item .item-icon, + .item[data-weblink="true"] .item-icon, + .item[data-has-custom-icon="true"] .item-icon { + visibility: visible !important; + opacity: 1 !important; + display: block !important; + } + + /* Ensure persistent icons are not overridden */ + .persistent-icon { + width: 32px !important; + height: 32px !important; + } + + /* Force immediate display of icons */ + .item[data-icon-locked="true"] .item-icon { + visibility: visible !important; + opacity: 1 !important; + display: block !important; + } + + /* Specific rule for this domain */ + .item[data-domain="${new URL(url).hostname}"] .item-icon { + content: url('${faviconUrl}') !important; + background-image: url('${faviconUrl}') !important; + } + `; + + console.log("Global CSS rules added for weblink icons"); + } catch (e) { + console.error("Error adding global CSS rules:", e); + } + + // Try to force a more comprehensive refresh of the file system view + try { + console.log("Attempting to force a comprehensive refresh"); + + // Try to find and trigger the refresh button if it exists + const refreshButtons = document.querySelectorAll('.refresh-btn, .refresh-button, [data-action="refresh"]'); + if (refreshButtons.length > 0) { + console.log("Found refresh button, clicking it"); + refreshButtons[0].click(); + } + + // Try to trigger a refresh event on the container + const containers = document.querySelectorAll('.explorer-container, .files-container, .desktop'); + if (containers.length > 0) { + console.log("Found container, triggering refresh event"); + $(containers[0]).trigger('refresh'); + } + + // If the append_to_element is provided, try to refresh it directly + if (append_to_element) { + console.log("Refreshing append_to_element directly"); + $(append_to_element).trigger('refresh'); + + // Also try to find its parent container and refresh that + const parentContainer = $(append_to_element).closest('.explorer-container, .files-container, .desktop'); + if (parentContainer.length > 0) { + console.log("Found parent container, triggering refresh event"); + parentContainer.trigger('refresh'); + } + } + + // As a last resort, try to reload the current directory + if (window.current_directory) { + console.log("Attempting to reload current directory"); + if (typeof window.load_directory === 'function') { + window.load_directory(window.current_directory); + } + } + } catch (e) { + console.error("Error during comprehensive refresh:", e); + } + + console.log("Created web link with URL:", url); + } catch (error) { + console.error("Error creating web link:", error); + UIAlert("Error creating web link: " + error.message); + } + } + } + }, // JPG Image { html: i18n('jpeg_image'), diff --git a/src/gui/src/helpers/open_item.js b/src/gui/src/helpers/open_item.js index 4125a2598..7342ab4c6 100644 --- a/src/gui/src/helpers/open_item.js +++ b/src/gui/src/helpers/open_item.js @@ -49,6 +49,462 @@ const open_item = async function(options){ UIAlert(`This shortcut can't be opened because its source has been deleted.`) } //---------------------------------------------------------------- + // Is this a .weblink file? + //---------------------------------------------------------------- + else if($(el_item).attr('data-name').toLowerCase().endsWith('.weblink')){ + try { + // Log the file information + console.log("Opening weblink file:", { + name: $(el_item).attr('data-name'), + path: item_path, + uid: file_uid + }); + + // First check localStorage using the file's UID + let url = null; + if (file_uid) { + url = localStorage.getItem('weblink_' + file_uid); + console.log("Retrieved URL from localStorage:", url); + } + + // Try to read the file content directly using the file's path + if (!url) { + try { + // Convert Windows-style path to unix-style path + const unixPath = item_path.replace(/\\/g, '/'); + console.log("Trying to read file content using unix-style path:", unixPath); + + // Try using the simpler puter.fs.read method with unix-style path + const content = await puter.fs.read({ + path: unixPath + }); + + console.log("File content read successfully:", content); + + // Handle different content types + if (content instanceof Blob) { + // If content is a Blob, convert it to text + console.log("Content is a Blob, converting to text"); + const text = await content.text(); + console.log("Blob converted to text:", text); + + // Try to parse the text as JSON + try { + const jsonData = JSON.parse(text); + if (jsonData.url) { + url = jsonData.url; + console.log("Retrieved URL from Blob content (JSON):", url); + + // Check for icon data in the JSON (support both old and new formats) + const iconUrl = jsonData.iconDataUrl || jsonData.faviconUrl; + if (iconUrl) { + console.log("Found icon URL in JSON:", iconUrl); + + // Use the favicon URL directly without generating a custom icon + const customIconUrl = iconUrl; + + // Store the icon URL in a global cache to ensure it persists + if (!window.weblink_icon_cache) { + window.weblink_icon_cache = {}; + } + + // Store by both UID and path for maximum reliability + if (file_uid) { + window.weblink_icon_cache[file_uid] = customIconUrl; + } + if (item_path) { + window.weblink_icon_cache[item_path] = customIconUrl; + } + + // Also store in localStorage for persistence across page refreshes + try { + localStorage.setItem(`weblink_icon_${file_uid}`, customIconUrl); + localStorage.setItem(`weblink_icon_${item_path.replace(/[^a-zA-Z0-9]/g, '_')}`, customIconUrl); + } catch (e) { + console.error("Error storing icon URL in localStorage:", e); + } + + // Create a new image element with the custom icon + const iconImg = document.createElement('img'); + iconImg.src = customIconUrl; + iconImg.className = 'item-icon persistent-icon'; + iconImg.style.width = '32px'; + iconImg.style.height = '32px'; + // Add important attributes to prevent the icon from being changed + iconImg.setAttribute('data-original-icon', customIconUrl); + iconImg.setAttribute('data-icon-locked', 'true'); + + // Apply multiple approaches to ensure the icon persists + console.log("Applying persistent icon using multiple approaches"); + + // APPROACH 1: Replace the icon element completely + const existingIcon = $(el_item).find('img.item-icon'); + let iconUpdated = false; + if (existingIcon.length > 0) { + // Create a completely new element to avoid any references to the old one + const newIconElement = document.createElement('img'); + newIconElement.className = 'item-icon persistent-icon'; + newIconElement.src = customIconUrl; + newIconElement.style.width = '32px'; + newIconElement.style.height = '32px'; + newIconElement.setAttribute('data-original-icon', customIconUrl); + newIconElement.setAttribute('data-icon-locked', 'true'); + + // Replace the existing icon with our new one + existingIcon[0].parentNode.replaceChild(newIconElement, existingIcon[0]); + console.log("Replaced icon with persistent icon (approach 1)"); + iconUpdated = true; + + // Add event listener to prevent the src from being changed + newIconElement.addEventListener('load', function() { + console.log("Icon loaded successfully"); + }); + + // Handle favicon loading error + newIconElement.addEventListener('error', function() { + console.log("Icon failed to load, using fallback"); + this.src = window.icons['link.svg']; + }); + } + + // APPROACH 2: Update all img elements inside the item + const allImgs = $(el_item).find('img'); + if (allImgs.length > 0) { + allImgs.each(function() { + $(this).attr('src', customIconUrl); + $(this).attr('data-original-icon', customIconUrl); + $(this).attr('data-icon-locked', 'true'); + // Add !important to the style to prevent it from being overridden + $(this).attr('style', `width: 32px !important; height: 32px !important;`); + }); + console.log("Updated all img elements with persistent icon (approach 2)"); + iconUpdated = true; + } + + // APPROACH 3: Add CSS styles to force the icon + // Get a unique identifier for this item + const itemId = $(el_item).attr('id') || $(el_item).attr('data-uid') || `weblink-${Date.now()}`; + if (!$(el_item).attr('id')) { + $(el_item).attr('id', itemId); + } + + // Add a style tag with !important rules + const styleId = `style-${itemId}`; + let styleTag = document.getElementById(styleId); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = styleId; + document.head.appendChild(styleTag); + } + + styleTag.textContent = ` + #${itemId} img.item-icon, + [data-uid="${file_uid}"] img.item-icon { + content: url('${customIconUrl}') !important; + background-image: url('${customIconUrl}') !important; + } + `; + + // APPROACH 4: Set data attributes on the item element + $(el_item).attr({ + 'data-icon': customIconUrl, + 'data-original-icon': customIconUrl, + 'data-icon-locked': 'true', + 'data-weblink-icon': customIconUrl + }); + + // APPROACH 5: Add a MutationObserver to ensure the icon persists + try { + // Create a MutationObserver to watch for changes to the icon + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + mutation.attributeName === 'src' && + mutation.target.classList.contains('item-icon')) { + // If the src attribute of the icon was changed, set it back + if (mutation.target.src !== customIconUrl) { + console.log("MutationObserver: Icon src changed, resetting"); + mutation.target.src = customIconUrl; + } + } + }); + }); + + // Start observing the item element + observer.observe(el_item, { + attributes: true, + childList: true, + subtree: true, + attributeFilter: ['src'] + }); + + // Store the observer in a global variable to prevent garbage collection + if (!window.icon_observers) { + window.icon_observers = {}; + } + window.icon_observers[file_uid] = observer; + + console.log("Set up MutationObserver to ensure icon persists"); + } catch (e) { + console.error("Error setting up MutationObserver:", e); + } + + // Force a refresh of the item's icon + if (!iconUpdated) { + console.log("Could not find icon element to replace, forcing refresh"); + // Try to trigger a refresh of the item + setTimeout(() => { + $(el_item).trigger('icon_update', { icon: customIconUrl }); + }, 100); + } + } + } + } catch (e) { + console.error("Error parsing Blob content as JSON:", e); + // Not valid JSON, try using the content directly + if (text && (text.startsWith('http://') || text.startsWith('https://'))) { + url = text; + console.log("Using Blob content as URL (direct):", url); + } + } + } else if (typeof content === 'string') { + // If content is a string, try to parse it as JSON + try { + const jsonData = JSON.parse(content); + if (jsonData.url) { + url = jsonData.url; + console.log("Retrieved URL from string content (JSON):", url); + + // Check for icon data in the JSON (support both old and new formats) + const iconUrl = jsonData.iconDataUrl || jsonData.faviconUrl; + if (iconUrl) { + console.log("Found icon URL in JSON:", iconUrl); + + // Use the favicon URL directly without generating a custom icon + const customIconUrl = iconUrl; + + // Store the icon URL in a global cache to ensure it persists + if (!window.weblink_icon_cache) { + window.weblink_icon_cache = {}; + } + + // Store by both UID and path for maximum reliability + if (file_uid) { + window.weblink_icon_cache[file_uid] = customIconUrl; + } + if (item_path) { + window.weblink_icon_cache[item_path] = customIconUrl; + } + + // Also store in localStorage for persistence across page refreshes + try { + localStorage.setItem(`weblink_icon_${file_uid}`, customIconUrl); + localStorage.setItem(`weblink_icon_${item_path.replace(/[^a-zA-Z0-9]/g, '_')}`, customIconUrl); + } catch (e) { + console.error("Error storing icon URL in localStorage:", e); + } + + // Apply multiple approaches to ensure the icon persists + console.log("Applying persistent icon using multiple approaches"); + + // APPROACH 1: Replace the icon element completely + const existingIcon = $(el_item).find('img.item-icon'); + let iconUpdated = false; + if (existingIcon.length > 0) { + // Create a completely new element to avoid any references to the old one + const newIconElement = document.createElement('img'); + newIconElement.className = 'item-icon persistent-icon'; + newIconElement.src = customIconUrl; + newIconElement.style.width = '32px'; + newIconElement.style.height = '32px'; + newIconElement.setAttribute('data-original-icon', customIconUrl); + newIconElement.setAttribute('data-icon-locked', 'true'); + + // Replace the existing icon with our new one + existingIcon[0].parentNode.replaceChild(newIconElement, existingIcon[0]); + console.log("Replaced icon with persistent icon (approach 1)"); + iconUpdated = true; + + // Add event listener to prevent the src from being changed + newIconElement.addEventListener('load', function() { + console.log("Icon loaded successfully"); + }); + + // Handle favicon loading error + newIconElement.addEventListener('error', function() { + console.log("Icon failed to load, using fallback"); + this.src = window.icons['link.svg']; + }); + } + + // APPROACH 2: Update all img elements inside the item + const allImgs = $(el_item).find('img'); + if (allImgs.length > 0) { + allImgs.each(function() { + $(this).attr('src', customIconUrl); + $(this).attr('data-original-icon', customIconUrl); + $(this).attr('data-icon-locked', 'true'); + // Add !important to the style to prevent it from being overridden + $(this).attr('style', `width: 32px !important; height: 32px !important;`); + }); + console.log("Updated all img elements with persistent icon (approach 2)"); + iconUpdated = true; + } + + // APPROACH 3: Add CSS styles to force the icon + // Get a unique identifier for this item + const itemId = $(el_item).attr('id') || $(el_item).attr('data-uid') || `weblink-${Date.now()}`; + if (!$(el_item).attr('id')) { + $(el_item).attr('id', itemId); + } + + // Add a style tag with !important rules + const styleId = `style-${itemId}`; + let styleTag = document.getElementById(styleId); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = styleId; + document.head.appendChild(styleTag); + } + + styleTag.textContent = ` + #${itemId} img.item-icon, + [data-uid="${file_uid}"] img.item-icon { + content: url('${customIconUrl}') !important; + background-image: url('${customIconUrl}') !important; + } + `; + + // APPROACH 4: Set data attributes on the item element + $(el_item).attr({ + 'data-icon': customIconUrl, + 'data-original-icon': customIconUrl, + 'data-icon-locked': 'true', + 'data-weblink-icon': customIconUrl + }); + + // APPROACH 5: Add a MutationObserver to ensure the icon persists + try { + // Create a MutationObserver to watch for changes to the icon + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + mutation.attributeName === 'src' && + mutation.target.classList.contains('item-icon')) { + // If the src attribute of the icon was changed, set it back + if (mutation.target.src !== customIconUrl) { + console.log("MutationObserver: Icon src changed, resetting"); + mutation.target.src = customIconUrl; + } + } + }); + }); + + // Start observing the item element + observer.observe(el_item, { + attributes: true, + childList: true, + subtree: true, + attributeFilter: ['src'] + }); + + // Store the observer in a global variable to prevent garbage collection + if (!window.icon_observers) { + window.icon_observers = {}; + } + window.icon_observers[file_uid] = observer; + + console.log("Set up MutationObserver to ensure icon persists"); + } catch (e) { + console.error("Error setting up MutationObserver:", e); + } + + // Force a refresh of the item's icon + if (!iconUpdated) { + console.log("Could not find icon element to replace, forcing refresh"); + // Try to trigger a refresh of the item + setTimeout(() => { + $(el_item).trigger('icon_update', { icon: customIconUrl }); + }, 100); + } + } + } + } catch (e) { + console.error("Error parsing string content as JSON:", e); + // Not valid JSON, try using the content directly + if (content && (content.startsWith('http://') || content.startsWith('https://'))) { + url = content; + console.log("Using string content as URL (direct):", url); + } + } + } else { + console.error("Unexpected content type:", typeof content); + } + } catch (e) { + console.error("Error reading file using path:", e); + + // Fallback to using AJAX with the file's UID + try { + console.log("Trying to read file content using UID:", file_uid); + + const content = await $.ajax({ + url: window.api_origin + "/fs/read", + type: 'POST', + contentType: "application/json", + data: JSON.stringify({ + uid: file_uid + }), + headers: { + "Authorization": "Bearer " + window.auth_token + } + }); + + console.log("File content read successfully using AJAX:", content); + + // Try to parse the content as JSON + if (content && content.content) { + try { + const jsonData = JSON.parse(content.content); + if (jsonData.url) { + url = jsonData.url; + console.log("Retrieved URL from file content (AJAX JSON):", url); + } + } catch (e) { + // Not valid JSON, try using the content directly + if (content.content && (content.content.startsWith('http://') || content.content.startsWith('https://'))) { + url = content.content; + console.log("Using file content as URL (AJAX direct):", url); + } + } + } + } catch (e) { + console.error("Error reading file using AJAX:", e); + } + } + } + + // If we have a valid URL, open it + if (url && (url.startsWith('http://') || url.startsWith('https://'))) { + console.log("Opening URL:", url); + window.open(url, '_blank', 'noopener,noreferrer'); + } else { + // Show a more detailed error message + console.error("Failed to retrieve URL from all sources"); + UIAlert(`Could not determine the URL for this web shortcut. + +Technical details: +- File name: ${$(el_item).attr('data-name')} +- File path: ${item_path} +- File UID: ${file_uid} + +Please try recreating the link.`); + } + } catch (error) { + console.error('Error opening web shortcut:', error); + UIAlert('Error opening web shortcut: ' + error.message); + } + } + //---------------------------------------------------------------- // Is this a trashed file? //---------------------------------------------------------------- else if(item_path.startsWith(window.trash_path + '/')){ diff --git a/src/gui/src/lib/mime.js b/src/gui/src/lib/mime.js index 1d17bca02..271a97bec 100644 --- a/src/gui/src/lib/mime.js +++ b/src/gui/src/lib/mime.js @@ -36,10 +36,22 @@ function Mime() { let ext = last.replace(/^.*\./, "").toLowerCase(); let hasPath = last.length < path.length; let hasDot = ext.length < last.length - 1; + + // Special case for .weblink files + if (ext === 'weblink') { + return 'application/x-weblink'; + } + return (hasDot || !hasPath) && this._types[ext] || null; }; Mime.prototype.getExtension = function(type) { type = /^\s*([^;\s]*)/.test(type) && RegExp.$1; + + // Special case for .weblink files + if (type === 'application/x-weblink') { + return 'weblink'; + } + return type && this._extensions[type.toLowerCase()] || null; }; var Mime_1 = Mime; From bf16e2044737001423e7c12c2105878c36aa9e35 Mon Sep 17 00:00:00 2001 From: rodrick-mpofu Date: Fri, 28 Mar 2025 05:17:02 -0400 Subject: [PATCH 2/8] fix: remove duplicate icons and hide .weblink extension --- src/gui/src/helpers/new_context_menu_item.js | 1257 ++++-------------- 1 file changed, 287 insertions(+), 970 deletions(-) diff --git a/src/gui/src/helpers/new_context_menu_item.js b/src/gui/src/helpers/new_context_menu_item.js index 2186e5543..8dbc2d0fa 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -21,251 +21,235 @@ import UIPrompt from '../UI/UIPrompt.js'; import UIAlert from '../UI/UIAlert.js'; import refresh_item_container from './refresh_item_container.js'; -// Initialize global weblink icon persistence system -(function initializeWeblinkIconSystem() { - // Create global cache for favicons if it doesn't exist - if (!window.favicon_cache) { - window.favicon_cache = {}; - } +// Initialize the favicon cache if it doesn't exist +window.favicon_cache = window.favicon_cache || {}; + +// Single, unified function for applying favicons to weblink items +function applyWeblinkIcon(item, faviconUrl, url) { + // Validate inputs + if (!item || !faviconUrl) return false; - // Load cached favicons from localStorage - try { - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key.startsWith('favicon_')) { - const domain = key.replace('favicon_', ''); - const value = localStorage.getItem(key); - window.favicon_cache[domain] = value; - } - } - } catch (e) { - console.error("Error loading cached favicons:", e); - } + const $item = $(item); + if (!$item.length) return false; - // Create a global MutationObserver to watch for new weblink items try { - // Function to apply favicon to a weblink item - const applyFaviconToWeblinkItem = (item) => { - // Check if this is a weblink item - if (!item || !item.classList || !item.classList.contains('item')) return; - - const fileName = item.getAttribute('data-name'); - if (!fileName || !fileName.toLowerCase().endsWith('.weblink')) return; - - console.log("Found weblink item:", fileName); - - // Try to get the favicon URL from various sources - let faviconUrl = null; - - // 1. Check if the item already has a favicon URL stored - faviconUrl = item.getAttribute('data-icon-url') || - item.getAttribute('data-favicon') || - item.getAttribute('data-icon'); - - // 2. If not, try to extract the domain from the item's data - if (!faviconUrl) { - const itemUrl = item.getAttribute('data-url'); - if (itemUrl) { - try { - const urlObj = new URL(itemUrl); - const domain = urlObj.hostname; - - // Check if we have a cached favicon for this domain - faviconUrl = window.favicon_cache[domain]; - - // If not, use the default icon - if (!faviconUrl) { - faviconUrl = window.icons['link.svg']; - } - } catch (e) { - console.error("Error extracting domain from URL:", e); - faviconUrl = window.icons['link.svg']; - } - } - } - - // If we still don't have a favicon URL, use the default icon - if (!faviconUrl) { - faviconUrl = window.icons['link.svg']; - } - - console.log("Using favicon URL for weblink item:", faviconUrl); - - // Apply the favicon to the item - // 1. Find the icon element - const iconElement = item.querySelector('img.item-icon'); - if (iconElement) { - // Set the src attribute - iconElement.src = faviconUrl; - - // Add data attributes to prevent the icon from being changed - iconElement.setAttribute('data-original-icon', faviconUrl); - iconElement.setAttribute('data-icon-locked', 'true'); - - // Add !important to the style to prevent it from being overridden - iconElement.style.cssText = ` - width: 32px !important; - height: 32px !important; - content: url('${faviconUrl}') !important; - background-image: url('${faviconUrl}') !important; - `; - - console.log("Applied favicon to icon element"); - } - - // 2. Set data attributes on the item element - item.setAttribute('data-icon', faviconUrl); - item.setAttribute('data-original-icon', faviconUrl); - item.setAttribute('data-icon-locked', 'true'); - item.setAttribute('data-weblink-icon', faviconUrl); - - // 3. Add a style tag with !important rules for this specific item - const itemId = item.id || item.getAttribute('data-uid') || `weblink-${Date.now()}`; - if (!item.id) { - item.id = itemId; - } - - const styleId = `style-${itemId}`; - let styleTag = document.getElementById(styleId); - if (!styleTag) { - styleTag = document.createElement('style'); - styleTag.id = styleId; - document.head.appendChild(styleTag); - } - - styleTag.textContent = ` - #${itemId} img.item-icon, - [data-uid="${itemId}"] img.item-icon { - content: url('${faviconUrl}') !important; - background-image: url('${faviconUrl}') !important; - } - `; - - console.log("Added style tag for weblink item"); - }; + // Clear any existing icons first to prevent stacking + $item.find('img.item-icon').remove(); - // Create a MutationObserver to watch for new weblink items - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - // Check if any new nodes were added - if (mutation.addedNodes.length > 0) { - mutation.addedNodes.forEach((node) => { - // Check if this is an element node - if (node.nodeType === Node.ELEMENT_NODE) { - // Check if this is a weblink item - if (node.classList && node.classList.contains('item')) { - const fileName = node.getAttribute('data-name'); - if (fileName && fileName.toLowerCase().endsWith('.weblink')) { - console.log("MutationObserver: Found new weblink item:", fileName); - applyFaviconToWeblinkItem(node); - } - } - - // Also check children for weblink items - const weblinkItems = node.querySelectorAll('.item[data-name$=".weblink"]'); - if (weblinkItems.length > 0) { - console.log("MutationObserver: Found new weblink items in children:", weblinkItems.length); - weblinkItems.forEach(applyFaviconToWeblinkItem); - } - } - }); - } - - // Check if any attributes were modified - if (mutation.type === 'attributes' && - mutation.attributeName === 'src' && - mutation.target.classList && - mutation.target.classList.contains('item-icon')) { - - // Check if this is a weblink item icon - const item = mutation.target.closest('.item[data-name$=".weblink"]'); - if (item) { - const originalIcon = mutation.target.getAttribute('data-original-icon'); - if (originalIcon && mutation.target.src !== originalIcon) { - console.log("MutationObserver: Weblink icon src changed, resetting"); - mutation.target.src = originalIcon; - } - } - } - }); + // Create a fresh icon element + const $icon = $(''); + $item.prepend($icon); + + // Set core attributes + $icon.attr({ + 'src': faviconUrl, + 'data-icon': faviconUrl }); - // Start observing the document + // Handle loading errors + $icon.on('error', function() { + this.src = window.icons['link.svg']; + }); + + // Set essential item attributes + $item.attr({ + 'data-icon': faviconUrl, + 'data-weblink': 'true' + }); + + // Store in favicon cache + if (url) { + try { + const domain = new URL(url).hostname; + window.favicon_cache[domain] = faviconUrl; + + // Also store in localStorage for persistence + localStorage.setItem(`favicon_${domain}`, faviconUrl); + + // Add domain to item attributes + $item.attr('data-domain', domain); + } catch (e) { + console.error("Error processing URL:", e); + } + } + + return true; + } catch (e) { + console.error("Error applying weblink icon:", e); + return false; + } +} + +// Set up global CSS once at startup +(function setupWeblinkStyles() { + let styleTag = document.getElementById('weblink-global-styles'); + if (!styleTag) { + styleTag = document.createElement('style'); + styleTag.id = 'weblink-global-styles'; + document.head.appendChild(styleTag); + + styleTag.textContent = ` + /* Ensure weblink icons are always visible */ + .item[data-weblink="true"] .item-icon, + .item[data-name$=".weblink"] .item-icon, + .weblink-icon { + visibility: visible !important; + opacity: 1 !important; + display: block !important; + width: 32px !important; + height: 32px !important; + margin: 0 auto !important; + background-size: contain !important; + object-fit: contain !important; + } + + /* Hide .weblink extension completely */ + .item[data-name$=".weblink"] .item-name { + visibility: hidden; + position: relative; + } + + /* Show only the site name */ + .item[data-name$=".weblink"] .item-name:after { + content: attr(data-display-name); + visibility: visible; + position: absolute; + left: 0; + right: 0; + text-align: center; + } + + /* Prevent multiple icons */ + .item[data-weblink="true"] img.item-icon ~ img.item-icon { + display: none !important; + } + `; + } + + // Add a global hook to modify file names when displayed + if (!window.originalItemNameDisplay) { + // Backup the original function if it exists + window.originalItemNameDisplay = window.update_item_name; + + // Override the function that updates item names + window.update_item_name = function(item, newName) { + // Call the original function first + if (window.originalItemNameDisplay) { + window.originalItemNameDisplay(item, newName); + } + + // Then check if this is a weblink and modify the display + if (item && $(item).length) { + const $item = $(item); + const itemName = $item.attr('data-name') || newName; + + // Check if this is a weblink + if (itemName && itemName.toLowerCase().endsWith('.weblink')) { + // Extract the simple name (without extension) + const nameWithoutExt = itemName.replace(/\.weblink$/i, ''); + + // Find the name element + const $nameEl = $item.find('.item-name'); + if ($nameEl.length) { + // Store the full name in data attribute + $nameEl.attr('data-full-name', itemName); + $nameEl.attr('data-display-name', nameWithoutExt); + + // Set the text directly + $nameEl.text(nameWithoutExt); + } + } + } + }; + } +})(); + +// Set up a global weblink item observer +(function setupWeblinkObserver() { + // Skip if already initialized + if (window.weblinkObserverInitialized) return; + window.weblinkObserverInitialized = true; + + // Function to find and fix weblink items + const fixWeblinkItems = () => { + // Find all weblink items + const weblinkItems = document.querySelectorAll('.item[data-name$=".weblink"]'); + + weblinkItems.forEach(item => { + const $item = $(item); + + // Skip if already processed + if ($item.attr('data-weblink-processed') === 'true') return; + + // Apply icon + const faviconUrl = $item.attr('data-icon') || $item.attr('data-favicon') || $item.attr('data-icon-url'); + const url = $item.attr('data-url'); + + // Apply icons first + if (faviconUrl) { + applyWeblinkIcon(item, faviconUrl, url); + } + + // Fix the display name + const $nameEl = $item.find('.item-name'); + if ($nameEl.length) { + const itemName = $item.attr('data-name'); + if (itemName) { + // Strip extension + const nameWithoutExt = itemName.replace(/\.weblink$/i, ''); + + // Apply to display name + $nameEl.attr('data-full-name', itemName); + $nameEl.attr('data-display-name', nameWithoutExt); + $nameEl.text(nameWithoutExt); + } + } + + // Mark as processed to avoid duplicate processing + $item.attr('data-weblink-processed', 'true'); + }); + }; + + // Run immediately + fixWeblinkItems(); + + // Set up MutationObserver to watch for DOM changes + const observer = new MutationObserver((mutations) => { + let shouldFix = false; + + mutations.forEach(mutation => { + // Check if we need to reapply icons and names + if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + if ( + node.classList && + (node.classList.contains('item') || node.querySelector('.item')) + ) { + shouldFix = true; + break; + } + } + } + } + }); + + if (shouldFix) { + fixWeblinkItems(); + } + }); + + // Watch for changes to item names observer.observe(document.body, { childList: true, subtree: true, attributes: true, - attributeFilter: ['src', 'data-name'] - }); - - // Store the observer in a global variable to prevent garbage collection - window.weblinkIconObserver = observer; - - console.log("Global weblink icon persistence system initialized"); - - // Add global CSS rules to ensure all weblink icons are properly displayed - try { - console.log("Adding global CSS rules for weblink icons"); - - // Create a global style tag for weblink icons if it doesn't exist - let globalStyleTag = document.getElementById('weblink-icons-global-style'); - if (!globalStyleTag) { - globalStyleTag = document.createElement('style'); - globalStyleTag.id = 'weblink-icons-global-style'; - document.head.appendChild(globalStyleTag); - } - - // Add comprehensive CSS rules to ensure icons are displayed correctly - globalStyleTag.textContent = ` - /* Ensure weblink icons are always visible */ - .item[data-name$=".weblink"] .item-icon, - .weblink-item .item-icon, - .item[data-weblink="true"] .item-icon, - .item[data-has-custom-icon="true"] .item-icon { - visibility: visible !important; - opacity: 1 !important; - display: block !important; - } - - /* Ensure persistent icons are not overridden */ - .persistent-icon { - width: 32px !important; - height: 32px !important; - } - - /* Force immediate display of icons */ - .item[data-icon-locked="true"] .item-icon { - visibility: visible !important; - opacity: 1 !important; - display: block !important; - } - `; - - console.log("Global CSS rules added for weblink icons"); - } catch (e) { - console.error("Error adding global CSS rules:", e); - } - - // Apply favicons to existing weblink items - setTimeout(() => { - const existingWeblinkItems = document.querySelectorAll('.item[data-name$=".weblink"]'); - if (existingWeblinkItems.length > 0) { - console.log("Found existing weblink items:", existingWeblinkItems.length); - existingWeblinkItems.forEach(applyFaviconToWeblinkItem); - } - }, 500); - - // Also apply favicons periodically to catch any items that might have been missed - setInterval(() => { - const weblinkItems = document.querySelectorAll('.item[data-name$=".weblink"]'); - if (weblinkItems.length > 0) { - weblinkItems.forEach(applyFaviconToWeblinkItem); - } - }, 5000); - - } catch (e) { - console.error("Error setting up global weblink icon persistence system:", e); - } + attributeFilter: ['data-name'] + }); + + // Also run when desktop refreshes + document.addEventListener('desktop-refreshed', fixWeblinkItems); + document.addEventListener('directory-loaded', fixWeblinkItems); })(); /** @@ -323,788 +307,121 @@ const new_context_menu_item = function(dirname, append_to_element){ }); if (url) { - try { - // Get a name for the link based on the URL - let linkName = 'New Link'; - try { - // Try to extract a name from the URL - const urlObj = new URL(url); - linkName = urlObj.hostname.replace(/^www\./, ''); - console.log("Extracted link name from URL:", linkName); - } catch (e) { - // If URL parsing fails, use default name - console.error("Error parsing URL:", e); - } - - // Try to fetch and preload the favicon using multiple sources - let faviconUrl = null; + // Extract domain for naming and favicon try { const urlObj = new URL(url); const domain = urlObj.hostname; - // Define multiple favicon sources to try - const faviconSources = [ - // Direct favicon.ico from the domain root (most reliable) - `https://${domain}/favicon.ico`, - // DuckDuckGo's favicon service - `https://icons.duckduckgo.com/ip3/${domain}.ico`, - // Google's favicon service with higher resolution - `https://www.google.com/s2/favicons?domain=${domain}&sz=128`, - // Fallback to Google's service with no size parameter - `https://www.google.com/s2/favicons?domain=${domain}`, - // Final fallback to default icon - window.icons['link.svg'] - ]; - - console.log("Attempting to fetch favicon from multiple sources"); - - // Try each source in sequence until one works - for (const source of faviconSources) { - try { - console.log("Trying favicon source:", source); - - // Skip the default icon in the validation check - if (source === window.icons['link.svg']) { - console.log("Using default icon as last resort"); - faviconUrl = source; - break; - } - - // Preload the favicon to ensure it's in the browser cache - const isValid = await new Promise((resolve) => { - const preloadImg = new Image(); - - // Set a small size to force loading - preloadImg.style.width = '1px'; - preloadImg.style.height = '1px'; - preloadImg.style.position = 'absolute'; - preloadImg.style.opacity = '0.01'; - - // Add to DOM to force loading - document.body.appendChild(preloadImg); - - preloadImg.onload = () => { - // Check if the image is a valid favicon (not a placeholder) - // Create a canvas to analyze the image - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = preloadImg.width; - canvas.height = preloadImg.height; - - // Draw the image to the canvas - ctx.drawImage(preloadImg, 0, 0); - - // Get the image data - let imageData; - try { - imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - } catch (e) { - console.warn("Could not get image data (CORS issue):", e); - // If we can't analyze the image due to CORS, assume it's valid - document.body.removeChild(preloadImg); - resolve(true); - return; - } - - // Check if the image is a single letter (Google's fallback) - // This is a heuristic: if most pixels are transparent or the same color, - // it's likely a placeholder with a letter - const data = imageData.data; - let transparentPixels = 0; - let colorCounts = {}; - - for (let i = 0; i < data.length; i += 4) { - // Check for transparency - if (data[i + 3] < 10) { - transparentPixels++; - } else { - // Count colors - const color = `${data[i]},${data[i + 1]},${data[i + 2]}`; - colorCounts[color] = (colorCounts[color] || 0) + 1; - } - } - - // Calculate total pixels - const totalPixels = canvas.width * canvas.height; - - // If more than 90% of pixels are transparent, it's likely not a real favicon - if (transparentPixels / totalPixels > 0.9) { - console.warn("Image is mostly transparent, likely not a real favicon"); - document.body.removeChild(preloadImg); - resolve(false); - return; - } - - // If one color dominates (>80% of non-transparent pixels), it might be a letter - const nonTransparentPixels = totalPixels - transparentPixels; - let dominantColorCount = 0; - - for (const color in colorCounts) { - if (colorCounts[color] > dominantColorCount) { - dominantColorCount = colorCounts[color]; - } - } - - // If one color is dominant and the image is small, it might be a letter - if (dominantColorCount / nonTransparentPixels > 0.8 && - canvas.width <= 64 && canvas.height <= 64) { - console.warn("Image has a dominant color, might be a letter placeholder"); - document.body.removeChild(preloadImg); - resolve(false); - return; - } - - // If we get here, the image is likely a valid favicon - console.log("Image appears to be a valid favicon"); - document.body.removeChild(preloadImg); - resolve(true); - }; - - preloadImg.onerror = () => { - console.warn("Failed to load favicon from source:", source); - if (document.body.contains(preloadImg)) { - document.body.removeChild(preloadImg); - } - resolve(false); - }; - - // Set a timeout in case the image takes too long to load - setTimeout(() => { - if (!preloadImg.complete) { - console.warn("Favicon preload timed out for source:", source); - if (document.body.contains(preloadImg)) { - document.body.removeChild(preloadImg); - } - resolve(false); - } - }, 1500); - - // Start loading the image - preloadImg.src = source; - }); - - if (isValid) { - console.log("Found valid favicon at:", source); - faviconUrl = source; - break; - } - } catch (sourceError) { - console.warn("Error trying favicon source:", sourceError); - // Continue to the next source - } - } - - // If no valid favicon was found, use the default icon - if (!faviconUrl) { - console.log("No valid favicon found, using default icon"); - faviconUrl = window.icons['link.svg']; - } - - // Create a permanent copy of the favicon in the DOM to ensure it's always available - const faviconId = `favicon-${Date.now()}-${Math.floor(Math.random() * 1000000)}`; - const faviconImg = document.createElement('img'); - faviconImg.id = faviconId; - faviconImg.src = faviconUrl; - faviconImg.style.position = 'absolute'; - faviconImg.style.width = '1px'; - faviconImg.style.height = '1px'; - faviconImg.style.opacity = '0.01'; - faviconImg.style.pointerEvents = 'none'; - faviconImg.style.left = '-9999px'; - faviconImg.setAttribute('data-permanent-favicon', 'true'); - faviconImg.setAttribute('data-domain', domain); - document.body.appendChild(faviconImg); - - // Store the favicon in a global cache - if (!window.favicon_cache) { - window.favicon_cache = {}; - } - window.favicon_cache[domain] = faviconUrl; - - // Also store in localStorage for persistence - try { - localStorage.setItem(`favicon_${domain}`, faviconUrl); - } catch (e) { - console.error("Error storing favicon in localStorage:", e); - } - - } catch (e) { - console.error("Error in favicon fetching process:", e); - faviconUrl = window.icons['link.svg']; + // Extract a simple name from the domain (e.g., "google" from "google.com") + let siteName = domain.replace(/^www\./, ''); + + // Further simplify by removing the TLD (.com, .org, etc.) + siteName = siteName.split('.')[0]; + + // Capitalize the first letter + siteName = siteName.charAt(0).toUpperCase() + siteName.slice(1); + + // Use simple name but keep .weblink extension for the file system + let linkName = siteName; + let fileName = linkName + '.weblink'; + + // Get favicon URL from Google favicon service + let faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; + + // Check if we have a cached favicon + if (window.favicon_cache[domain]) { + faviconUrl = window.favicon_cache[domain]; } + + // Store in favicon cache + window.favicon_cache[domain] = faviconUrl; // Store the URL and favicon in a comprehensive JSON object const weblink_content = JSON.stringify({ url: url, faviconUrl: faviconUrl, - iconDataUrl: faviconUrl, // For backward compatibility type: 'weblink', - domain: new URL(url).hostname, + domain: domain, created: Date.now(), modified: Date.now(), version: '2.0', metadata: { originalUrl: url, originalFaviconUrl: faviconUrl, - linkName: linkName + linkName: linkName, + simpleName: siteName } }); - console.log("Creating weblink file:", { - dirname: dirname, - name: linkName + '.weblink', - content: weblink_content, - url: url, - faviconUrl: faviconUrl - }); - - // Create the file with favicon - with enhanced metadata and attributes + // Create the file with favicon const item = await window.create_file({ dirname: dirname, append_to_element: append_to_element, - name: linkName + '.weblink', + name: fileName, content: weblink_content, icon: faviconUrl, type: 'weblink', metadata: JSON.stringify({ faviconUrl: faviconUrl, - iconDataUrl: faviconUrl, // Add for backward compatibility url: url, - domain: new URL(url).hostname, + domain: domain, timestamp: Date.now(), - version: '2.0' // Version to identify enhanced weblinks + version: '2.0' }), - // Add comprehensive HTML attributes to force the icon to be visible html_attributes: { - 'data-has-custom-icon': 'true', - 'data-icon-url': faviconUrl, 'data-weblink': 'true', - 'data-favicon': faviconUrl, - 'data-original-icon': faviconUrl, - 'data-icon-locked': 'true', - 'data-weblink-icon': faviconUrl, + 'data-icon': faviconUrl, 'data-url': url, - 'data-domain': new URL(url).hostname, - 'data-icon-version': '2.0', - 'style': `--icon-url: url('${faviconUrl}') !important;` + 'data-domain': domain, + 'data-display-name': linkName, + 'data-hide-extension': 'true' }, - // Add any additional parameters that might help with visibility force_refresh: true, - show_immediately: true, - skip_history: false, - priority: 'high', - // Add custom CSS class for targeting - class: 'weblink-item persistent-icon-item' + class: 'weblink-item' }); - // Store the URL and favicon in localStorage for extra reliability + // Apply icon using our consolidated function if (item) { - const uid = $(item).attr('data-uid'); - if (uid) { - localStorage.setItem('weblink_' + uid, url); - localStorage.setItem('weblink_icon_' + uid, faviconUrl); - - // Also store by domain for cross-item consistency - try { - const domain = new URL(url).hostname; - localStorage.setItem('favicon_' + domain, faviconUrl); - } catch (e) { - console.error("Error storing domain favicon:", e); - } - } - } - - // If the item was created successfully, ensure the icon is set - if (item) { - const $item = $(item); - - // Apply icon in multiple ways to ensure it's visible and persists - const applyIcon = async () => { - console.log("Applying persistent icon to item:", faviconUrl); - - // APPROACH 1: Replace the entire icon element with a persistent one - const $icon = $item.find('img.item-icon'); - if ($icon.length > 0) { - // Create a completely new image element with enhanced attributes - const newIconElement = document.createElement('img'); - newIconElement.className = 'item-icon persistent-icon'; - newIconElement.src = faviconUrl; - - // Add important styling to prevent overrides - newIconElement.style.cssText = ` - width: 32px !important; - height: 32px !important; - content: url('${faviconUrl}') !important; - background-image: url('${faviconUrl}') !important; - background-size: contain !important; - background-repeat: no-repeat !important; - background-position: center !important; - `; - - // Add data attributes to prevent the icon from being changed - newIconElement.setAttribute('data-icon', faviconUrl); - newIconElement.setAttribute('data-original-icon', faviconUrl); - newIconElement.setAttribute('data-icon-locked', 'true'); - newIconElement.setAttribute('data-weblink-icon', faviconUrl); - newIconElement.setAttribute('data-domain', new URL(url).hostname); - - // Replace the existing icon with our new one - $icon[0].parentNode.replaceChild(newIconElement, $icon[0]); - - // Handle favicon loading error - newIconElement.onerror = function() { - console.warn("Icon failed to load, using fallback"); - this.src = window.icons['link.svg']; - }; - - // Add event listener to prevent the src from being changed - newIconElement.addEventListener('load', function() { - console.log("Icon loaded successfully"); - // Force a repaint to ensure the icon is displayed - this.style.display = 'none'; - void this.offsetHeight; - this.style.display = ''; - }); - } else { - console.warn("No icon element found, creating one"); - const newIcon = document.createElement('img'); - newIcon.className = 'item-icon persistent-icon'; - newIcon.src = faviconUrl; - - // Add important styling - newIcon.style.cssText = ` - width: 32px !important; - height: 32px !important; - content: url('${faviconUrl}') !important; - background-image: url('${faviconUrl}') !important; - background-size: contain !important; - background-repeat: no-repeat !important; - background-position: center !important; - `; - - // Add data attributes - newIcon.setAttribute('data-icon', faviconUrl); - newIcon.setAttribute('data-original-icon', faviconUrl); - newIcon.setAttribute('data-icon-locked', 'true'); - - // Add to the item - $item.prepend(newIcon); - - // Handle errors - newIcon.onerror = function() { - console.warn("Icon failed to load, using fallback"); - this.src = window.icons['link.svg']; - }; - } - - // APPROACH 2: Set item attributes and inline styles - $item.attr({ - 'data-icon': faviconUrl, - 'data-url': url, - 'data-type': 'weblink', - }); - - // Add inline style with !important to force the icon - const currentStyle = $item.attr('style') || ''; - $item.attr('style', currentStyle + - `;--icon-url: url('${faviconUrl}') !important;` + - `background-image: url('${faviconUrl}') !important;`); - - // APPROACH 3: Set background image on all possible containers - const $iconContainer = $item.find('.item-icon-container'); - if ($iconContainer.length > 0) { - $iconContainer.css({ - 'background-image': `url('${faviconUrl}') !important`, - 'background-size': 'contain !important', - 'background-repeat': 'no-repeat !important', - 'background-position': 'center !important' - }); - } - - // APPROACH 4: Add a custom style tag for this specific item - const itemId = $item.attr('id') || `weblink-${Date.now()}`; - if (!$item.attr('id')) { - $item.attr('id', itemId); - } - - const styleId = `style-${itemId}`; - let $styleTag = $(`#${styleId}`); - if ($styleTag.length === 0) { - $styleTag = $(``); - $('head').append($styleTag); - } - - $styleTag.html(` - #${itemId} .item-icon { - background-image: url('${faviconUrl}') !important; - content: url('${faviconUrl}') !important; - } - #${itemId}[data-has-custom-icon="true"] .item-icon-container { - background-image: url('${faviconUrl}') !important; - } - `); - - // Force a DOM reflow to ensure the icon is displayed - void $item[0].offsetHeight; - - // Force multiple refreshes of the desktop view to ensure the file appears - console.log("Forcing multiple refreshes of the desktop view"); - await refresh_item_container(dirname); - - // Add a small delay and refresh again to ensure the file appears - await new Promise(resolve => setTimeout(resolve, 100)); - await refresh_item_container(dirname); - - // Apply the icon again after a short delay to ensure it's visible - setTimeout(() => { - const $iconAfterRefresh = $item.find('img.item-icon'); - if ($iconAfterRefresh.length > 0) { - $iconAfterRefresh.attr('src', faviconUrl); - } - console.log("Icon applied after refresh"); - }, 100); - }; - - // Apply icon immediately - await applyIcon(); - - // And apply again after short delays to ensure it sticks - setTimeout(() => { applyIcon(); }, 300); - setTimeout(() => { applyIcon(); }, 1000); - - // APPROACH 5: Direct DOM manipulation for maximum control - // This is a more aggressive approach that directly modifies the DOM - setTimeout(() => { - try { - // Find all possible icon elements related to this item - const itemId = $item.attr('id'); - const uid = $item.attr('data-uid'); - - // Query selectors to find all possible icon elements - const selectors = [ - `#${itemId} img.item-icon`, - `[data-uid="${uid}"] img.item-icon`, - `[data-uid="${uid}"] .item-icon-container img`, - `.item[data-name="${linkName}.weblink"] img.item-icon` - ]; - - // Try each selector - selectors.forEach(selector => { - const elements = document.querySelectorAll(selector); - if (elements.length > 0) { - console.log(`Found ${elements.length} elements with selector: ${selector}`); - elements.forEach(el => { - // Force the src attribute - el.src = faviconUrl; - // Also set as a background image - el.style.backgroundImage = `url('${faviconUrl}')`; - // Force a repaint - el.style.display = 'none'; - void el.offsetHeight; - el.style.display = ''; - }); - } - }); - - // Also try to find the parent container and set its background - const containers = document.querySelectorAll(`[data-uid="${uid}"] .item-icon-container`); - containers.forEach(container => { - container.style.backgroundImage = `url('${faviconUrl}')`; - }); - - console.log("Direct DOM manipulation completed"); - } catch (e) { - console.error("Error during direct DOM manipulation:", e); - } - }, 500); - - // APPROACH 6: Use MutationObserver to watch for DOM changes - // This will ensure the icon is set correctly even if the DOM changes - try { - const uid = $item.attr('data-uid'); - if (uid) { - console.log("Setting up MutationObserver for item:", uid); - - // Create a MutationObserver to watch for changes to the DOM - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - // Check if any new nodes were added - if (mutation.addedNodes.length > 0) { - // Look for icon elements in the added nodes - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - // Check if this is an icon element - if (node.classList && node.classList.contains('item-icon')) { - console.log("MutationObserver: Found new icon element"); - node.src = faviconUrl; - } - - // Also check children - const icons = node.querySelectorAll('.item-icon'); - if (icons.length > 0) { - console.log("MutationObserver: Found new icon elements in children"); - icons.forEach(icon => { - icon.src = faviconUrl; - }); - } - } - }); - } - - // Check if any attributes were modified - if (mutation.type === 'attributes' && - mutation.attributeName === 'src' && - mutation.target.classList && - mutation.target.classList.contains('item-icon')) { - // If the src attribute of an icon was changed, set it back - if (mutation.target.src !== faviconUrl) { - console.log("MutationObserver: Icon src changed, resetting"); - mutation.target.src = faviconUrl; - } - } - }); - }); - - // Start observing the document - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['src'] - }); - - // Stop observing after 5 seconds to avoid memory leaks - setTimeout(() => { - console.log("Stopping MutationObserver"); - observer.disconnect(); - }, 5000); - } - } catch (e) { - console.error("Error setting up MutationObserver:", e); - } - - console.log("Set icon directly on item:", faviconUrl); - } - - // Store the URL in localStorage for extra reliability - if (item) { - const uid = $(item).attr('data-uid'); - if (uid) { - localStorage.setItem('weblink_' + uid, url); - } - } - - // APPROACH 7: Create a permanent visible element with the icon - // This will ensure the icon is always visible - try { - console.log("Creating permanent visible element with icon"); - - // Find the container where the file should be displayed - let container = null; - if (append_to_element) { - container = append_to_element; - } else { - // Try to find the desktop or current directory container - container = document.querySelector('.desktop, .explorer-container.active, .files-container.active'); - } + applyWeblinkIcon(item, faviconUrl, url); + // Ensure the item is visible in the container + const container = append_to_element || document.querySelector('.desktop, .explorer-container.active, .files-container.active'); if (container) { - console.log("Found container for permanent element"); + // Force a refresh of the container + await refresh_item_container(dirname); - // Generate a unique ID for this element - const uniqueId = `weblink-${Date.now()}-${Math.floor(Math.random() * 1000000)}`; - - // Create a permanent element with the icon - const permanentElement = document.createElement('div'); - permanentElement.id = uniqueId; - permanentElement.className = 'item weblink-item permanent-item'; - permanentElement.setAttribute('data-name', linkName + '.weblink'); - permanentElement.setAttribute('data-type', 'weblink'); - permanentElement.setAttribute('data-icon', faviconUrl); - permanentElement.setAttribute('data-url', url); - permanentElement.setAttribute('data-permanent', 'true'); - - // Add inline styles to ensure it's always visible - permanentElement.style.display = 'inline-block'; - permanentElement.style.position = 'relative'; - permanentElement.style.margin = '10px'; - permanentElement.style.textAlign = 'center'; - permanentElement.style.verticalAlign = 'top'; - permanentElement.style.width = '80px'; - permanentElement.style.height = '80px'; - permanentElement.style.zIndex = '1000'; // High z-index to ensure visibility - - // Create the icon element - const iconElement = document.createElement('img'); - iconElement.className = 'item-icon'; - iconElement.src = faviconUrl; - iconElement.style.width = '32px'; - iconElement.style.height = '32px'; - iconElement.style.display = 'block'; - iconElement.style.margin = '0 auto 5px auto'; - - // Create the name element - const nameElement = document.createElement('div'); - nameElement.className = 'item-name'; - nameElement.textContent = linkName + '.weblink'; - nameElement.style.fontSize = '12px'; - nameElement.style.wordWrap = 'break-word'; - - // Add the elements to the permanent element - permanentElement.appendChild(iconElement); - permanentElement.appendChild(nameElement); - - // Add the permanent element to the container - container.appendChild(permanentElement); - - // Add a click handler to open the URL - permanentElement.addEventListener('click', () => { - window.open(url, '_blank', 'noopener,noreferrer'); - }); - - // Add a style tag to ensure this element is always visible - const styleTag = document.createElement('style'); - styleTag.id = `style-${uniqueId}`; - styleTag.textContent = ` - #${uniqueId} { - display: inline-block !important; - visibility: visible !important; - opacity: 1 !important; - } - #${uniqueId} .item-icon { - content: url('${faviconUrl}') !important; - background-image: url('${faviconUrl}') !important; - } - `; - document.head.appendChild(styleTag); - - // Set up a MutationObserver to ensure the element is not removed - const observer = new MutationObserver((mutations) => { - // Check if our element was removed - if (!document.getElementById(uniqueId)) { - console.log("Permanent element was removed, re-adding it"); - container.appendChild(permanentElement); - } + // Hide the extension in the displayed name + const $item = $(item); + const $nameElement = $item.find('.item-name'); + if ($nameElement.length > 0) { + // Store the original name for reference + $nameElement.attr('data-full-name', fileName); + $nameElement.attr('data-display-name', linkName); - // Also check if the icon was changed - const icon = document.querySelector(`#${uniqueId} .item-icon`); - if (icon && icon.src !== faviconUrl) { - console.log("Icon was changed, resetting it"); - icon.src = faviconUrl; + // Set the text directly (no extension) + $nameElement.text(linkName); + } + + // If this is the desktop, trigger a desktop refresh + if (container.classList.contains('desktop')) { + if (typeof window.refresh_desktop === 'function') { + window.refresh_desktop(); + } else if (typeof window.refresh_desktop_items === 'function') { + window.refresh_desktop_items(); + } + } + + // Trigger a custom event to ensure icon is properly applied + const event = new CustomEvent('weblink-created', { + detail: { + item: item, + url: url, + faviconUrl: faviconUrl } }); - - // Start observing the container - observer.observe(container, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['src', 'style', 'class'] - }); - - // Keep the observer running indefinitely - // Store it in a global variable to prevent garbage collection - window.weblinkObservers = window.weblinkObservers || {}; - window.weblinkObservers[uniqueId] = observer; - - console.log("Permanent element created with ID:", uniqueId); + document.dispatchEvent(event); } - } catch (e) { - console.error("Error creating permanent element:", e); } - - // Add global CSS rules to ensure all weblink icons are properly displayed - try { - console.log("Adding global CSS rules for weblink icons"); - - // Create a global style tag for weblink icons if it doesn't exist - let globalStyleTag = document.getElementById('weblink-icons-global-style'); - if (!globalStyleTag) { - globalStyleTag = document.createElement('style'); - globalStyleTag.id = 'weblink-icons-global-style'; - document.head.appendChild(globalStyleTag); - } - - // Add comprehensive CSS rules to ensure icons are displayed correctly - globalStyleTag.textContent = ` - /* Ensure weblink icons are always visible */ - .item[data-name$=".weblink"] .item-icon, - .weblink-item .item-icon, - .item[data-weblink="true"] .item-icon, - .item[data-has-custom-icon="true"] .item-icon { - visibility: visible !important; - opacity: 1 !important; - display: block !important; - } - - /* Ensure persistent icons are not overridden */ - .persistent-icon { - width: 32px !important; - height: 32px !important; - } - - /* Force immediate display of icons */ - .item[data-icon-locked="true"] .item-icon { - visibility: visible !important; - opacity: 1 !important; - display: block !important; - } - - /* Specific rule for this domain */ - .item[data-domain="${new URL(url).hostname}"] .item-icon { - content: url('${faviconUrl}') !important; - background-image: url('${faviconUrl}') !important; - } - `; - - console.log("Global CSS rules added for weblink icons"); - } catch (e) { - console.error("Error adding global CSS rules:", e); - } - - // Try to force a more comprehensive refresh of the file system view - try { - console.log("Attempting to force a comprehensive refresh"); - - // Try to find and trigger the refresh button if it exists - const refreshButtons = document.querySelectorAll('.refresh-btn, .refresh-button, [data-action="refresh"]'); - if (refreshButtons.length > 0) { - console.log("Found refresh button, clicking it"); - refreshButtons[0].click(); - } - - // Try to trigger a refresh event on the container - const containers = document.querySelectorAll('.explorer-container, .files-container, .desktop'); - if (containers.length > 0) { - console.log("Found container, triggering refresh event"); - $(containers[0]).trigger('refresh'); - } - - // If the append_to_element is provided, try to refresh it directly - if (append_to_element) { - console.log("Refreshing append_to_element directly"); - $(append_to_element).trigger('refresh'); - - // Also try to find its parent container and refresh that - const parentContainer = $(append_to_element).closest('.explorer-container, .files-container, .desktop'); - if (parentContainer.length > 0) { - console.log("Found parent container, triggering refresh event"); - parentContainer.trigger('refresh'); - } - } - - // As a last resort, try to reload the current directory - if (window.current_directory) { - console.log("Attempting to reload current directory"); - if (typeof window.load_directory === 'function') { - window.load_directory(window.current_directory); - } - } - } catch (e) { - console.error("Error during comprehensive refresh:", e); - } - - console.log("Created web link with URL:", url); } catch (error) { console.error("Error creating web link:", error); UIAlert("Error creating web link: " + error.message); From aeac392a7c4e4d9dbab93e6b084d2103ba32c44e Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 27 Jun 2025 22:46:34 -0700 Subject: [PATCH 3/8] Remove mods and favicons --- mods/mods_available/web-shortcuts/index.js | 240 ------------------ .../web-shortcuts/manifest.json | 11 - mods/mods_available/web-shortcuts/module.js | 41 --- .../mods_available/web-shortcuts/package.json | 11 - .../web-shortcuts/public/main.js | 197 -------------- src/gui/src/helpers/item_icon.js | 52 ---- src/gui/src/helpers/new_context_menu_item.js | 73 +----- 7 files changed, 8 insertions(+), 617 deletions(-) delete mode 100644 mods/mods_available/web-shortcuts/index.js delete mode 100644 mods/mods_available/web-shortcuts/manifest.json delete mode 100644 mods/mods_available/web-shortcuts/module.js delete mode 100644 mods/mods_available/web-shortcuts/package.json delete mode 100644 mods/mods_available/web-shortcuts/public/main.js diff --git a/mods/mods_available/web-shortcuts/index.js b/mods/mods_available/web-shortcuts/index.js deleted file mode 100644 index 630149ddb..000000000 --- a/mods/mods_available/web-shortcuts/index.js +++ /dev/null @@ -1,240 +0,0 @@ -/** - * Web Shortcuts Mod for Puter - * - * This mod adds a "Create Web Shortcut" option to the context menu - * that allows users to create .weblink files that open websites. - */ - -const BaseService = require("../../../src/backend/src/services/BaseService"); - -class WebShortcutsService extends BaseService { - async _init() { - const svc_puterHomepage = this.services.get('puter-homepage'); - svc_puterHomepage.register_script('/web-shortcuts/main.js'); - } -} - -// Function to extract URL from text (handles pasted URLs) -function extractURL(text) { - // Remove any warning messages that might be in the pasted content - const cleanText = text.replace(/⚠️Warning⚠️.*?(?=http)/s, '').trim(); - - // Try to find a URL in the text - const urlRegex = /(https?:\/\/[^\s]+)/g; - const matches = cleanText.match(urlRegex); - - if (matches && matches.length > 0) { - return matches[0]; - } - - return text; -} - -// Function to validate URL -function isValidURL(url) { - try { - new URL(url); - return true; - } catch (error) { - return false; - } -} - -// Function to create a web shortcut -async function createWebShortcut(targetPath, url = null) { - try { - // If no URL provided, prompt the user - if (!url) { - let userInput = prompt('Enter or paste the URL for the web shortcut:', 'https://example.com'); - if (!userInput) { - console.log('User cancelled URL input'); - return; - } - url = extractURL(userInput); - } - - // Ensure URL has protocol - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url; - } - - // Validate URL - if (!isValidURL(url)) { - console.error('Invalid URL:', url); - alert('Invalid URL. Please enter a valid URL.'); - return; - } - - // Get the website title (for the shortcut name) - const validUrl = new URL(url); - const siteName = validUrl.hostname; - - // Get the favicon - const faviconUrl = `https://www.google.com/s2/favicons?domain=${validUrl.origin}&sz=64`; - - // Create a JSON file that will store the shortcut data - const shortcutData = { - url: validUrl.href, - icon: faviconUrl, - type: 'web_shortcut' - }; - - // Create the shortcut file - const shortcutFileName = `${siteName}.weblink`; - - // Get the target path (default to desktop if not provided) - const desktopPath = window.desktop_path || '/Desktop'; - const targetDirectory = targetPath || desktopPath; - - // Write the file - const result = await window.puter.fs.write( - targetDirectory + '/' + shortcutFileName, - JSON.stringify(shortcutData), - { dedupeName: true } - ); - - console.log('Web shortcut created:', result); - } catch (error) { - console.error('Error creating web shortcut:', error); - alert('Error creating web shortcut: ' + error.message); - } -} - -// Handle URL drops on desktop -window.addEventListener('dragover', function(e) { - // Check if we're dragging over the desktop - if (!$(e.target).closest('.desktop').length) return; - - // Check if we have text/uri-list or text/plain data - if (e.dataTransfer.types.includes('text/uri-list') || - e.dataTransfer.types.includes('text/plain')) { - e.preventDefault(); - e.stopPropagation(); - } -}); - -window.addEventListener('drop', async function(e) { - // Check if we're dropping on the desktop - if (!$(e.target).closest('.desktop').length) return; - - // Check if we have text/uri-list or text/plain data - if (e.dataTransfer.types.includes('text/uri-list')) { - e.preventDefault(); - e.stopPropagation(); - - const url = e.dataTransfer.getData('text/uri-list'); - if (isValidURL(url)) { - await createWebShortcut(window.desktop_path, url); - } - } else if (e.dataTransfer.types.includes('text/plain')) { - e.preventDefault(); - e.stopPropagation(); - - const text = e.dataTransfer.getData('text/plain'); - const url = extractURL(text); - if (isValidURL(url)) { - await createWebShortcut(window.desktop_path, url); - } - } -}); - -// Handle URL pastes on desktop -window.addEventListener('paste', async function(e) { - // Check if we're pasting on the desktop - if (!$(e.target).closest('.desktop').length) return; - - const text = e.clipboardData.getData('text/plain'); - const url = extractURL(text); - - if (isValidURL(url)) { - e.preventDefault(); - e.stopPropagation(); - await createWebShortcut(window.desktop_path, url); - } -}); - -// Add "Create Web Shortcut" to the desktop context menu -window.addEventListener('ctxmenu-will-open', function(e) { - const options = e.detail.options; - - // Only add to desktop context menu or directory context menus - if (!options || !options.items) return; - - // Check if this is a desktop or directory context menu - const isDesktopOrDirMenu = options.items.some(item => - (item.html === 'New Folder' || item.html === i18n('new_folder')) || - (item.html === 'Paste' || item.html === i18n('paste')) - ); - - if (isDesktopOrDirMenu) { - // Find the position to insert our menu item (after "New Folder") - let insertIndex = options.items.findIndex(item => - item.html === 'New Folder' || item.html === i18n('new_folder') - ); - - if (insertIndex === -1) { - // If "New Folder" not found, insert at the beginning - insertIndex = 0; - } else { - // Insert after "New Folder" - insertIndex += 1; - } - - // Get the target path - let targetPath; - if (options.parent_element) { - const $parentElement = $(options.parent_element); - if ($parentElement.hasClass('item-container')) { - targetPath = $parentElement.attr('data-path'); - } else if ($parentElement.hasClass('item') && $parentElement.attr('data-is_dir') === '1') { - targetPath = $parentElement.attr('data-path'); - } - } - - // Insert our menu item - options.items.splice(insertIndex, 0, { - html: 'Create Web Shortcut', - icon: '', - onClick: function() { - createWebShortcut(targetPath); - } - }); - } -}); - -// Add "Create Web Shortcut" to the "New" submenu in the desktop context menu -const originalUIContextMenu = window.UIContextMenu; -window.UIContextMenu = function(options) { - if (options && options.items) { - // Find the "New" submenu - const newItemIndex = options.items.findIndex(item => - (item.html === 'New' || item.html === i18n('new')) && - Array.isArray(item.items) - ); - - if (newItemIndex !== -1 && options.items[newItemIndex].items) { - // Add our item to the "New" submenu - options.items[newItemIndex].items.push({ - html: 'Web Shortcut', - icon: '', - onClick: function() { - // Get the target path - let targetPath; - if (options.parent_element) { - const $parentElement = $(options.parent_element); - if ($parentElement.hasClass('item-container')) { - targetPath = $parentElement.attr('data-path'); - } else if ($parentElement.hasClass('item') && $parentElement.attr('data-is_dir') === '1') { - targetPath = $parentElement.attr('data-path'); - } - } - createWebShortcut(targetPath); - } - }); - } - } - - return originalUIContextMenu(options); -}; - -module.exports = WebShortcutsService; \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/manifest.json b/mods/mods_available/web-shortcuts/manifest.json deleted file mode 100644 index b6ff9617b..000000000 --- a/mods/mods_available/web-shortcuts/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "web-shortcuts", - "version": "1.0.0", - "description": "Create web shortcuts on the desktop", - "author": "Puter", - "license": "MIT", - "dependencies": ["fs", "path"], - "client": { - "scripts": ["/mods/web-shortcuts"] - } -} \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/module.js b/mods/mods_available/web-shortcuts/module.js deleted file mode 100644 index bf8adfcaa..000000000 --- a/mods/mods_available/web-shortcuts/module.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const WebShortcutsService = require('./index'); - -module.exports = { - name: 'web-shortcuts', - version: '1.0.0', - description: 'Create web shortcuts on the desktop', - author: 'Puter', - license: 'MIT', - dependencies: ['fs', 'path'], - init: async (puter) => { - const service = new WebShortcutsService(puter); - await service.init(); - return service; - }, - routes: { - '/mods/web-shortcuts': { - GET: (req, res) => { - res.sendFile(__dirname + '/public/main.js'); - } - } - } -}; \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/package.json b/mods/mods_available/web-shortcuts/package.json deleted file mode 100644 index 488d32a19..000000000 --- a/mods/mods_available/web-shortcuts/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "web-shortcuts", - "version": "1.0.0", - "description": "Create web shortcuts on the desktop", - "author": "Puter", - "license": "MIT", - "dependencies": { - "fs": "*", - "path": "*" - } -} \ No newline at end of file diff --git a/mods/mods_available/web-shortcuts/public/main.js b/mods/mods_available/web-shortcuts/public/main.js deleted file mode 100644 index b98e86cd8..000000000 --- a/mods/mods_available/web-shortcuts/public/main.js +++ /dev/null @@ -1,197 +0,0 @@ -// Web Shortcuts Mod -(function() { - console.log('[Web Shortcuts] Mod initializing...'); - - // URL validation helper - function isValidURL(str) { - const pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain - '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string - '(\\#[-a-z\\d_]*)?$','i'); // fragment - return !!pattern.test(str); - } - - // Extract URL from text - function extractURL(text) { - // Clean the text - text = text.trim(); - - // If it's already a valid URL, return it - if (isValidURL(text)) { - return text; - } - - // Try to extract a URL - const urlRegex = /(https?:\/\/[^\s]+)/g; - const matches = text.match(urlRegex); - return matches ? matches[0] : null; - } - - // Create web shortcut - async function createWebShortcut(url, name = null) { - console.log('[Web Shortcuts] Creating shortcut with URL:', url); - try { - if (!url) { - url = await window.puter.prompt('Enter URL:'); - if (!url) return; - } - - // Add https:// if no protocol specified - if (!url.startsWith('http://') && !url.startsWith('https://')) { - url = 'https://' + url; - } - - if (!isValidURL(url)) { - console.log('[Web Shortcuts] Invalid URL:', url); - window.puter.alert('Invalid URL'); - return; - } - - // Get domain/hostname and build favicon link - const { hostname } = new URL(url); - const favicon = `https://www.google.com/s2/favicons?domain=${hostname}&sz=64`; - - // Use hostname as default name if none provided - if (!name) { - name = await window.puter.prompt('Enter shortcut name:', hostname); - if (!name) return; - } - - console.log('[Web Shortcuts] Creating shortcut:', { url, name, favicon }); - - const shortcutData = { - url: url, - favicon: favicon, - created: new Date().toISOString(), - type: 'link' - }; - - // Build the path for storing the shortcut - const filePath = `${window.desktop_path}/${name}.weblink`; - - // Write the file - const file = await window.puter.fs.write( - filePath, - JSON.stringify(shortcutData), - { - type: 'link', - icon: favicon - } - ); - - console.log('[Web Shortcuts] File created:', file); - - // Create the UI icon on the desktop - window.UIItem({ - appendTo: $('.desktop.item-container'), - 'data-type': 'link', - uid: file.uid, - path: filePath, - icon: favicon, - name: name, - is_dir: false, - metadata: JSON.stringify(shortcutData) - }); - - window.puter.notify('Web shortcut created successfully'); - } catch (error) { - console.error('[Web Shortcuts] Error creating shortcut:', error); - window.puter.alert('Error creating web shortcut: ' + (error.message || 'Please check the URL and try again')); - } - } - - // Add context menu items - window.addEventListener('DOMContentLoaded', () => { - console.log('[Web Shortcuts] DOM Content Loaded'); - - // Get desktop element - const el_desktop = document.querySelector('.desktop'); - console.log('[Web Shortcuts] Desktop element found:', !!el_desktop); - - if (!el_desktop) return; - - // Handle paste events - el_desktop.addEventListener('paste', (e) => { - console.log('[Web Shortcuts] Paste event on desktop:', e.target === el_desktop); - if (e.target !== el_desktop) return; - const text = e.clipboardData.getData('text'); - if (isValidURL(text)) { - e.preventDefault(); - createWebShortcut(text); - } - }); - - // Handle drop events - el_desktop.addEventListener('drop', (e) => { - console.log('[Web Shortcuts] Drop event on desktop:', e.target === el_desktop); - if (e.target !== el_desktop) return; - e.preventDefault(); - const url = e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text/plain'); - if (url && isValidURL(url)) { - createWebShortcut(url); - } - }); - - // Listen for context menu opening - window.addEventListener('ctxmenu-will-open', (e) => { - console.log('[Web Shortcuts] Context menu will open:', e.detail); - const options = e.detail.options; - - // Only modify desktop context menu - if (!options || !options.items) { - console.log('[Web Shortcuts] No menu options found'); - return; - } - - // Check if this is a desktop context menu - const isDesktopMenu = options.items.some(item => - (item.html === 'New Folder' || item.html === window.i18n('new_folder')) || - (item.html === 'Paste' || item.html === window.i18n('paste')) - ); - console.log('[Web Shortcuts] Is desktop menu:', isDesktopMenu); - - if (isDesktopMenu) { - // Find the position to insert our menu item (after "New") - const newIndex = options.items.findIndex(item => - item.html === 'New' || item.html === window.i18n('new') - ); - console.log('[Web Shortcuts] New menu index:', newIndex); - - // Insert our menu item after "New" and before the next divider - if (newIndex !== -1) { - let insertIndex = newIndex + 1; - // Find the next divider - while (insertIndex < options.items.length && options.items[insertIndex] !== '-') { - insertIndex++; - } - - console.log('[Web Shortcuts] Inserting at index:', insertIndex); - - // Insert our item before the divider - options.items.splice(insertIndex, 0, { - html: 'Create Web Shortcut', - icon: '', - onClick: () => createWebShortcut() - }); - - console.log('[Web Shortcuts] Menu items after insertion:', options.items); - } - } - }); - - // Add right-click handler to desktop - el_desktop.addEventListener('contextmenu', (e) => { - console.log('[Web Shortcuts] Context menu event on desktop:', e.target === el_desktop); - // Only handle right-clicks directly on the desktop, not on items - if (e.target !== el_desktop) return; - - // The rest of the context menu handling will be done by the ctxmenu-will-open event - }); - - console.log('[Web Shortcuts] All event listeners attached'); - }); - - console.log('[Web Shortcuts] Mod initialization complete'); -})(); \ No newline at end of file diff --git a/src/gui/src/helpers/item_icon.js b/src/gui/src/helpers/item_icon.js index d078c5cb8..ec2390211 100644 --- a/src/gui/src/helpers/item_icon.js +++ b/src/gui/src/helpers/item_icon.js @@ -196,58 +196,6 @@ const item_icon = async (fsentry)=>{ } // *.weblink else if(fsentry.name.toLowerCase().endsWith('.weblink')){ - let faviconUrl = null; - - // First try to get icon from data attribute - if (fsentry.icon) { - faviconUrl = fsentry.icon; - } - // Then try metadata - else if (fsentry.metadata) { - try { - const metadata = JSON.parse(fsentry.metadata); - if (metadata && metadata.faviconUrl) { - faviconUrl = metadata.faviconUrl; - } else if (metadata && metadata.url) { - // If we have the URL but no favicon, generate the Google favicon URL - const urlObj = new URL(metadata.url); - faviconUrl = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; - } - } catch (e) { - console.error("Error parsing weblink metadata:", e); - } - } - // Finally try content - else if (fsentry.content) { - try { - const content = JSON.parse(fsentry.content); - if (content && content.faviconUrl) { - faviconUrl = content.faviconUrl; - } else if (content && content.url) { - // If we have the URL but no favicon, generate the Google favicon URL - const urlObj = new URL(content.url); - faviconUrl = `https://www.google.com/s2/favicons?domain=${urlObj.hostname}&sz=64`; - } - } catch (e) { - console.error("Error parsing weblink content:", e); - } - } - - // If we found a favicon URL, use it - if (faviconUrl) { - return { - image: faviconUrl, - type: 'icon', - onerror: function() { - // If favicon fails to load, switch to default icon - const $icons = $(`img[data-icon="${faviconUrl}"]`); - $icons.attr('src', window.icons['link.svg']); - return window.icons['link.svg']; - } - }; - } - - // Fallback to default link icon return {image: window.icons['link.svg'], type: 'icon'}; } // -------------------------------------------------- diff --git a/src/gui/src/helpers/new_context_menu_item.js b/src/gui/src/helpers/new_context_menu_item.js index 8dbc2d0fa..ae778606f 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -307,10 +307,10 @@ const new_context_menu_item = function(dirname, append_to_element){ }); if (url) { - // Extract domain for naming and favicon - try { - const urlObj = new URL(url); - const domain = urlObj.hostname; + // Extract domain for naming + try { + const urlObj = new URL(url); + const domain = urlObj.hostname; // Extract a simple name from the domain (e.g., "google" from "google.com") let siteName = domain.replace(/^www\./, ''); @@ -325,21 +325,9 @@ const new_context_menu_item = function(dirname, append_to_element){ let linkName = siteName; let fileName = linkName + '.weblink'; - // Get favicon URL from Google favicon service - let faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; - - // Check if we have a cached favicon - if (window.favicon_cache[domain]) { - faviconUrl = window.favicon_cache[domain]; - } - - // Store in favicon cache - window.favicon_cache[domain] = faviconUrl; - - // Store the URL and favicon in a comprehensive JSON object + // Store the URL in a simple JSON object const weblink_content = JSON.stringify({ url: url, - faviconUrl: faviconUrl, type: 'weblink', domain: domain, created: Date.now(), @@ -347,22 +335,20 @@ const new_context_menu_item = function(dirname, append_to_element){ version: '2.0', metadata: { originalUrl: url, - originalFaviconUrl: faviconUrl, linkName: linkName, simpleName: siteName } }); - // Create the file with favicon + // Create the file with standard link icon const item = await window.create_file({ dirname: dirname, append_to_element: append_to_element, name: fileName, content: weblink_content, - icon: faviconUrl, + icon: window.icons['link.svg'], type: 'weblink', metadata: JSON.stringify({ - faviconUrl: faviconUrl, url: url, domain: domain, timestamp: Date.now(), @@ -370,7 +356,7 @@ const new_context_menu_item = function(dirname, append_to_element){ }), html_attributes: { 'data-weblink': 'true', - 'data-icon': faviconUrl, + 'data-icon': window.icons['link.svg'], 'data-url': url, 'data-domain': domain, 'data-display-name': linkName, @@ -379,49 +365,6 @@ const new_context_menu_item = function(dirname, append_to_element){ force_refresh: true, class: 'weblink-item' }); - - // Apply icon using our consolidated function - if (item) { - applyWeblinkIcon(item, faviconUrl, url); - - // Ensure the item is visible in the container - const container = append_to_element || document.querySelector('.desktop, .explorer-container.active, .files-container.active'); - if (container) { - // Force a refresh of the container - await refresh_item_container(dirname); - - // Hide the extension in the displayed name - const $item = $(item); - const $nameElement = $item.find('.item-name'); - if ($nameElement.length > 0) { - // Store the original name for reference - $nameElement.attr('data-full-name', fileName); - $nameElement.attr('data-display-name', linkName); - - // Set the text directly (no extension) - $nameElement.text(linkName); - } - - // If this is the desktop, trigger a desktop refresh - if (container.classList.contains('desktop')) { - if (typeof window.refresh_desktop === 'function') { - window.refresh_desktop(); - } else if (typeof window.refresh_desktop_items === 'function') { - window.refresh_desktop_items(); - } - } - - // Trigger a custom event to ensure icon is properly applied - const event = new CustomEvent('weblink-created', { - detail: { - item: item, - url: url, - faviconUrl: faviconUrl - } - }); - document.dispatchEvent(event); - } - } } catch (error) { console.error("Error creating web link:", error); UIAlert("Error creating web link: " + error.message); From 1df874c581af000c5127d09845ef6ab7188693e4 Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 27 Jun 2025 22:49:58 -0700 Subject: [PATCH 4/8] remove `refresh_item_container` since it's not used --- src/gui/src/helpers/new_context_menu_item.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/gui/src/helpers/new_context_menu_item.js b/src/gui/src/helpers/new_context_menu_item.js index ae778606f..b544ff738 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -19,7 +19,6 @@ import UIPrompt from '../UI/UIPrompt.js'; import UIAlert from '../UI/UIAlert.js'; -import refresh_item_container from './refresh_item_container.js'; // Initialize the favicon cache if it doesn't exist window.favicon_cache = window.favicon_cache || {}; From ad62182566455a1a07735bccf82a4a94397a5301 Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 27 Jun 2025 22:53:49 -0700 Subject: [PATCH 5/8] Update new_context_menu_item.js --- src/gui/src/helpers/new_context_menu_item.js | 228 ------------------- 1 file changed, 228 deletions(-) diff --git a/src/gui/src/helpers/new_context_menu_item.js b/src/gui/src/helpers/new_context_menu_item.js index b544ff738..394456b75 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -23,234 +23,6 @@ import UIAlert from '../UI/UIAlert.js'; // Initialize the favicon cache if it doesn't exist window.favicon_cache = window.favicon_cache || {}; -// Single, unified function for applying favicons to weblink items -function applyWeblinkIcon(item, faviconUrl, url) { - // Validate inputs - if (!item || !faviconUrl) return false; - - const $item = $(item); - if (!$item.length) return false; - - try { - // Clear any existing icons first to prevent stacking - $item.find('img.item-icon').remove(); - - // Create a fresh icon element - const $icon = $(''); - $item.prepend($icon); - - // Set core attributes - $icon.attr({ - 'src': faviconUrl, - 'data-icon': faviconUrl - }); - - // Handle loading errors - $icon.on('error', function() { - this.src = window.icons['link.svg']; - }); - - // Set essential item attributes - $item.attr({ - 'data-icon': faviconUrl, - 'data-weblink': 'true' - }); - - // Store in favicon cache - if (url) { - try { - const domain = new URL(url).hostname; - window.favicon_cache[domain] = faviconUrl; - - // Also store in localStorage for persistence - localStorage.setItem(`favicon_${domain}`, faviconUrl); - - // Add domain to item attributes - $item.attr('data-domain', domain); - } catch (e) { - console.error("Error processing URL:", e); - } - } - - return true; - } catch (e) { - console.error("Error applying weblink icon:", e); - return false; - } -} - -// Set up global CSS once at startup -(function setupWeblinkStyles() { - let styleTag = document.getElementById('weblink-global-styles'); - if (!styleTag) { - styleTag = document.createElement('style'); - styleTag.id = 'weblink-global-styles'; - document.head.appendChild(styleTag); - - styleTag.textContent = ` - /* Ensure weblink icons are always visible */ - .item[data-weblink="true"] .item-icon, - .item[data-name$=".weblink"] .item-icon, - .weblink-icon { - visibility: visible !important; - opacity: 1 !important; - display: block !important; - width: 32px !important; - height: 32px !important; - margin: 0 auto !important; - background-size: contain !important; - object-fit: contain !important; - } - - /* Hide .weblink extension completely */ - .item[data-name$=".weblink"] .item-name { - visibility: hidden; - position: relative; - } - - /* Show only the site name */ - .item[data-name$=".weblink"] .item-name:after { - content: attr(data-display-name); - visibility: visible; - position: absolute; - left: 0; - right: 0; - text-align: center; - } - - /* Prevent multiple icons */ - .item[data-weblink="true"] img.item-icon ~ img.item-icon { - display: none !important; - } - `; - } - - // Add a global hook to modify file names when displayed - if (!window.originalItemNameDisplay) { - // Backup the original function if it exists - window.originalItemNameDisplay = window.update_item_name; - - // Override the function that updates item names - window.update_item_name = function(item, newName) { - // Call the original function first - if (window.originalItemNameDisplay) { - window.originalItemNameDisplay(item, newName); - } - - // Then check if this is a weblink and modify the display - if (item && $(item).length) { - const $item = $(item); - const itemName = $item.attr('data-name') || newName; - - // Check if this is a weblink - if (itemName && itemName.toLowerCase().endsWith('.weblink')) { - // Extract the simple name (without extension) - const nameWithoutExt = itemName.replace(/\.weblink$/i, ''); - - // Find the name element - const $nameEl = $item.find('.item-name'); - if ($nameEl.length) { - // Store the full name in data attribute - $nameEl.attr('data-full-name', itemName); - $nameEl.attr('data-display-name', nameWithoutExt); - - // Set the text directly - $nameEl.text(nameWithoutExt); - } - } - } - }; - } -})(); - -// Set up a global weblink item observer -(function setupWeblinkObserver() { - // Skip if already initialized - if (window.weblinkObserverInitialized) return; - window.weblinkObserverInitialized = true; - - // Function to find and fix weblink items - const fixWeblinkItems = () => { - // Find all weblink items - const weblinkItems = document.querySelectorAll('.item[data-name$=".weblink"]'); - - weblinkItems.forEach(item => { - const $item = $(item); - - // Skip if already processed - if ($item.attr('data-weblink-processed') === 'true') return; - - // Apply icon - const faviconUrl = $item.attr('data-icon') || $item.attr('data-favicon') || $item.attr('data-icon-url'); - const url = $item.attr('data-url'); - - // Apply icons first - if (faviconUrl) { - applyWeblinkIcon(item, faviconUrl, url); - } - - // Fix the display name - const $nameEl = $item.find('.item-name'); - if ($nameEl.length) { - const itemName = $item.attr('data-name'); - if (itemName) { - // Strip extension - const nameWithoutExt = itemName.replace(/\.weblink$/i, ''); - - // Apply to display name - $nameEl.attr('data-full-name', itemName); - $nameEl.attr('data-display-name', nameWithoutExt); - $nameEl.text(nameWithoutExt); - } - } - - // Mark as processed to avoid duplicate processing - $item.attr('data-weblink-processed', 'true'); - }); - }; - - // Run immediately - fixWeblinkItems(); - - // Set up MutationObserver to watch for DOM changes - const observer = new MutationObserver((mutations) => { - let shouldFix = false; - - mutations.forEach(mutation => { - // Check if we need to reapply icons and names - if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { - for (const node of mutation.addedNodes) { - if (node.nodeType === Node.ELEMENT_NODE) { - if ( - node.classList && - (node.classList.contains('item') || node.querySelector('.item')) - ) { - shouldFix = true; - break; - } - } - } - } - }); - - if (shouldFix) { - fixWeblinkItems(); - } - }); - - // Watch for changes to item names - observer.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ['data-name'] - }); - - // Also run when desktop refreshes - document.addEventListener('desktop-refreshed', fixWeblinkItems); - document.addEventListener('directory-loaded', fixWeblinkItems); -})(); - /** * Returns a context menu item to create a new folder and a variety of file types. * From c4e32474b2a033b11e2bcee26e7b15e8399bad20 Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 27 Jun 2025 23:05:53 -0700 Subject: [PATCH 6/8] Remove some redundant checks and favicon handling logic --- src/gui/src/helpers/new_context_menu_item.js | 3 - src/gui/src/helpers/open_item.js | 379 +------------------ 2 files changed, 1 insertion(+), 381 deletions(-) diff --git a/src/gui/src/helpers/new_context_menu_item.js b/src/gui/src/helpers/new_context_menu_item.js index 394456b75..cfaf3e5f4 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -20,9 +20,6 @@ import UIPrompt from '../UI/UIPrompt.js'; import UIAlert from '../UI/UIAlert.js'; -// Initialize the favicon cache if it doesn't exist -window.favicon_cache = window.favicon_cache || {}; - /** * Returns a context menu item to create a new folder and a variety of file types. * diff --git a/src/gui/src/helpers/open_item.js b/src/gui/src/helpers/open_item.js index 7342ab4c6..335f7028c 100644 --- a/src/gui/src/helpers/open_item.js +++ b/src/gui/src/helpers/open_item.js @@ -53,211 +53,29 @@ const open_item = async function(options){ //---------------------------------------------------------------- else if($(el_item).attr('data-name').toLowerCase().endsWith('.weblink')){ try { - // Log the file information - console.log("Opening weblink file:", { - name: $(el_item).attr('data-name'), - path: item_path, - uid: file_uid - }); - // First check localStorage using the file's UID let url = null; if (file_uid) { url = localStorage.getItem('weblink_' + file_uid); - console.log("Retrieved URL from localStorage:", url); } // Try to read the file content directly using the file's path if (!url) { try { - // Convert Windows-style path to unix-style path - const unixPath = item_path.replace(/\\/g, '/'); - console.log("Trying to read file content using unix-style path:", unixPath); - - // Try using the simpler puter.fs.read method with unix-style path const content = await puter.fs.read({ - path: unixPath + path: item_path }); - console.log("File content read successfully:", content); - // Handle different content types if (content instanceof Blob) { // If content is a Blob, convert it to text - console.log("Content is a Blob, converting to text"); const text = await content.text(); - console.log("Blob converted to text:", text); // Try to parse the text as JSON try { const jsonData = JSON.parse(text); if (jsonData.url) { url = jsonData.url; - console.log("Retrieved URL from Blob content (JSON):", url); - - // Check for icon data in the JSON (support both old and new formats) - const iconUrl = jsonData.iconDataUrl || jsonData.faviconUrl; - if (iconUrl) { - console.log("Found icon URL in JSON:", iconUrl); - - // Use the favicon URL directly without generating a custom icon - const customIconUrl = iconUrl; - - // Store the icon URL in a global cache to ensure it persists - if (!window.weblink_icon_cache) { - window.weblink_icon_cache = {}; - } - - // Store by both UID and path for maximum reliability - if (file_uid) { - window.weblink_icon_cache[file_uid] = customIconUrl; - } - if (item_path) { - window.weblink_icon_cache[item_path] = customIconUrl; - } - - // Also store in localStorage for persistence across page refreshes - try { - localStorage.setItem(`weblink_icon_${file_uid}`, customIconUrl); - localStorage.setItem(`weblink_icon_${item_path.replace(/[^a-zA-Z0-9]/g, '_')}`, customIconUrl); - } catch (e) { - console.error("Error storing icon URL in localStorage:", e); - } - - // Create a new image element with the custom icon - const iconImg = document.createElement('img'); - iconImg.src = customIconUrl; - iconImg.className = 'item-icon persistent-icon'; - iconImg.style.width = '32px'; - iconImg.style.height = '32px'; - // Add important attributes to prevent the icon from being changed - iconImg.setAttribute('data-original-icon', customIconUrl); - iconImg.setAttribute('data-icon-locked', 'true'); - - // Apply multiple approaches to ensure the icon persists - console.log("Applying persistent icon using multiple approaches"); - - // APPROACH 1: Replace the icon element completely - const existingIcon = $(el_item).find('img.item-icon'); - let iconUpdated = false; - if (existingIcon.length > 0) { - // Create a completely new element to avoid any references to the old one - const newIconElement = document.createElement('img'); - newIconElement.className = 'item-icon persistent-icon'; - newIconElement.src = customIconUrl; - newIconElement.style.width = '32px'; - newIconElement.style.height = '32px'; - newIconElement.setAttribute('data-original-icon', customIconUrl); - newIconElement.setAttribute('data-icon-locked', 'true'); - - // Replace the existing icon with our new one - existingIcon[0].parentNode.replaceChild(newIconElement, existingIcon[0]); - console.log("Replaced icon with persistent icon (approach 1)"); - iconUpdated = true; - - // Add event listener to prevent the src from being changed - newIconElement.addEventListener('load', function() { - console.log("Icon loaded successfully"); - }); - - // Handle favicon loading error - newIconElement.addEventListener('error', function() { - console.log("Icon failed to load, using fallback"); - this.src = window.icons['link.svg']; - }); - } - - // APPROACH 2: Update all img elements inside the item - const allImgs = $(el_item).find('img'); - if (allImgs.length > 0) { - allImgs.each(function() { - $(this).attr('src', customIconUrl); - $(this).attr('data-original-icon', customIconUrl); - $(this).attr('data-icon-locked', 'true'); - // Add !important to the style to prevent it from being overridden - $(this).attr('style', `width: 32px !important; height: 32px !important;`); - }); - console.log("Updated all img elements with persistent icon (approach 2)"); - iconUpdated = true; - } - - // APPROACH 3: Add CSS styles to force the icon - // Get a unique identifier for this item - const itemId = $(el_item).attr('id') || $(el_item).attr('data-uid') || `weblink-${Date.now()}`; - if (!$(el_item).attr('id')) { - $(el_item).attr('id', itemId); - } - - // Add a style tag with !important rules - const styleId = `style-${itemId}`; - let styleTag = document.getElementById(styleId); - if (!styleTag) { - styleTag = document.createElement('style'); - styleTag.id = styleId; - document.head.appendChild(styleTag); - } - - styleTag.textContent = ` - #${itemId} img.item-icon, - [data-uid="${file_uid}"] img.item-icon { - content: url('${customIconUrl}') !important; - background-image: url('${customIconUrl}') !important; - } - `; - - // APPROACH 4: Set data attributes on the item element - $(el_item).attr({ - 'data-icon': customIconUrl, - 'data-original-icon': customIconUrl, - 'data-icon-locked': 'true', - 'data-weblink-icon': customIconUrl - }); - - // APPROACH 5: Add a MutationObserver to ensure the icon persists - try { - // Create a MutationObserver to watch for changes to the icon - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - mutation.attributeName === 'src' && - mutation.target.classList.contains('item-icon')) { - // If the src attribute of the icon was changed, set it back - if (mutation.target.src !== customIconUrl) { - console.log("MutationObserver: Icon src changed, resetting"); - mutation.target.src = customIconUrl; - } - } - }); - }); - - // Start observing the item element - observer.observe(el_item, { - attributes: true, - childList: true, - subtree: true, - attributeFilter: ['src'] - }); - - // Store the observer in a global variable to prevent garbage collection - if (!window.icon_observers) { - window.icon_observers = {}; - } - window.icon_observers[file_uid] = observer; - - console.log("Set up MutationObserver to ensure icon persists"); - } catch (e) { - console.error("Error setting up MutationObserver:", e); - } - - // Force a refresh of the item's icon - if (!iconUpdated) { - console.log("Could not find icon element to replace, forcing refresh"); - // Try to trigger a refresh of the item - setTimeout(() => { - $(el_item).trigger('icon_update', { icon: customIconUrl }); - }, 100); - } - } } } catch (e) { console.error("Error parsing Blob content as JSON:", e); @@ -273,161 +91,6 @@ const open_item = async function(options){ const jsonData = JSON.parse(content); if (jsonData.url) { url = jsonData.url; - console.log("Retrieved URL from string content (JSON):", url); - - // Check for icon data in the JSON (support both old and new formats) - const iconUrl = jsonData.iconDataUrl || jsonData.faviconUrl; - if (iconUrl) { - console.log("Found icon URL in JSON:", iconUrl); - - // Use the favicon URL directly without generating a custom icon - const customIconUrl = iconUrl; - - // Store the icon URL in a global cache to ensure it persists - if (!window.weblink_icon_cache) { - window.weblink_icon_cache = {}; - } - - // Store by both UID and path for maximum reliability - if (file_uid) { - window.weblink_icon_cache[file_uid] = customIconUrl; - } - if (item_path) { - window.weblink_icon_cache[item_path] = customIconUrl; - } - - // Also store in localStorage for persistence across page refreshes - try { - localStorage.setItem(`weblink_icon_${file_uid}`, customIconUrl); - localStorage.setItem(`weblink_icon_${item_path.replace(/[^a-zA-Z0-9]/g, '_')}`, customIconUrl); - } catch (e) { - console.error("Error storing icon URL in localStorage:", e); - } - - // Apply multiple approaches to ensure the icon persists - console.log("Applying persistent icon using multiple approaches"); - - // APPROACH 1: Replace the icon element completely - const existingIcon = $(el_item).find('img.item-icon'); - let iconUpdated = false; - if (existingIcon.length > 0) { - // Create a completely new element to avoid any references to the old one - const newIconElement = document.createElement('img'); - newIconElement.className = 'item-icon persistent-icon'; - newIconElement.src = customIconUrl; - newIconElement.style.width = '32px'; - newIconElement.style.height = '32px'; - newIconElement.setAttribute('data-original-icon', customIconUrl); - newIconElement.setAttribute('data-icon-locked', 'true'); - - // Replace the existing icon with our new one - existingIcon[0].parentNode.replaceChild(newIconElement, existingIcon[0]); - console.log("Replaced icon with persistent icon (approach 1)"); - iconUpdated = true; - - // Add event listener to prevent the src from being changed - newIconElement.addEventListener('load', function() { - console.log("Icon loaded successfully"); - }); - - // Handle favicon loading error - newIconElement.addEventListener('error', function() { - console.log("Icon failed to load, using fallback"); - this.src = window.icons['link.svg']; - }); - } - - // APPROACH 2: Update all img elements inside the item - const allImgs = $(el_item).find('img'); - if (allImgs.length > 0) { - allImgs.each(function() { - $(this).attr('src', customIconUrl); - $(this).attr('data-original-icon', customIconUrl); - $(this).attr('data-icon-locked', 'true'); - // Add !important to the style to prevent it from being overridden - $(this).attr('style', `width: 32px !important; height: 32px !important;`); - }); - console.log("Updated all img elements with persistent icon (approach 2)"); - iconUpdated = true; - } - - // APPROACH 3: Add CSS styles to force the icon - // Get a unique identifier for this item - const itemId = $(el_item).attr('id') || $(el_item).attr('data-uid') || `weblink-${Date.now()}`; - if (!$(el_item).attr('id')) { - $(el_item).attr('id', itemId); - } - - // Add a style tag with !important rules - const styleId = `style-${itemId}`; - let styleTag = document.getElementById(styleId); - if (!styleTag) { - styleTag = document.createElement('style'); - styleTag.id = styleId; - document.head.appendChild(styleTag); - } - - styleTag.textContent = ` - #${itemId} img.item-icon, - [data-uid="${file_uid}"] img.item-icon { - content: url('${customIconUrl}') !important; - background-image: url('${customIconUrl}') !important; - } - `; - - // APPROACH 4: Set data attributes on the item element - $(el_item).attr({ - 'data-icon': customIconUrl, - 'data-original-icon': customIconUrl, - 'data-icon-locked': 'true', - 'data-weblink-icon': customIconUrl - }); - - // APPROACH 5: Add a MutationObserver to ensure the icon persists - try { - // Create a MutationObserver to watch for changes to the icon - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - mutation.attributeName === 'src' && - mutation.target.classList.contains('item-icon')) { - // If the src attribute of the icon was changed, set it back - if (mutation.target.src !== customIconUrl) { - console.log("MutationObserver: Icon src changed, resetting"); - mutation.target.src = customIconUrl; - } - } - }); - }); - - // Start observing the item element - observer.observe(el_item, { - attributes: true, - childList: true, - subtree: true, - attributeFilter: ['src'] - }); - - // Store the observer in a global variable to prevent garbage collection - if (!window.icon_observers) { - window.icon_observers = {}; - } - window.icon_observers[file_uid] = observer; - - console.log("Set up MutationObserver to ensure icon persists"); - } catch (e) { - console.error("Error setting up MutationObserver:", e); - } - - // Force a refresh of the item's icon - if (!iconUpdated) { - console.log("Could not find icon element to replace, forcing refresh"); - // Try to trigger a refresh of the item - setTimeout(() => { - $(el_item).trigger('icon_update', { icon: customIconUrl }); - }, 100); - } - } } } catch (e) { console.error("Error parsing string content as JSON:", e); @@ -442,54 +105,14 @@ const open_item = async function(options){ } } catch (e) { console.error("Error reading file using path:", e); - - // Fallback to using AJAX with the file's UID - try { - console.log("Trying to read file content using UID:", file_uid); - - const content = await $.ajax({ - url: window.api_origin + "/fs/read", - type: 'POST', - contentType: "application/json", - data: JSON.stringify({ - uid: file_uid - }), - headers: { - "Authorization": "Bearer " + window.auth_token - } - }); - - console.log("File content read successfully using AJAX:", content); - - // Try to parse the content as JSON - if (content && content.content) { - try { - const jsonData = JSON.parse(content.content); - if (jsonData.url) { - url = jsonData.url; - console.log("Retrieved URL from file content (AJAX JSON):", url); - } - } catch (e) { - // Not valid JSON, try using the content directly - if (content.content && (content.content.startsWith('http://') || content.content.startsWith('https://'))) { - url = content.content; - console.log("Using file content as URL (AJAX direct):", url); - } - } - } - } catch (e) { - console.error("Error reading file using AJAX:", e); - } } } // If we have a valid URL, open it if (url && (url.startsWith('http://') || url.startsWith('https://'))) { - console.log("Opening URL:", url); window.open(url, '_blank', 'noopener,noreferrer'); } else { // Show a more detailed error message - console.error("Failed to retrieve URL from all sources"); UIAlert(`Could not determine the URL for this web shortcut. Technical details: From 75d22b010c976245f4438f80dde046050619d038 Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 27 Jun 2025 23:09:25 -0700 Subject: [PATCH 7/8] Update package-lock.json --- package-lock.json | 108 +++------------------------------------------- 1 file changed, 5 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index d977df0e1..eb3f66dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,8 +21,6 @@ "javascript-time-ago": "^2.5.11", "json-colorizer": "^3.0.1", "open": "^10.1.0", - "openai": "^4.73.1", - "qs": "^6.14.0", "sharp": "^0.33.5", "sharp-bmp": "^0.1.5", "sharp-ico": "^0.1.5", @@ -7189,21 +7187,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -8728,18 +8711,6 @@ "node": ">= 0.4" } }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es6-error": { "version": "4.1.1", "dev": true, @@ -9089,21 +9060,6 @@ "version": "2.0.0", "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "funding": [ @@ -13230,7 +13186,7 @@ "version": "6.13.0", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -14007,64 +13963,10 @@ "version": "1.0.6", "license": "MIT", "dependencies": { + "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" }, "engines": { "node": ">= 0.4" @@ -16423,4 +16325,4 @@ "license": "AGPL-3.0-only" } } -} +} \ No newline at end of file From 06f5601a2ebe94c4651bc65220a6fcc5cd9fc910 Mon Sep 17 00:00:00 2001 From: jelveh Date: Fri, 27 Jun 2025 23:10:53 -0700 Subject: [PATCH 8/8] Update package.json --- package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package.json b/package.json index 53f0e6f6d..8c0a4f373 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,6 @@ "javascript-time-ago": "^2.5.11", "json-colorizer": "^3.0.1", "open": "^10.1.0", - "openai": "^4.73.1", - "qs": "^6.14.0", "sharp": "^0.33.5", "sharp-bmp": "^0.1.5", "sharp-ico": "^0.1.5", @@ -63,4 +61,4 @@ "string-template": "^1.0.0", "uuid": "^9.0.1" } -} +} \ No newline at end of file