diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css index 4c7d1637e2..a291bff67f 100644 --- a/src/gui/src/css/style.css +++ b/src/gui/src/css/style.css @@ -112,6 +112,7 @@ pre { --select-saturation: 74.22%; --select-lightness: 55.88%; --select-color: hsl(var(--select-hue), var(--select-saturation), var(--select-lightness)); + --sidebar-header-text-color: hsl(210, 41.18%, 90%); /* Default to light color */ } html, body { @@ -1188,7 +1189,7 @@ span.header-sort-icon img { height: calc(100% - 30px); } -.window-cover-page.window-filedialog .window-body { +.window-cover-page .window-body { height: calc(100% - 109px) !important; } @@ -1218,7 +1219,7 @@ span.header-sort-icon img { margin: 0; font-weight: bold; font-size: 13px; - color: #8f96a3; + color: var(--sidebar-header-text-color); text-shadow: 1px 1px rgb(247 247 247 / 15%); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; diff --git a/src/gui/src/helpers.js b/src/gui/src/helpers.js index f855fa62e6..bb1e83ad11 100644 --- a/src/gui/src/helpers.js +++ b/src/gui/src/helpers.js @@ -870,6 +870,49 @@ window.create_file = async(options)=>{ } } +// Create a .weblink file storing a URL +window.create_weblink = async (options) => { + let dirname = options.dirname; + let appendto_element = options.append_to_element; + let urlStr = String(options.url ?? '').trim(); + + if(!(urlStr.startsWith('http://') || urlStr.startsWith('https://'))){ + await UIAlert({ message: i18n('enter_valid_url') ?? 'Please enter a valid URL starting with http:// or https://.' }); + return; + } + + let hostname = 'Link'; + try{ + const u = new URL(urlStr); + hostname = u.hostname.replace(/^www\./i, ''); + }catch(e){ + // fallback + } + + const filename = `${hostname}.weblink`; + + try{ + await puter.fs.upload(new File([urlStr], filename, { type: 'text/plain' }), dirname, { + overwrite: false, + dedupeName: true, + success: async function (data){ + const created_file = $(appendto_element).find('.item[data-path="'+html_encode(dirname)+'/'+html_encode(data.name)+'"]'); + if(created_file.length > 0){ + // Do not immediately rename; keep derived name + // Add action to actions_history for undo ability + window.actions_history.push({ + operation: 'create_file', + data: created_file + }); + } + } + }); + }catch(err){ + console.log(err); + await UIAlert({ message: err?.message ?? (i18n('failed_to_create') ?? 'Failed to create link.') }); + } +} + window.available_templates = async () => { const baseRoute = `/${window.user.username}` const keywords = ["template", "templates", i18n('template')] diff --git a/src/gui/src/helpers/item_icon.js b/src/gui/src/helpers/item_icon.js index 44d8d9c6ae..87bdadeef7 100644 --- a/src/gui/src/helpers/item_icon.js +++ b/src/gui/src/helpers/item_icon.js @@ -194,6 +194,11 @@ 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')){ + // Use HTML file icon as a recognizable web link indicator + return {image: window.icons['file-html.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 64c0da2bf8..49ab40b8b3 100644 --- a/src/gui/src/helpers/new_context_menu_item.js +++ b/src/gui/src/helpers/new_context_menu_item.js @@ -55,6 +55,21 @@ 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'}); } }, + // Link Shortcut + { + html: 'New Link', + icon: ``, + onClick: async function(){ + const url = prompt('Enter URL (http:// or https://)', 'https://'); + if(!url) + return; + if(!(url.startsWith('http://') || url.startsWith('https://'))){ + alert('Please enter a valid URL starting with http:// or https://.'); + return; + } + await window.create_weblink({ dirname: dirname, append_to_element: append_to_element, url }); + } + }, // 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 efdf3686fd..5b6095dd47 100644 --- a/src/gui/src/helpers/open_item.js +++ b/src/gui/src/helpers/open_item.js @@ -130,6 +130,30 @@ const open_item = async function(options){ } } //---------------------------------------------------------------- + // Web link shortcut (.weblink): open URL in new tab + //---------------------------------------------------------------- + else if(!is_dir && path.extname(item_path).toLowerCase() === '.weblink'){ + try{ + let urlText = await puter.fs.read(item_path); + if (typeof urlText === 'object' && urlText?.text) { + urlText = await urlText.text(); + } + const urlStr = (typeof urlText === 'string') ? urlText.trim() : String(urlText ?? '').trim(); + const isHttp = urlStr.startsWith('http://') || urlStr.startsWith('https://'); + if(!isHttp){ + await UIAlert({ message: i18n('invalid_url') ?? 'Invalid URL in link shortcut.' }); + return; + } + const newWin = window.open(urlStr, '_blank', 'noopener,noreferrer'); + if(!newWin){ + await UIAlert({ message: i18n('popup_blocked') ?? 'Popup was blocked. Please allow popups for this site.' }); + } + }catch(e){ + console.log(e); + await UIAlert({ message: i18n('failed_to_open') ?? 'Failed to open link.' }); + } + } + //---------------------------------------------------------------- // Does the user have a preference for this file type? //---------------------------------------------------------------- else if(!associated_app_name && !is_dir && window.user_preferences[`default_apps${path.extname(item_path).toLowerCase()}`]) { diff --git a/src/gui/src/keyboard.js b/src/gui/src/keyboard.js index 23c5871470..2b7ec517b1 100644 --- a/src/gui/src/keyboard.js +++ b/src/gui/src/keyboard.js @@ -733,10 +733,6 @@ $(document).bind("keyup keydown", async function(e){ if((e.ctrlKey || e.metaKey) && e.which === 86 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){ let target_path, target_el; - // continue only if there is something in the clipboard - if(window.clipboard.length === 0) - return; - let parent_container = determine_active_container_parent(); if(parent_container){ @@ -745,11 +741,29 @@ $(document).bind("keyup keydown", async function(e){ // don't allow pasting in Trash if((target_path === window.trash_path || target_path.startsWith(window.trash_path + '/')) && window.clipboard_op !== 'move') return; - // execute clipboard operation - if(window.clipboard_op === 'copy') - window.copy_clipboard_items(target_path); - else if(window.clipboard_op === 'move') - window.move_clipboard_items(target_el, target_path); + + // If Puter clipboard has items, perform normal paste + if(window.clipboard.length > 0){ + if(window.clipboard_op === 'copy') + window.copy_clipboard_items(target_path); + else if(window.clipboard_op === 'move') + window.move_clipboard_items(target_el, target_path); + return false; + } + + // Otherwise, try system clipboard text as URL + try{ + if(navigator.clipboard && navigator.clipboard.readText){ + navigator.clipboard.readText().then(async (clipText)=>{ + const text = (clipText ?? '').trim(); + if(text && (text.startsWith('http://') || text.startsWith('https://'))){ + await window.create_weblink({ dirname: target_path, append_to_element: target_el, url: text }); + } + }); + } + }catch(err){ + // ignored + } } return false; } diff --git a/src/gui/src/services/ThemeService.js b/src/gui/src/services/ThemeService.js index b49d41e684..3221f1e1df 100644 --- a/src/gui/src/services/ThemeService.js +++ b/src/gui/src/services/ThemeService.js @@ -150,3 +150,14 @@ export class ThemeService extends Service { )); } } + +function adjustSidebarHeaderTextColor(hue, saturation, transparency, lightness) { + // Calculate adjusted brightness based on lightness and transparency + const adjustedLightness = lightness * (1 - transparency); + return adjustedLightness > 50 ? `hsl(${hue}, ${saturation}%, 10%)` : `hsl(${hue}, ${saturation}%, 90%)`; +} + +export function updateTheme(hue, saturation, transparency, lightness) { + const sidebarHeaderTextColor = adjustSidebarHeaderTextColor(hue, saturation, transparency, lightness); + document.documentElement.style.setProperty('--sidebar-header-text-color', sidebarHeaderTextColor); +}