diff --git a/package-lock.json b/package-lock.json index 4e781e338..eb3f66dfd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16325,4 +16325,4 @@ "license": "AGPL-3.0-only" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 2a7b70a15..8c0a4f373 100644 --- a/package.json +++ b/package.json @@ -61,4 +61,4 @@ "string-template": "^1.0.0", "uuid": "^9.0.1" } -} +} \ No newline at end of file diff --git a/src/gui/src/helpers/item_icon.js b/src/gui/src/helpers/item_icon.js index 18cf51836..ec2390211 100644 --- a/src/gui/src/helpers/item_icon.js +++ b/src/gui/src/helpers/item_icon.js @@ -194,6 +194,10 @@ 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')){ + 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..cfaf3e5f4 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -7,16 +7,18 @@ * 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'; /** * Returns a context menu item to create a new folder and a variety of file types. @@ -55,6 +57,89 @@ 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) { + // Extract domain for naming + try { + const urlObj = new URL(url); + const domain = urlObj.hostname; + + // Extract a simple name from the domain (e.g., "google" from "google.com") + let siteName = domain.replace(/^www\./, ''); + + // Further simplify by removing the TLD (.com, .org, etc.) + siteName = siteName.split('.')[0]; + + // Capitalize the first letter + siteName = siteName.charAt(0).toUpperCase() + siteName.slice(1); + + // Use simple name but keep .weblink extension for the file system + let linkName = siteName; + let fileName = linkName + '.weblink'; + + // Store the URL in a simple JSON object + const weblink_content = JSON.stringify({ + url: url, + type: 'weblink', + domain: domain, + created: Date.now(), + modified: Date.now(), + version: '2.0', + metadata: { + originalUrl: url, + linkName: linkName, + simpleName: siteName + } + }); + + // Create the file with standard link icon + const item = await window.create_file({ + dirname: dirname, + append_to_element: append_to_element, + name: fileName, + content: weblink_content, + icon: window.icons['link.svg'], + type: 'weblink', + metadata: JSON.stringify({ + url: url, + domain: domain, + timestamp: Date.now(), + version: '2.0' + }), + html_attributes: { + 'data-weblink': 'true', + 'data-icon': window.icons['link.svg'], + 'data-url': url, + 'data-domain': domain, + 'data-display-name': linkName, + 'data-hide-extension': 'true' + }, + force_refresh: true, + class: 'weblink-item' + }); + } 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..335f7028c 100644 --- a/src/gui/src/helpers/open_item.js +++ b/src/gui/src/helpers/open_item.js @@ -49,6 +49,85 @@ 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 { + // First check localStorage using the file's UID + let url = null; + if (file_uid) { + url = localStorage.getItem('weblink_' + file_uid); + } + + // Try to read the file content directly using the file's path + if (!url) { + try { + const content = await puter.fs.read({ + path: item_path + }); + + // Handle different content types + if (content instanceof Blob) { + // If content is a Blob, convert it to text + const text = await content.text(); + + // Try to parse the text as JSON + try { + const jsonData = JSON.parse(text); + if (jsonData.url) { + url = jsonData.url; + } + } 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; + } + } 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); + } + } + + // If we have a valid URL, open it + if (url && (url.startsWith('http://') || url.startsWith('https://'))) { + window.open(url, '_blank', 'noopener,noreferrer'); + } else { + // Show a more detailed error message + 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;