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;