diff --git a/doc/scratch.md b/doc/scratch.md index 51d001c4bd..13595cf7ca 100644 --- a/doc/scratch.md +++ b/doc/scratch.md @@ -83,3 +83,549 @@ await (async () => { return await response.json(); })(); ``` + +## 2025-01-XX: Fix Sidebar Header Text Contrast Bug + +### Bug Description +When adjusting the lightness level of screen themes, sidebar header texts in the explorer become unreadable due to poor contrast between text and background colors. + +**Current Behavior:** +- Sidebar header text color is hardcoded to `#8f96a3` in `.window-sidebar-title` CSS class +- Sidebar background adapts to theme lightness via CSS variables +- At certain lightness values, the contrast between hardcoded text color and background becomes insufficient +- Text becomes difficult or impossible to read + +**Expected Behavior:** +- Sidebar header text should remain readable with adequate contrast across all theme lightness settings +- Should meet WCAG accessibility standards (minimum 4.5:1 contrast ratio for normal text) + +### Root Cause Analysis + +**Files Involved:** +1. `src/gui/src/css/style.css` (lines 1217-1234) + - `.window-sidebar-title` has hardcoded `color: #8f96a3;` + - Sidebar background uses: `hsla(var(--window-sidebar-hue), var(--window-sidebar-saturation), var(--window-sidebar-lightness), calc(0.5 + 0.5*var(--window-sidebar-alpha)))` + +2. `src/gui/src/services/ThemeService.js` + - Sets CSS variables: `--window-sidebar-hue`, `--window-sidebar-saturation`, `--window-sidebar-lightness`, `--window-sidebar-alpha` + - Sets `--window-sidebar-color` to `var(--primary-color)` which is either white or '#373e44' + - Does NOT set a variable for sidebar title text color + +3. `src/gui/src/css/style.css` (lines 99-103) + - CSS variables defined: `--window-sidebar-hue`, `--window-sidebar-saturation`, `--window-sidebar-lightness`, `--window-sidebar-alpha`, `--window-sidebar-color` + +### Proposed Solution + +#### Step 1: Create Contrast Calculation Utility +- **File**: `src/gui/src/services/ThemeService.js` (or create separate utility) +- **Action**: Add helper functions to: + - Convert HSL to RGB + - Calculate relative luminance (WCAG formula) + - Calculate contrast ratio between two colors + - Determine optimal text color (black or white) based on background color + - Consider sidebar background's effective color: `calc(0.5 + 0.5*alpha)` means final alpha is `0.5 + 0.5*alpha` + +#### Step 2: Calculate Sidebar Title Color Dynamically +- **File**: `src/gui/src/services/ThemeService.js` +- **Location**: In `reload_()` method +- **Action**: + - Calculate effective sidebar background color (considering alpha blend: `0.5 + 0.5*alpha`) + - Determine if background is light or dark + - Calculate appropriate text color that meets WCAG standards + - Set CSS variable `--window-sidebar-title-color` with calculated color + - Consider fallback to lighter/darker shades of the theme color if pure black/white doesn't work + +#### Step 3: Update CSS to Use Dynamic Color +- **File**: `src/gui/src/css/style.css` +- **Location**: `.window-sidebar-title` rule (line 1221) +- **Action**: + - Replace hardcoded `color: #8f96a3;` with `color: var(--window-sidebar-title-color, #8f96a3);` + - Use fallback color in case CSS variable is not set (for backwards compatibility) + +#### Step 4: Testing Plan +1. Test with various lightness values (0-100%) +2. Test with different hue and saturation values +3. Verify contrast ratio meets WCAG AA standards (4.5:1) for normal text +4. Test edge cases: + - Very light backgrounds (lig > 90%) + - Very dark backgrounds (lig < 10%) + - Medium backgrounds (lig ~ 50-60%) +5. Test with different alpha values +6. Visual regression testing - ensure text is readable at all settings + +#### Step 5: Implementation Details + +**Contrast Calculation Algorithm:** +```javascript +// Calculate relative luminance (WCAG) +function getLuminance(rgb) { + const [r, g, b] = rgb.map(val => { + val = val / 255; + return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +// Calculate contrast ratio +function getContrastRatio(color1, color2) { + const l1 = getLuminance(color1); + const l2 = getLuminance(color2); + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); +} + +// Get optimal text color +function getOptimalTextColor(backgroundColor) { + // Calculate effective background color (considering alpha blend) + // Try black and white, choose one with better contrast + const blackContrast = getContrastRatio([0, 0, 0], backgroundColor); + const whiteContrast = getContrastRatio([255, 255, 255], backgroundColor); + + if (blackContrast >= 4.5 || whiteContrast < 4.5) { + return '#000000'; // or darker shade if needed + } else { + return '#ffffff'; // or lighter shade if needed + } +} +``` + +**ThemeService Integration:** +- In `reload_()` method, after setting other CSS variables: + 1. Calculate effective sidebar background RGB + 2. Determine optimal text color + 3. Set `--window-sidebar-title-color` CSS variable + +### Files to Modify + +1. ✅ `src/gui/src/services/ThemeService.js` + - Add contrast calculation utilities + - Calculate and set `--window-sidebar-title-color` in `reload_()` + +2. ✅ `src/gui/src/css/style.css` + - Update `.window-sidebar-title` to use CSS variable with fallback + +### Alternative Approaches Considered + +1. **CSS-only solution using `mix-blend-mode`**: + - Pros: No JS needed + - Cons: Browser compatibility issues, may affect other elements + +2. **CSS `color-contrast()` function**: + - Pros: Native CSS solution + - Cons: Limited browser support as of 2024 + +3. **Predefined color palettes**: + - Pros: Simple, predictable + - Cons: May not work for all lightness values, less flexible + +### Implementation Priority +- **High**: This is an accessibility issue affecting user experience +- **Impact**: Affects all users who customize theme lightness +- **Risk**: Low - isolated change to theme service and CSS + +## 2025-12-07: Add "Set as Desktop Background" Context Menu Item to Images + +### Feature Description +Add a "Set as Desktop Background" context menu option that appears when users right-click on image files. This option should: +- Only appear for image file types (PNG, JPG, etc.) +- Not appear for folders, text files, or other non-image files +- Not appear for items in trash or trashed items +- Immediately update the desktop background when clicked +- Persist the preference to user settings + +### Current Behavior +- Users can set desktop background via Settings window +- Context menus on image files show standard options (Open, Download, Copy, etc.) +- No quick way to set an image as desktop background directly from file system + +### Expected Behavior +- Context menu on image files includes "Set as Desktop Background" option +- Clicking it immediately applies the image as desktop background +- Preference is saved to user account and persists across sessions + +### Step-by-Step Plan + +#### Step 1: Add Helper Function to Detect Image Files +- **Location**: `src/gui/src/UI/UIItem.js` (or create helper file) +- **Action**: Create or use existing utility to check if a file is an image + - Check if `options.type` starts with `'image/'` + - Alternatively, check file extension if MIME type not available + - Handle common image extensions: jpg, jpeg, png, gif, webp, bmp, svg, etc. + +#### Step 2: Add Helper Function to Get File Read URL +- **Location**: `src/gui/src/UI/UIItem.js` +- **Action**: Create async function to get read_url for a file + - Use `puter.fs.stat()` to get file information + - Extract `read_url` from the response + - Handle errors appropriately + +#### Step 3: Add Helper Function to Set Desktop Background from File +- **Location**: `src/gui/src/UI/UIItem.js` or `src/gui/src/helpers.js` +- **Action**: Create function that: + - Takes file path/uid as parameter + - Gets read_url for the file + - Calls `window.set_desktop_background()` with the URL + - Makes POST request to `/set-desktop-bg` API endpoint to persist + - Handles authentication (similar to UIWindowDesktopBGSettings.js) + - Uses default fit mode 'cover' (consistent with existing behavior) + +#### Step 4: Add Context Menu Item in Single Item Context Menu +- **Location**: `src/gui/src/UI/UIItem.js` - Single item context menu section (around line 964-1384) +- **Action**: + - Add conditional check: only show if file is an image AND not trashed AND not trash item + - Insert menu item after "Share With…" or in appropriate location (likely after "Download" or before "Properties") + - Use appropriate icon (could use existing desktop background or image icon) + - Call helper function when clicked + +#### Step 5: Add Internationalization String +- **Location**: Internationalization files +- **Action**: + - Add translation key for "Set as Desktop Background" + - Use `i18n('set_as_desktop_background')` in menu item + +#### Step 6: Add Icon for Menu Item (if needed) +- **Location**: Check if suitable icon exists in `src/gui/src/images/` +- **Action**: + - Use existing icon (e.g., 'desktop-bg.svg' or similar) + - Or use generic image icon if no specific desktop background icon exists + +### Files That Will Be Modified + +1. **`src/gui/src/UI/UIItem.js`** + - **Purpose**: Add context menu item and helper functions + - **Lines**: ~740-1390 (context menu section) + - **Changes**: + - Add image detection logic + - Add function to get file read_url + - Add function to set desktop background from file + - Add menu item in single-item context menu section + - Conditional rendering based on file type and trash status + +2. **Internationalization Files** (if applicable) + - **Location**: `src/gui/src/i18n/` or similar + - **Purpose**: Add translation for "Set as Desktop Background" + - **Changes**: Add new translation key + +### Functions That Will Be Modified + +1. **Context Menu Handler in `UIItem.js`** + - **Function**: Anonymous function in `$(el_item).bind("contextmenu taphold", ...)` handler + - **Location**: Line ~740-1393 + - **Changes**: + - Add conditional check for image files + - Add menu item to `menu_items` array when conditions are met + - Place menu item in appropriate position (likely after "Download" or in logical grouping) + +2. **New Helper Functions to Add**: + + a. **`is_image_file(options)` or similar** + - **Purpose**: Check if a file is an image type + - **Logic**: + ```javascript + function is_image_file(options) { + if (options.is_dir) return false; + if (options.type && options.type.startsWith('image/')) return true; + // Fallback: check extension + const ext = path.extname(options.name || '').toLowerCase(); + const image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.tiff', '.ico']; + return image_extensions.includes(ext); + } + ``` + + b. **`get_file_read_url(file_path_or_uid)` or similar** + - **Purpose**: Get read_url for a file + - **Logic**: + ```javascript + async function get_file_read_url(file_path_or_uid) { + return new Promise((resolve, reject) => { + puter.fs.stat(file_path_or_uid, { + success: (fsentry) => { + // Get read_url - might need to call puter.fs.sign() or check fsentry structure + // Check how UIWindowDesktopBGSettings.js does it (line 136-138) + resolve(fsentry.read_url); + }, + error: reject + }); + }); + } + ``` + + c. **`set_desktop_background_from_file(file_path, file_uid)` or similar** + - **Purpose**: Set desktop background from file and persist to server + - **Logic**: + ```javascript + async function set_desktop_background_from_file(file_path, file_uid) { + try { + // Get read_url + const fsentry = await new Promise((resolve, reject) => { + puter.fs.stat(file_path, { + success: resolve, + error: reject + }); + }); + + const read_url = fsentry.read_url; + if (!read_url) { + // Fallback: might need to sign the file + // Check how other parts of codebase get read_url + throw new Error('Could not get read URL for file'); + } + + // Set desktop background immediately + window.set_desktop_background({ + url: read_url, + fit: 'cover' + }); + + // Persist to server (similar to UIWindowDesktopBGSettings.js lines 164-186) + await $.ajax({ + url: window.api_origin + "/set-desktop-bg", + type: 'POST', + data: JSON.stringify({ + url: read_url, + fit: 'cover', + color: null + }), + async: true, + contentType: "application/json", + headers: { + "Authorization": "Bearer " + window.auth_token + }, + statusCode: { + 401: function () { + window.logout(); + } + } + }); + } catch (err) { + console.error('Failed to set desktop background:', err); + UIAlert('Failed to set desktop background. Please try again.'); + } + } + ``` + +### Detailed Implementation Notes + +#### Context Menu Item Placement +- **Best Position**: After "Download" and before "Zip" (around line 1185-1189) +- **Alternative**: After "Share With…" (around line 1109) or before "Properties" (around line 1357) +- **Consideration**: Group with other file operations, not with sharing/publishing options + +#### Image Detection Logic +- Primary: Check `options.type` starts with `'image/'` +- Fallback: Check file extension (`.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`, `.svg`, etc.) +- Edge cases: + - Handle files without MIME type + - Handle case-insensitive extensions + - Exclude directories + +#### Trash Item Exclusion +- Check `is_trashed` variable (line 182, 764) +- Check if path starts with `window.trash_path` (line 965) +- Check if item is the trash folder itself: `is_trash` (line 965) + +#### Read URL Acquisition +- Research needed: Check how `UIWindowDesktopBGSettings.js` gets `read_url` from `selected_file.read_url` (line 138) +- May need to call `puter.fs.sign()` or similar API +- Check `open_item.js` or `launch_app.js` for examples of getting read_url + +#### Error Handling +- Handle cases where file cannot be read +- Handle authentication failures +- Handle API errors gracefully +- Show user-friendly error messages + +#### Testing Checklist +1. ✅ Right-click on PNG image → "Set as Desktop Background" appears +2. ✅ Right-click on JPG image → "Set as Desktop Background" appears +3. ✅ Right-click on non-image file → "Set as Desktop Background" does NOT appear +4. ✅ Right-click on folder → "Set as Desktop Background" does NOT appear +5. ✅ Right-click on image in trash → "Set as Desktop Background" does NOT appear +6. ✅ Click "Set as Desktop Background" → Background changes immediately +7. ✅ Background persists after page refresh +8. ✅ Background persists across sessions (after logout/login) +9. ✅ Menu item appears in correct position +10. ✅ Icon displays correctly (if added) +11. ✅ Internationalization works for non-English languages + +### Implementation Priority +- **Medium-High**: User experience enhancement +- **Impact**: Improves convenience for users who want to customize desktop +- **Risk**: Low - isolated change to context menu, reuses existing desktop background functionality +- **Dependencies**: None - uses existing APIs and functions + +## 2025--XX: Auto-Hide Top Toolbar Feature + +### Feature Description +Implement an auto-hide feature for the top toolbar that hides it after 2 seconds of inactivity and shows it again when the mouse moves near the top edge of the screen (within 50px). + +### Current Behavior +- Top toolbar is always visible +- Toolbar occupies screen space continuously (30px height) +- No option to hide or minimize toolbar + +### Expected Behavior +- Toolbar automatically hides after 2 seconds of mouse inactivity +- Toolbar reappears when mouse moves within 50px of top screen edge +- Smooth fade/slide animation for hiding and showing +- Optional setting to toggle auto-hide on/off (default: off for backwards compatibility) +- Maintains full functionality when visible + +### Step-by-Step Implementation Plan + +#### Step 1: Add CSS Classes for Auto-Hide Animation +- **File**: `src/gui/src/css/style.css` +- **Location**: After `.toolbar` class definition (around line 1752) +- **Action**: Add CSS classes for hidden state and transitions + - `.toolbar-auto-hide-hidden` - class to hide toolbar + - CSS transition for smooth animation (opacity and transform) + - Ensure toolbar maintains functionality when transitioning + +#### Step 2: Add User Preference for Auto-Hide +- **File**: `src/gui/src/globals.js` +- **Location**: In `window.user_preferences` defaults (around line 98-102) +- **Action**: Add default preference: `toolbar_auto_hide: false` +- **File**: `src/gui/src/UI/UIDesktop.js` +- **Location**: In user preferences loading section (around line 709-713) +- **Action**: Load `toolbar_auto_hide` preference from KV store + +#### Step 3: Add Auto-Hide Logic to UIDesktop +- **File**: `src/gui/src/UI/UIDesktop.js` +- **Location**: After toolbar HTML insertion (around line 1146) +- **Action**: + - Initialize auto-hide state variables + - Set up mouse move tracking + - Set up inactivity timeout (2 seconds) + - Implement show/hide functions with smooth animations + - Check mouse proximity to top edge (50px threshold) + +#### Step 4: Add Settings UI Toggle (Optional but Recommended) +- **File**: `src/gui/src/UI/Settings/UITabPersonalization.js` or appropriate settings tab +- **Action**: Add checkbox/toggle to enable/disable auto-hide feature +- **Action**: Save preference using `window.mutate_user_preferences()` + +### Files That Will Be Modified + +1. **`src/gui/src/css/style.css`** + - **Purpose**: Add CSS for auto-hide animation + - **Changes**: + - Add `.toolbar-auto-hide-hidden` class with transform/opacity + - Add transition properties for smooth animation + - Ensure z-index and positioning work correctly when hidden + +2. **`src/gui/src/globals.js`** + - **Purpose**: Add default preference value + - **Changes**: Add `toolbar_auto_hide: false` to default preferences + +3. **`src/gui/src/UI/UIDesktop.js`** + - **Purpose**: Implement auto-hide logic + - **Changes**: + - Load `toolbar_auto_hide` preference + - Add mouse tracking event listeners + - Add timeout management for inactivity + - Add show/hide functions + - Conditional logic to only activate if preference is enabled + +4. **`src/gui/src/UI/Settings/UITabPersonalization.js`** (Optional) + - **Purpose**: Add UI toggle for feature + - **Changes**: Add checkbox/toggle control + +5. **`src/gui/src/helpers/update_mouse_position.js`** (May need to check) + - **Purpose**: Verify mouse position tracking is available + - **Action**: Check if `window.mouseY` is already being tracked + +### Functions That Will Be Modified/Created + +1. **New Functions in `UIDesktop.js`**: + + a. **`init_toolbar_auto_hide()`** + - **Purpose**: Initialize auto-hide functionality + - **Logic**: + ```javascript + function init_toolbar_auto_hide() { + if (!window.user_preferences.toolbar_auto_hide) return; + + let hideTimeout; + let isHidden = false; + const toolbar = $('.toolbar'); + const HIDE_DELAY = 2000; // 2 seconds + const SHOW_THRESHOLD = 50; // 50px from top + + function hideToolbar() { + if (!isHidden) { + toolbar.addClass('toolbar-auto-hide-hidden'); + isHidden = true; + } + } + + function showToolbar() { + if (isHidden) { + toolbar.removeClass('toolbar-auto-hide-hidden'); + isHidden = false; + } + clearTimeout(hideTimeout); + hideTimeout = setTimeout(hideToolbar, HIDE_DELAY); + } + + // Track mouse movement + $(document).on('mousemove', function(e) { + if (e.clientY <= SHOW_THRESHOLD) { + showToolbar(); + } else { + // Reset hide timeout if toolbar is visible + if (!isHidden) { + clearTimeout(hideTimeout); + hideTimeout = setTimeout(hideToolbar, HIDE_DELAY); + } + } + }); + + // Initial timeout + hideTimeout = setTimeout(hideToolbar, HIDE_DELAY); + } + ``` + + b. **Toolbar event handlers** + - When toolbar buttons are hovered/clicked, keep toolbar visible + - Reset timeout on toolbar interaction + +### Implementation Details + +#### CSS Animation Approach +- Use `transform: translateY(-100%)` to slide up +- Use `opacity: 0` for fade effect +- Transition duration: ~300ms for smooth animation +- Use `pointer-events: none` when hidden to prevent interaction issues + +#### Mouse Tracking +- Use existing `window.mouseY` if available from `update_mouse_position.js` +- Or use `$(document).mousemove()` event with `e.clientY` +- Check `e.clientY <= 50` to show toolbar + +#### Timeout Management +- Clear existing timeout before setting new one +- Handle edge cases (window focus, mouse leaving window, etc.) + +#### Backwards Compatibility +- Default to `toolbar_auto_hide: false` so existing users are not affected +- Feature only activates if explicitly enabled + +### Testing Checklist +1. ✅ Toolbar remains visible by default (backwards compatibility) +2. ✅ Enable auto-hide in settings (if implemented) +3. ✅ Toolbar hides after 2 seconds of inactivity +4. ✅ Toolbar shows when mouse moves near top edge (within 50px) +5. ✅ Smooth animation when hiding/showing +6. ✅ Toolbar buttons remain functional when visible +7. ✅ Toolbar stays visible when hovering over it +8. ✅ Toolbar stays visible when clicking toolbar buttons +9. ✅ Preference persists across page refresh +10. ✅ Works on different screen sizes +11. ✅ Works when window loses focus and regains focus + +### Implementation Priority +- **Medium**: User experience enhancement +- **Impact**: Improves screen space utilization +- **Risk**: Low - isolated change, backwards compatible (default off) +- **Dependencies**: None - uses existing mouse tracking and preferences system diff --git a/src/gui/src/UI/Settings/UITabAccount.js b/src/gui/src/UI/Settings/UITabAccount.js index 95de65cf62..aafe4f6ac1 100644 --- a/src/gui/src/UI/Settings/UITabAccount.js +++ b/src/gui/src/UI/Settings/UITabAccount.js @@ -37,6 +37,10 @@ export default { h += `
`; h += `
`; h += `
`; + // show remove button only if user has a profile picture + if(window.user?.profile?.picture) { + h += ``; + } h += `
`; // change password button @@ -150,6 +154,24 @@ export default { }); }) + $el_window.find('.remove-profile-picture').on('click', function (e) { + console.log('Removing profile picture...'); + + // Clear the profile picture from user's profile + update_profile(window.user.username, {picture: null}); + + // Update the profile picture display to default avatar + const defaultAvatar = window.icons['profile.svg']; + $el_window.find('.profile-picture').css('background-image', `url('${defaultAvatar}')`); + $('.profile-image').css('background-image', `url('${defaultAvatar}')`); + $('.profile-image').removeClass('profile-image-has-picture'); + + // Show the remove button (if hidden) + $el_window.find('.remove-profile-picture').show(); + + console.log('Profile picture removed successfully'); + }); + $el_window.on('file_opened', async function(e){ let selected_file = Array.isArray(e.detail) ? e.detail[0] : e.detail; // set profile picture diff --git a/src/gui/src/UI/Settings/UITabPersonalization.js b/src/gui/src/UI/Settings/UITabPersonalization.js index b5002f3cb1..f3dcc7bff4 100644 --- a/src/gui/src/UI/Settings/UITabPersonalization.js +++ b/src/gui/src/UI/Settings/UITabPersonalization.js @@ -48,6 +48,15 @@ export default { +
+ +
${i18n('menubar_style')}
@@ -102,6 +111,20 @@ export default { window.change_clock_visible(); + // Load and set toolbar auto-hide preference + const currentValue = window.user_preferences?.toolbar_auto_hide || false; + $el_window.find('.toolbar-auto-hide-toggle').prop('checked', currentValue); + + // Handle toolbar auto-hide toggle change + $el_window.find('.toolbar-auto-hide-toggle').on('change', function(e) { + const isEnabled = $(this).prop('checked'); + window.mutate_user_preferences({ + toolbar_auto_hide: isEnabled + }); + // Reload page to apply changes (since auto-hide is initialized on page load) + window.location.reload(); + }); + puter.kv.get('menubar_style').then(async (val) => { if(val === 'system' || !val){ $el_window.find('#menubar_style_system').prop('checked', true); diff --git a/src/gui/src/UI/UIDesktop.js b/src/gui/src/UI/UIDesktop.js index 6b9d619fb6..97ca11755d 100644 --- a/src/gui/src/UI/UIDesktop.js +++ b/src/gui/src/UI/UIDesktop.js @@ -706,10 +706,14 @@ async function UIDesktop(options){ } // update local user preferences + const toolbar_auto_hide_value = await puter.kv.get('user_preferences.toolbar_auto_hide'); const user_preferences = { show_hidden_files: JSON.parse(await puter.kv.get('user_preferences.show_hidden_files')), language: await puter.kv.get('user_preferences.language'), clock_visible: await puter.kv.get('user_preferences.clock_visible'), + toolbar_auto_hide: toolbar_auto_hide_value !== null && toolbar_auto_hide_value !== undefined + ? JSON.parse(toolbar_auto_hide_value) + : false, }; // update default apps @@ -719,6 +723,52 @@ async function UIDesktop(options){ } window.update_user_preferences(user_preferences); + + // Initialize toolbar auto-hide after preferences are loaded + if (user_preferences.toolbar_auto_hide) { + const toolbar = $('.toolbar'); + let hideTimeout; + const HIDE_DELAY = 2000; // 2 seconds of inactivity + const SHOW_THRESHOLD = 50; // 50px from top to show toolbar + + function hideToolbar() { + toolbar.addClass('toolbar-auto-hide-hidden'); + } + + function showToolbar() { + toolbar.removeClass('toolbar-auto-hide-hidden'); + clearTimeout(hideTimeout); + } + + function resetHideTimeout() { + clearTimeout(hideTimeout); + hideTimeout = setTimeout(hideToolbar, HIDE_DELAY); + } + + // Show toolbar when mouse enters toolbar area + toolbar.on('mouseenter', function() { + showToolbar(); + }); + + // Track ALL mouse movement for inactivity detection + $(document).on('mousemove', function(e) { + // Always show toolbar when mouse is near top edge + if (e.clientY <= SHOW_THRESHOLD) { + showToolbar(); + } else { + // Reset timeout on any mouse movement (inactivity detection) + resetHideTimeout(); + } + }); + + // Reset timeout on mouse click anywhere + $(document).on('mousedown click', function() { + resetHideTimeout(); + }); + + // Initial hide after 2 seconds of inactivity + hideTimeout = setTimeout(hideToolbar, HIDE_DELAY); + } }); // Append to diff --git a/src/gui/src/UI/UIItem.js b/src/gui/src/UI/UIItem.js index 3c2f6e294f..20e447eaa4 100644 --- a/src/gui/src/UI/UIItem.js +++ b/src/gui/src/UI/UIItem.js @@ -30,6 +30,88 @@ import truncate_filename from '../helpers/truncate_filename.js'; import launch_app from "../helpers/launch_app.js" import open_item from "../helpers/open_item.js" +/** + * Checks if a file is an image based on MIME type or file extension + * @param {Object} options - File options object + * @param {boolean} options.is_dir - Whether the item is a directory + * @param {string} options.type - MIME type of the file + * @param {string} options.name - Name of the file + * @returns {boolean} True if the file is an image + */ +function is_image_file(options) { + // Directories are not images + if (options.is_dir) return false; + + // Check MIME type first (primary method) + if (options.type && options.type.startsWith('image/')) { + return true; + } + + // Fallback: check file extension + const ext = path.extname(options.name || '').toLowerCase(); + const image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.tiff', '.tif', '.ico']; + return image_extensions.includes(ext); +} + +/** + * Sets the desktop background from a file and persists it to the server + * @param {string} file_path - Path to the file + * @param {string} file_uid - UID of the file + * @returns {Promise} + */ +async function set_desktop_background_from_file(file_path, file_uid) { + try { + // Get file signature with read_url + // Use host_app_uid if available, otherwise undefined (backend handles this) + const app_uid = window.host_app_uid || undefined; + const file_signature = await puter.fs.sign(app_uid, { + uid: file_uid, + action: 'read' + }); + + // Handle both single item and array response + const signature = Array.isArray(file_signature?.items) + ? file_signature.items[0] + : (file_signature?.items || file_signature); + + const read_url = signature?.read_url; + + if (!read_url) { + throw new Error('Could not get read URL for file'); + } + + // Set desktop background immediately + window.set_desktop_background({ + url: read_url, + fit: 'cover' + }); + + // Persist to server + await $.ajax({ + url: window.api_origin + "/set-desktop-bg", + type: 'POST', + data: JSON.stringify({ + url: read_url, + fit: 'cover', + color: null + }), + async: true, + contentType: "application/json", + headers: { + "Authorization": "Bearer " + window.auth_token + }, + statusCode: { + 401: function () { + window.logout(); + } + } + }); + } catch (err) { + console.error('Failed to set desktop background:', err); + UIAlert('Failed to set desktop background. Please try again.'); + } +} + function UIItem(options){ const matching_appendto_count = $(options.appendTo).length; if(matching_appendto_count > 1){ @@ -1184,6 +1266,19 @@ function UIItem(options){ }); } // ------------------------------------------- + // Set as Desktop Background + // ------------------------------------------- + if(!is_trash && !is_trashed && !options.is_dir && is_image_file(options)){ + menu_items.push({ + html: i18n('set_as_desktop_background'), + onClick: async function(){ + const file_uid = $(el_item).attr('data-uid'); + const file_path = $(el_item).attr('data-path'); + await set_desktop_background_from_file(file_path, file_uid); + } + }); + } + // ------------------------------------------- // Zip // ------------------------------------------- if(!is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.zip')){ diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js index 99bce7db0c..793805e66a 100644 --- a/src/gui/src/UI/UIWindow.js +++ b/src/gui/src/UI/UIWindow.js @@ -1609,7 +1609,9 @@ async function UIWindow(options) { // -------------------------------------------------------- // Close button // -------------------------------------------------------- - $(`#window-${win_id} > .window-head > .window-close-btn`).click(function () { + $(`#window-${win_id} > .window-head > .window-close-btn`).click(function (event) { + event.stopPropagation(); + event.preventDefault(); $(el_window).close({ shrink_to_target: options.on_close_shrink_to_target }); @@ -1618,7 +1620,9 @@ async function UIWindow(options) { // -------------------------------------------------------- // Minimize button // -------------------------------------------------------- - $(`#window-${win_id} > .window-head > .window-minimize-btn`).click(function () { + $(`#window-${win_id} > .window-head > .window-minimize-btn`).click(function (event) { + event.stopPropagation(); + event.preventDefault(); $(el_window).hideWindow(); }) diff --git a/src/gui/src/css/style.css b/src/gui/src/css/style.css index 4c7d1637e2..7aba4406ba 100644 --- a/src/gui/src/css/style.css +++ b/src/gui/src/css/style.css @@ -1218,7 +1218,7 @@ span.header-sort-icon img { margin: 0; font-weight: bold; font-size: 13px; - color: #8f96a3; + color: var(--window-sidebar-title-color, #8f96a3); text-shadow: 1px 1px rgb(247 247 247 / 15%); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -1243,7 +1243,7 @@ span.header-sort-icon img { margin-top: 2px; padding: 4px; border-radius: 3px; - color: #444444; + color: var(--window-sidebar-item-color, #444444); font-size: 13px; cursor: pointer; transition: 0.15s background-color; @@ -1748,7 +1748,14 @@ label { justify-content: flex-end; align-content: center; flex-wrap: wrap; - padding-right: 10px + padding-right: 10px; + transition: transform 0.3s ease-out, opacity 0.3s ease-out; +} + +.toolbar-auto-hide-hidden { + transform: translateY(-100%); + opacity: 0; + pointer-events: none; } .show-desktop-btn { diff --git a/src/gui/src/globals.js b/src/gui/src/globals.js index a6c0ad105b..3c93ff95a2 100644 --- a/src/gui/src/globals.js +++ b/src/gui/src/globals.js @@ -99,6 +99,7 @@ if (window.user_preferences === null) { show_hidden_files: false, language: navigator.language.split("-")[0] || navigator.userLanguage || 'en', clock_visible: 'auto', + toolbar_auto_hide: false, } } diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js index 613120afc0..47bbc13811 100644 --- a/src/gui/src/i18n/translations/en.js +++ b/src/gui/src/i18n/translations/en.js @@ -238,6 +238,7 @@ const en = { refresh: 'Refresh', release_address_confirmation: `Are you sure you want to release this address?`, remove_from_taskbar:'Remove from Taskbar', + remove_profile_picture: 'Remove Profile Picture', rename: 'Rename', repeat: 'Repeat', replace: 'Replace', @@ -269,8 +270,11 @@ const en = { settings: "Settings", set_new_password: "Set New Password", share: "Share", + toolbar_auto_hide: "Auto-hide Toolbar", + toolbar_auto_hide_description: "Automatically hide the toolbar after 2 seconds of inactivity. Move mouse near top edge to show it again.", share_to: "Share to", share_with: "Share with:", + set_as_desktop_background: "Set as Desktop Background", shortcut_to: "Shortcut to", show_all_windows: "Show All Windows", show_hidden: 'Show hidden', diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js index 59a8e4ddd3..88973eae15 100644 --- a/src/gui/src/initgui.js +++ b/src/gui/src/initgui.js @@ -1225,6 +1225,13 @@ window.initgui = async function(options){ if($(e.target).hasClass('taskbar') || $(e.target).closest('.taskbar').length > 0) return; + // if close or minimize button is clicked, don't activate window + // This prevents the window from coming to foreground when closing/minimizing + if($(e.target).hasClass('window-close-btn') || $(e.target).closest('.window-close-btn').length > 0) + return; + if($(e.target).hasClass('window-minimize-btn') || $(e.target).closest('.window-minimize-btn').length > 0) + return; + // if mouse is clicked on a window, activate it if(window.mouseover_window !== undefined){ // if popover clicked on, don't activate window. This is because if an app diff --git a/src/gui/src/services/ThemeService.js b/src/gui/src/services/ThemeService.js index b49d41e684..4d2e04ee31 100644 --- a/src/gui/src/services/ThemeService.js +++ b/src/gui/src/services/ThemeService.js @@ -32,6 +32,114 @@ const default_values = { light_text: false, }; +/** + * Convert HSL color to RGB array [r, g, b] with values 0-255 + * @param {number} h - Hue (0-360) + * @param {number} s - Saturation (0-100) + * @param {number} l - Lightness (0-100) + * @returns {Array} RGB array [r, g, b] with values 0-255 + */ +function hslToRgb(h, s, l) { + h = h / 360; + s = s / 100; + l = l / 100; + + let r, g, b; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return [ + Math.round(r * 255), + Math.round(g * 255), + Math.round(b * 255) + ]; +} + +/** + * Calculate relative luminance using WCAG formula + * @param {Array} rgb - RGB array [r, g, b] with values 0-255 + * @returns {number} Relative luminance (0-1) + */ +function getLuminance(rgb) { + const [r, g, b] = rgb.map(val => { + val = val / 255; + return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +/** + * Calculate contrast ratio between two colors (WCAG formula) + * @param {Array} rgb1 - First RGB array [r, g, b] with values 0-255 + * @param {Array} rgb2 - Second RGB array [r, g, b] with values 0-255 + * @returns {number} Contrast ratio (1-21) + */ +function getContrastRatio(rgb1, rgb2) { + const l1 = getLuminance(rgb1); + const l2 = getLuminance(rgb2); + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); +} + +/** + * Blend color with white background (simulating alpha blend on white) + * Sidebar uses: calc(0.5 + 0.5*alpha), so effective alpha is 0.5 + 0.5*alpha + * When alpha=1, effective is 1.0; when alpha=0, effective is 0.5 + * @param {Array} rgb - RGB array [r, g, b] with values 0-255 + * @param {number} alpha - Alpha value (0-1) + * @returns {Array} Blended RGB array [r, g, b] with values 0-255 + */ +function blendWithWhite(rgb, alpha) { + const effectiveAlpha = 0.5 + 0.5 * alpha; + const [r, g, b] = rgb; + return [ + Math.round(r * effectiveAlpha + 255 * (1 - effectiveAlpha)), + Math.round(g * effectiveAlpha + 255 * (1 - effectiveAlpha)), + Math.round(b * effectiveAlpha + 255 * (1 - effectiveAlpha)) + ]; +} + +/** + * Determine optimal text color (black or white) based on background color + * Returns the color that provides better contrast meeting WCAG AA standards (4.5:1) + * @param {Array} backgroundColor - RGB array [r, g, b] with values 0-255 + * @returns {string} Hex color string ('#000000' for black, '#ffffff' for white) + */ +function getOptimalTextColor(backgroundColor) { + const black = [0, 0, 0]; + const white = [255, 255, 255]; + + const blackContrast = getContrastRatio(black, backgroundColor); + const whiteContrast = getContrastRatio(white, backgroundColor); + + // Choose the color with better contrast + // If both meet 4.5:1, prefer the one with higher contrast + // If neither meets 4.5:1, still choose the better one + if (blackContrast >= whiteContrast) { + return '#000000'; + } else { + return '#ffffff'; + } +} + export class ThemeService extends Service { #broadcastService; @@ -86,8 +194,9 @@ export class ThemeService extends Service { ...this.state, ...data.colors, }; - this.reload_(); } + // Always reload to set initial CSS variables + this.reload_(); } reset () { @@ -122,6 +231,21 @@ export class ThemeService extends Service { this.root.style.setProperty('--primary-alpha', s.alpha); this.root.style.setProperty('--primary-color', s.light_text ? 'white' : '#373e44'); + // Calculate optimal sidebar colors based on effective background color + // Sidebar background uses: calc(0.5 + 0.5*alpha), so we need to blend with white + try { + const sidebarRgb = hslToRgb(s.hue, s.sat, s.lig); + const blendedSidebarRgb = blendWithWhite(sidebarRgb, s.alpha); + const sidebarTextColor = getOptimalTextColor(blendedSidebarRgb); + + // Set CSS variables for both sidebar title and sidebar items + this.root.style.setProperty('--window-sidebar-title-color', sidebarTextColor); + this.root.style.setProperty('--window-sidebar-item-color', sidebarTextColor); + console.log('[ThemeService] Sidebar text colors set to:', sidebarTextColor); + } catch (error) { + console.error('[ThemeService] Error calculating sidebar colors:', error); + } + // TODO: Should we debounce this to reduce traffic? this.#broadcastService.sendBroadcast('themeChanged', { palette: { diff --git a/src/puter-js/src/modules/KV.js b/src/puter-js/src/modules/KV.js index d49def1bb4..254f0f2a1d 100644 --- a/src/puter-js/src/modules/KV.js +++ b/src/puter-js/src/modules/KV.js @@ -10,6 +10,7 @@ const gui_cache_keys = [ 'user_preferences.show_hidden_files', 'user_preferences.language', 'user_preferences.clock_visible', + 'user_preferences.toolbar_auto_hide', 'has_seen_welcome_window', ];