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;