Implement link shortcut feature (refs #682)

This commit is contained in:
rodrick-mpofu
2025-03-25 02:24:20 -04:00
parent 7ed779bbf3
commit 94853d33bf
11 changed files with 2322 additions and 70 deletions
+240
View File
@@ -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: '<img src="' + window.icons['link.svg'] + '" style="width:16px; height:16px; margin-bottom: -3px;">',
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: '<img src="' + window.icons['link.svg'] + '" style="width:16px; height:16px; margin-bottom: -3px;">',
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;
@@ -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"]
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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');
}
}
}
};
@@ -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": "*"
}
}
@@ -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: '<img src="' + window.icons['link.svg'] + '" style="width:16px; height:16px; margin-bottom: -3px;">',
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');
})();
+237 -68
View File
@@ -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"
+2
View File
@@ -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",
+56
View File
@@ -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
// --------------------------------------------------
File diff suppressed because it is too large Load Diff
+456
View File
@@ -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 + '/')){
+12
View File
@@ -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;