From 94853d33bf9db0fc3ccd663ec5cae0566c4a390e Mon Sep 17 00:00:00 2001 From: rodrick-mpofu Date: Tue, 25 Mar 2025 02:24:20 -0400 Subject: [PATCH] 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;