mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-24 06:05:40 +00:00
Files tab for dashboard view (#2331)
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
Docker Image CI / build-and-push-image (push) Has been cancelled
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
release-please / release-please (push) Has been cancelled
test / test-backend (24.x) (push) Has been cancelled
test / API tests (node env, api-test) (24.x) (push) Has been cancelled
test / puterjs (node env, vitest) (24.x) (push) Has been cancelled
* Dashboard - Added files tab with basic folder and files listing.
* clearing out some linter errors
* Removed Public folder for now
* Added "modified" column
Subfolder are listed before the files
* Dashboard - files tab: context menu basics
* Dashboard - Files tab: subfolders can be opened. Items can be opened from more-menu or by double clicking on them
* Dashboard - Files tab: use UIContentMenu instead of custom menu for "more menu"
* Dashboard - Files tab: Delete implemented, Trash folder added
* refactored context menu rendering
* Implemented UIItem context menu functionalities
Added path
* Rename files and folders
* Rename fix
* tweaks and fixes
* WIP: Multiple selection, drag and drop, trash folder improvements
* clean
* Files handling improvements
* empty trash fix
* grid view
* grid view improvements
* update icons
context menu for multiple items shows file counts such as: Delete (9)
* Path navifation buttons
* better way to get user's directories
* Added icons for various file types
* Use puter.kv instead of window.user_preferences
* sorting and column resizing
* documentation of the functions
* Style update. Dashboard colors now as CSS variables
* refactored restoring from trash and emptying trash with proper callbacks & refreshing
* select documents folder by default
style tweaking
* Drag to select files and folders
* dark mode
* style tweaks
* removed some dead code..
* Keyboard shortcuts (copied from UIWindow - untested & in progress)
* Add drag-and-drop file upload support to Dashboard
Implement native file drag-and-drop from local filesystem to Dashboard:
- Add dragster handlers for main content area, sidebar folders, and folder rows
- Create uploadFiles() method with progress modal and directory refresh
- Add visual feedback CSS for drop zones (highlight on hover)
- Fix subfolder drop detection to prevent duplicate uploads
- Add callback support to unzipItem/untarItem for directory refresh
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style tweaks
* Fix cut/paste functionality in Dashboard
The cut operation was storing clipboard items as plain path strings, but
window.move_clipboard_items() looks for .item DOM elements which don't
exist in Dashboard (it uses .row elements). Added a Dashboard-specific
moveClipboardItems() method that calls puter.fs.move() directly with UIDs,
and updated cut to store objects with path and uid like copy does.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add mobile phone layout optimizations for Dashboard
- Add responsive CSS for screens ≤480px with two-row file list layout
- Wrap item-size and item-modified in item-metadata div for mobile grid
- Hide directories sidebar on mobile (use breadcrumbs for navigation)
- Reorganize header with breadcrumbs on top row, nav buttons below
- Add single-tap to open folders on touch devices
- Fix context menu "Open" action in Dashboard by adding onOpen callback
- Hide column headers on mobile since metadata is shown per-item
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add CSS variables to Dashboard bento elements for dark mode support
Replace hardcoded color values with CSS custom properties throughout
the bento card components to enable proper dark mode theming. Adds new
variables for icon shadows, fancy headers, gradients, and usage bars
with appropriate values for both light and dark color schemes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add spacebar image preview feature to Dashboard Files
Pressing spacebar on a selected image file now shows a full-size preview
popover centered over the files container. Features include:
- Toggle behavior: press spacebar again to close
- Auto-switch: selecting another file updates the preview
- Click outside to dismiss
- Works in both grid and list view
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add floating action bar for multi-file selection in Dashboard Files
When 2+ files are selected, a floating action bar appears above the footer
with quick access buttons for common operations:
- Download, Cut, Copy, Delete for normal files
- Restore and Delete Permanently for items in Trash
The bar slides up with a smooth animation and disappears when clicking
the background to deselect. Includes mobile-responsive styling with
icon-only buttons on smaller screens.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add mobile select mode for multi-file selection in Dashboard Files
On mobile devices, users can now tap a select button in the path actions
to enter select mode. In this mode, tapping files toggles their selection
instead of opening them, with checkboxes appearing next to each item.
The floating action bar appears with 1+ selected items (vs 2+ on desktop)
and includes a Done button to exit select mode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Center empty directory message in Dashboard Files
Replace row-based layout with centered flex positioning for the
"No files in this directory" message, improving visual presentation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fixed issue where clicking on files background created blue dot (drag rectangle selection bug)
* Image preview size tweak
* Add URL-based routing to Dashboard with hash fragments
Enable shareable/bookmarkable URLs for Dashboard tabs and file paths.
The URL hash reflects current location (e.g., #files/username/Documents,
#usage, #account). Browser back/forward navigation now works for folder
navigation, and direct URL entry loads the correct tab and path.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fixing draggable styles
* drag clone fixes
* Add smart column resizing and filename truncation to Dashboard Files
- Add middle-truncation for filenames preserving extensions (e.g., "longfile…name.txt")
- Double-click column resize handles to auto-fit content width
- Reset saved column widths to defaults if they don't fit smaller screens
- Apply truncation to both list and grid views
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Separated dashboard styles into dashboard.css and loading it dynamically only when navigating to /dashboard
* Files and folders can be dropped over path items in order to move them. This is useful when you want to move items into parent folders.
* Use custom context menu component for item-more menu on mobile devices
* Created callback system to undo_last_action so that dashboard file list would always refresh after undo operation
* gray update
* hovering folder while dragging items opens the folder
* Fixed issue with hover-dropping
* Fixed chained drag-over-folder-and-hover system
* dark mode tweak
* - [x] Right clicking on background of an empty folder doesn't trigger context menu
- [x] Right clicking on path should keep path item active
- [x] Sidebar folder don't have context menu
- [x] Drag item should be opaque so I can see through it
- [x] Transition animation issue when hovering path action buttons
- [x] Double clicking on column resizer: add padding
- [x] Load dashboard.css before loading the page content
- [x] Mobile: opening menu, clicking outside menu should close it
- [x] Mobile: on smaller screen there should be still two columns
- [x] Mobile: file name truncation in grid view doesn't work
* Added missing badges
* WIP has_worker
* display worker icon on the .js files which are workers
* Display worker badge on worker files, hovering badge shows worker URL.
Worker URL added to file properties modal.
* disable "publish as website" from context menu for folders that are already published
* Loading spinner for Files
* tweaks and fixes
* Files clean loading spinner
* Files: worker URL in the properties modal is clickable link
* use custom tooltip to display worker URL
* refactored renderDirectory
* Fix Sidebar folder highlighting
* add Beta badge on Files tab
* Implemeting socket based item updating
* Put newly added items into the position where they would land when list is sorted
* restore from trash refresh fix
* Revert "Revert "Adds worker badge to .js files which have been registered as workers. Hovering badge shows popover with worker URL. File properties modal also shows clickable worker URL. (#2462)""
This reverts commit c2750f2ca1.
* worker in stat
* update puterjs and frontend code to support returnWorkers in stat
* Better way of handling worker info
* Fixed issue where right clicking on background opened two instances of a context menu
* Fixes issue where while renaming a file, double clicking on word opened the file
* fixes issue where while dragging multiple items the top-most item was transparent resulting blurry mess
* when opening a folder, update view instantly
* Improved drag-to-select items
* Added feature where typing selects matching file items
Fixed issue where keyboard listener for file list was active when context menu was open
* Fixed issues with File -> New in context menu
Fixes issue where files-selection-actions menu didn't disappear after deleting items
* Improved drag to select behavior
Improved drag items
Improved worker badge handling
* Cancel zone for dragging items (when spring-loading subfolders)
* context menu's submenu tweak
* Fixed issue where resizing window didn't work (when using "Open in New Window" from context menu)
* Disabled buttons on .files-selection-actions while drag-to-select is happening
* Fixes issue where creating new items using right-clicking re-rendered the directory view
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: ProgrammerIn-wonderland <3838shah@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
ProgrammerIn-wonderland
parent
4279cd92df
commit
9d598f7965
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* ContextMenuModal
|
||||
*
|
||||
* A mobile-friendly context menu modal that appears positioned over a target element.
|
||||
* Adapted from voice-recorder project for Puter dashboard use.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Detects if device is touch-primary (mobile/tablet)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTouchPrimaryDevice () {
|
||||
return (
|
||||
window.matchMedia('(pointer: coarse)').matches &&
|
||||
window.matchMedia('(hover: none)').matches
|
||||
);
|
||||
}
|
||||
|
||||
export default class ContextMenuModal {
|
||||
constructor (options = {}) {
|
||||
this.onClose = options.onClose || (() => {
|
||||
});
|
||||
this.backdrop = null;
|
||||
this.modal = null;
|
||||
this.menuItems = null;
|
||||
this.ignoreInteractions = false;
|
||||
|
||||
// Event handler references for cleanup
|
||||
this.backdropClickHandler = null;
|
||||
this.escapeKeyHandler = null;
|
||||
this.itemClickHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the modal positioned over a specific element
|
||||
* @param {Array} menuItems - Array of menu item objects or '-' for separator
|
||||
* @param {DOMRect} targetRect - Bounding rectangle of the tapped item
|
||||
*/
|
||||
show (menuItems, targetRect) {
|
||||
if ( this.backdrop ) return; // Already showing
|
||||
|
||||
this.menuItems = menuItems;
|
||||
|
||||
// Create backdrop
|
||||
this.backdrop = document.createElement('div');
|
||||
this.backdrop.className = 'context-menu-modal-backdrop';
|
||||
|
||||
// Create modal dialog
|
||||
this.modal = document.createElement('div');
|
||||
this.modal.className = 'context-menu-modal-dialog';
|
||||
|
||||
// Build modal content
|
||||
this.modal.innerHTML = `
|
||||
<div class="context-menu-items">
|
||||
${this.renderMenuItems(menuItems)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add modal to backdrop
|
||||
this.backdrop.appendChild(this.modal);
|
||||
|
||||
// Add to DOM
|
||||
document.body.appendChild(this.backdrop);
|
||||
|
||||
// Position modal after adding to DOM (so we can measure it)
|
||||
this.positionModal(targetRect);
|
||||
|
||||
// Setup event listeners
|
||||
this.setupEventListeners();
|
||||
|
||||
// Ignore interactions briefly to prevent accidental selection on touch devices
|
||||
this.ignoreInteractions = true;
|
||||
setTimeout(() => {
|
||||
this.ignoreInteractions = false;
|
||||
}, 100);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
this.backdrop.classList.add('show');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the modal over the target element
|
||||
* @param {DOMRect} targetRect - Bounding rectangle of the target
|
||||
*/
|
||||
positionModal (targetRect) {
|
||||
const isMobile = isTouchPrimaryDevice();
|
||||
const modalHeight = this.modal.offsetHeight;
|
||||
const modalWidth = this.modal.offsetWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const viewportWidth = window.innerWidth;
|
||||
const margin = 20; // Minimum margin from viewport edges
|
||||
|
||||
// Default: align with item left and top
|
||||
let top = targetRect.top;
|
||||
let left = targetRect.left;
|
||||
|
||||
// Use target width as minimum, but allow modal to be wider if needed
|
||||
const width = Math.max(targetRect.width, modalWidth);
|
||||
|
||||
// Horizontal positioning - center over item if possible
|
||||
const itemCenter = targetRect.left + (targetRect.width / 2);
|
||||
const modalHalfWidth = width / 2;
|
||||
|
||||
if ( itemCenter - modalHalfWidth >= margin &&
|
||||
itemCenter + modalHalfWidth <= viewportWidth - margin ) {
|
||||
left = itemCenter - modalHalfWidth;
|
||||
} else {
|
||||
// Align with item left, but ensure within viewport
|
||||
left = 20; //Math.max(margin, Math.min(left, viewportWidth - width - margin));
|
||||
}
|
||||
|
||||
// Vertical positioning - ensure modal stays within viewport
|
||||
if ( top + modalHeight > viewportHeight - margin ) {
|
||||
// Would go off bottom, shift up
|
||||
top = Math.max(margin, viewportHeight - modalHeight - margin);
|
||||
}
|
||||
|
||||
if ( top < margin ) {
|
||||
top = margin;
|
||||
}
|
||||
|
||||
// Apply positioning
|
||||
this.modal.style.top = `${top}px`;
|
||||
this.modal.style.left = isMobile ? `${left}px` : '300px';
|
||||
this.modal.style.width = isMobile ? '90%' : 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render menu items as HTML
|
||||
* Supports both Puter format (html/onClick) and voice-recorder format (label/action)
|
||||
* @param {Array} menuItems - Array of menu items
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
renderMenuItems (menuItems) {
|
||||
return menuItems.map((item, index) => {
|
||||
// Handle separators
|
||||
if ( item === '-' || item.is_divider ) {
|
||||
return '<div class="context-menu-separator"></div>';
|
||||
}
|
||||
|
||||
// Get label - support both formats
|
||||
const label = item.label || item.html || '';
|
||||
|
||||
// Check for delete/danger styling
|
||||
const isDelete = label.toLowerCase().includes('delete');
|
||||
const deleteClass = isDelete ? 'context-menu-item--delete' : '';
|
||||
|
||||
// Get icon - support both formats (HTML string or base64)
|
||||
let iconHtml = '';
|
||||
if ( item.icon ) {
|
||||
if ( item.icon.startsWith('data:') ) {
|
||||
// Base64 image
|
||||
iconHtml = `<img src="${item.icon}" alt="" />`;
|
||||
} else {
|
||||
// HTML string (SVG)
|
||||
iconHtml = item.icon;
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<button class="context-menu-item ${deleteClass}" data-index="${index}">
|
||||
<div class="context-menu-item-icon">
|
||||
${iconHtml}
|
||||
</div>
|
||||
<span class="context-menu-item-label">${label}</span>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners
|
||||
*/
|
||||
setupEventListeners () {
|
||||
// Close on backdrop click
|
||||
this.backdropClickHandler = (e) => {
|
||||
if ( e.target === this.backdrop ) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
this.backdrop.addEventListener('click', this.backdropClickHandler);
|
||||
|
||||
// Handle menu item clicks
|
||||
this.itemClickHandler = (e) => {
|
||||
if ( this.ignoreInteractions ) return;
|
||||
|
||||
const itemBtn = e.target.closest('.context-menu-item');
|
||||
if ( ! itemBtn ) return;
|
||||
|
||||
const index = parseInt(itemBtn.dataset.index, 10);
|
||||
const menuItem = this.menuItems[index];
|
||||
|
||||
if ( menuItem && menuItem !== '-' && !menuItem.is_divider ) {
|
||||
// Support both action formats
|
||||
const handler = menuItem.action || menuItem.onClick;
|
||||
if ( handler ) {
|
||||
this.close();
|
||||
// Execute action after close animation starts
|
||||
setTimeout(() => {
|
||||
handler();
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.modal.addEventListener('click', this.itemClickHandler);
|
||||
|
||||
// Handle Escape key
|
||||
this.escapeKeyHandler = (e) => {
|
||||
if ( e.key === 'Escape' ) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this.escapeKeyHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal with animation
|
||||
*/
|
||||
close () {
|
||||
if ( ! this.backdrop ) return;
|
||||
|
||||
// Remove event listeners
|
||||
if ( this.backdropClickHandler ) {
|
||||
this.backdrop.removeEventListener('click', this.backdropClickHandler);
|
||||
}
|
||||
if ( this.itemClickHandler && this.modal ) {
|
||||
this.modal.removeEventListener('click', this.itemClickHandler);
|
||||
}
|
||||
if ( this.escapeKeyHandler ) {
|
||||
document.removeEventListener('keydown', this.escapeKeyHandler);
|
||||
}
|
||||
|
||||
// Trigger closing animation
|
||||
this.backdrop.classList.remove('show');
|
||||
|
||||
// Remove from DOM after animation
|
||||
setTimeout(() => {
|
||||
if ( this.backdrop && this.backdrop.parentNode ) {
|
||||
this.backdrop.parentNode.removeChild(this.backdrop);
|
||||
}
|
||||
this.backdrop = null;
|
||||
this.modal = null;
|
||||
this.menuItems = null;
|
||||
this.onClose();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
export { isTouchPrimaryDevice };
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ function getTimeGreeting () {
|
||||
|
||||
function buildRecentAppsHTML () {
|
||||
let h = '';
|
||||
|
||||
|
||||
if ( window.launch_apps?.recent?.length > 0 ) {
|
||||
h += '<div class="bento-recent-apps-grid">';
|
||||
|
||||
@@ -42,10 +42,10 @@ function buildRecentAppsHTML () {
|
||||
}
|
||||
|
||||
h += `<div class="bento-recent-app" data-app-name="${html_encode(app_info.name)}" data-target-link="${html_encode(app_info.target_link)}">`;
|
||||
// Icon
|
||||
h += `<img class="bento-recent-app-icon" src="${html_encode(app_info.icon || window.icons['app.svg'])}">`;
|
||||
// Title
|
||||
h += `<span class="bento-recent-app-title">${html_encode(app_info.title)}</span>`;
|
||||
// Icon
|
||||
h += `<img class="bento-recent-app-icon" src="${html_encode(app_info.icon || window.icons['app.svg'])}">`;
|
||||
// Title
|
||||
h += `<span class="bento-recent-app-title">${html_encode(app_info.title)}</span>`;
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
@@ -59,57 +59,57 @@ function buildRecentAppsHTML () {
|
||||
h += '<span>Apps you use will appear here</span>';
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
|
||||
return h;
|
||||
}
|
||||
|
||||
function buildUsageHTML () {
|
||||
let h = '';
|
||||
h += '<div class="bento-usage-grid">';
|
||||
|
||||
|
||||
// Your Plan section
|
||||
h += '<div class="bento-usage-section bento-usage-card bento-plan-section">';
|
||||
h += '<a href="#" class="bento-usage-card-header bento-plan-header">';
|
||||
h += `<h3>${i18n('your_plan')}</h3>`;
|
||||
h += '<span class="bento-usage-card-arrow">›</span>';
|
||||
h += '</a>';
|
||||
h += '<div class="bento-usage-card-info bento-plan-info">';
|
||||
h += '<span class="bento-usage-card-used bento-plan-name">--</span>';
|
||||
h += '<span class="bento-usage-card-details bento-plan-details"><span class="bento-plan-badge"></span></span>';
|
||||
h += '</div>';
|
||||
h += '<a href="#" class="bento-plan-upgrade" style="display: none;">Upgrade →</a>';
|
||||
h += '<a href="#" class="bento-usage-card-header bento-plan-header">';
|
||||
h += `<h3>${i18n('your_plan')}</h3>`;
|
||||
h += '<span class="bento-usage-card-arrow">›</span>';
|
||||
h += '</a>';
|
||||
h += '<div class="bento-usage-card-info bento-plan-info">';
|
||||
h += '<span class="bento-usage-card-used bento-plan-name">--</span>';
|
||||
h += '<span class="bento-usage-card-details bento-plan-details"><span class="bento-plan-badge"></span></span>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<a href="#" class="bento-plan-upgrade" style="display: none;">Upgrade →</a>';
|
||||
h += '</div>';
|
||||
|
||||
// Storage section
|
||||
h += '<div class="bento-usage-section bento-usage-card">';
|
||||
h += '<a href="#" class="bento-usage-card-header" data-target-tab="usage">';
|
||||
h += `<h3>Your ${i18n('Storage')}</h3>`;
|
||||
h += '<span class="bento-usage-card-arrow">›</span>';
|
||||
h += '</a>';
|
||||
h += '<div class="bento-usage-card-bar-wrapper">';
|
||||
h += '<div class="bento-usage-card-bar bento-storage-bar"></div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-usage-card-info">';
|
||||
h += '<span class="bento-usage-card-used bento-storage-used">-- Used</span>';
|
||||
h += '<span class="bento-usage-card-details"><span class="bento-storage-percent">--%</span> of <span class="bento-storage-capacity">--</span></span>';
|
||||
h += '</div>';
|
||||
h += '<a href="#" class="bento-usage-card-header" data-target-tab="usage">';
|
||||
h += `<h3>Your ${i18n('Storage')}</h3>`;
|
||||
h += '<span class="bento-usage-card-arrow">›</span>';
|
||||
h += '</a>';
|
||||
h += '<div class="bento-usage-card-bar-wrapper">';
|
||||
h += '<div class="bento-usage-card-bar bento-storage-bar"></div>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<div class="bento-usage-card-info">';
|
||||
h += '<span class="bento-usage-card-used bento-storage-used">-- Used</span>';
|
||||
h += '<span class="bento-usage-card-details"><span class="bento-storage-percent">--%</span> of <span class="bento-storage-capacity">--</span></span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
// Resources section
|
||||
h += '<div class="bento-usage-section bento-usage-card">';
|
||||
h += '<a href="#" class="bento-usage-card-header" data-target-tab="usage">';
|
||||
h += `<h3>Your ${i18n('Resources')}</h3>`;
|
||||
h += '<span class="bento-usage-card-arrow">›</span>';
|
||||
h += '</a>';
|
||||
h += '<div class="bento-usage-card-bar-wrapper">';
|
||||
h += '<div class="bento-usage-card-bar bento-resources-bar"></div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-usage-card-info">';
|
||||
h += '<span class="bento-usage-card-used bento-resources-used">-- Used</span>';
|
||||
h += '<span class="bento-usage-card-details"><span class="bento-resources-percent">--%</span> of <span class="bento-resources-capacity">--</span></span>';
|
||||
h += '</div>';
|
||||
h += '<a href="#" class="bento-usage-card-header" data-target-tab="usage">';
|
||||
h += `<h3>Your ${i18n('Resources')}</h3>`;
|
||||
h += '<span class="bento-usage-card-arrow">›</span>';
|
||||
h += '</a>';
|
||||
h += '<div class="bento-usage-card-bar-wrapper">';
|
||||
h += '<div class="bento-usage-card-bar bento-resources-bar"></div>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<div class="bento-usage-card-info">';
|
||||
h += '<span class="bento-usage-card-used bento-resources-used">-- Used</span>';
|
||||
h += '<span class="bento-usage-card-details"><span class="bento-resources-percent">--%</span> of <span class="bento-resources-capacity">--</span></span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
@@ -117,74 +117,74 @@ function buildUsageHTML () {
|
||||
const TabHome = {
|
||||
id: 'home',
|
||||
label: 'Home',
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
|
||||
|
||||
html () {
|
||||
const username = window.user?.username || 'User';
|
||||
const greeting = getTimeGreeting();
|
||||
const profilePicture = window.user?.profile?.picture || window.icons['profile.svg'];
|
||||
|
||||
|
||||
let h = '';
|
||||
h += '<div class="bento-container">';
|
||||
|
||||
|
||||
// Welcome card (square)
|
||||
h += '<div class="bento-card bento-welcome">';
|
||||
h += '<div class="bento-welcome-inner">';
|
||||
h += '<div class="bento-welcome-pattern"></div>';
|
||||
h += `<div class="bento-welcome-content">`;
|
||||
h += `<div class="bento-welcome-avatar profile-pic" style="background-image: url(${html_encode(profilePicture)})"></div>`;
|
||||
h += `<span class="bento-greeting">${greeting},</span>`;
|
||||
h += `<h1 class="bento-username username">${html_encode(username)}</h1>`;
|
||||
h += '<p class="bento-tagline">Your personal cloud computer</p>';
|
||||
// Show warning if account is temporary/unsaved
|
||||
if ( window.user?.is_temp ) {
|
||||
h += '<button class="bento-save-account-warning">';
|
||||
h += `<svg style="width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="#ffbb00"></path></g></svg>`;
|
||||
h += `<span>${i18n('save_session')}</span>`;
|
||||
h += '</button>';
|
||||
}
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-welcome-inner">';
|
||||
h += '<div class="bento-welcome-pattern"></div>';
|
||||
h += '<div class="bento-welcome-content">';
|
||||
h += `<div class="bento-welcome-avatar profile-pic" style="background-image: url(${html_encode(profilePicture)})"></div>`;
|
||||
h += `<span class="bento-greeting">${greeting},</span>`;
|
||||
h += `<h1 class="bento-username username">${html_encode(username)}</h1>`;
|
||||
h += '<p class="bento-tagline">Your personal cloud computer</p>';
|
||||
// Show warning if account is temporary/unsaved
|
||||
if ( window.user?.is_temp ) {
|
||||
h += '<button class="bento-save-account-warning">';
|
||||
h += '<svg style="width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="var(--dashboard-warning-icon)"></path></g></svg>';
|
||||
h += `<span>${i18n('save_session')}</span>`;
|
||||
h += '</button>';
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
// Recent apps card (rectangle)
|
||||
h += '<div class="bento-card bento-recent">';
|
||||
h += '<div class="bento-card-fancy-header">';
|
||||
h += '<div class="bento-card-fancy-icon bento-card-fancy-icon-apps">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-card-fancy-text">';
|
||||
h += '<h2>Apps</h2>';
|
||||
h += '<span class="bento-card-fancy-subtitle">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
||||
h += 'Recently used';
|
||||
h += '</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-recent-apps-container">';
|
||||
h += buildRecentAppsHTML();
|
||||
h += '</div>';
|
||||
h += '<div class="bento-card-fancy-header">';
|
||||
h += '<div class="bento-card-fancy-icon bento-card-fancy-icon-apps">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<div class="bento-card-fancy-text">';
|
||||
h += '<h2>Apps</h2>';
|
||||
h += '<span class="bento-card-fancy-subtitle">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
||||
h += 'Recently used';
|
||||
h += '</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-recent-apps-container">';
|
||||
h += buildRecentAppsHTML();
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
// Usage card (spans full width on second row)
|
||||
h += '<div class="bento-card bento-usage">';
|
||||
h += '<div class="bento-card-fancy-header">';
|
||||
h += '<div class="bento-card-fancy-icon bento-card-fancy-icon-usage">';
|
||||
h += '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4M3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707M2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10m9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5m.754-4.246a.39.39 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.39.39 0 0 0-.029-.518z"/><path fill-rule="evenodd" d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A8 8 0 0 1 0 10m8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-card-fancy-text">';
|
||||
h += `<h2>${i18n('usage')}</h2>`;
|
||||
h += '<span class="bento-card-fancy-subtitle">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>';
|
||||
h += 'Monthly overview';
|
||||
h += '</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-usage-container">';
|
||||
h += buildUsageHTML();
|
||||
h += '</div>';
|
||||
h += '<div class="bento-card-fancy-header">';
|
||||
h += '<div class="bento-card-fancy-icon bento-card-fancy-icon-usage">';
|
||||
h += '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4M3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707M2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10m9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5m.754-4.246a.39.39 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.39.39 0 0 0-.029-.518z"/><path fill-rule="evenodd" d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A8 8 0 0 1 0 10m8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3"/></svg>';
|
||||
h += '</div>';
|
||||
|
||||
h += '<div class="bento-card-fancy-text">';
|
||||
h += `<h2>${i18n('usage')}</h2>`;
|
||||
h += '<span class="bento-card-fancy-subtitle">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>';
|
||||
h += 'Monthly overview';
|
||||
h += '</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += '<div class="bento-usage-container">';
|
||||
h += buildUsageHTML();
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
h += '</div>';
|
||||
return h;
|
||||
},
|
||||
@@ -238,7 +238,7 @@ const TabHome = {
|
||||
},
|
||||
|
||||
async loadRecentApps ($el_window) {
|
||||
if ( !window.launch_apps?.recent?.length ) {
|
||||
if ( ! window.launch_apps?.recent?.length ) {
|
||||
try {
|
||||
window.launch_apps = await $.ajax({
|
||||
url: `${window.api_origin}/get-launch-apps?icon_size=64`,
|
||||
@@ -260,12 +260,12 @@ const TabHome = {
|
||||
// Load plan data
|
||||
try {
|
||||
const hasSubscription = window.user?.subscription?.active;
|
||||
const planName = hasSubscription
|
||||
const planName = hasSubscription
|
||||
? (window.user?.subscription?.plan_name || i18n('billing.offering.pro'))
|
||||
: i18n('billing.offering.free');
|
||||
|
||||
|
||||
$el_window.find('.bento-plan-name').text(planName);
|
||||
|
||||
|
||||
if ( hasSubscription ) {
|
||||
$el_window.find('.bento-plan-badge').text('Active subscription').addClass('active');
|
||||
$el_window.find('.bento-plan-upgrade').hide();
|
||||
@@ -320,4 +320,3 @@ const TabHome = {
|
||||
};
|
||||
|
||||
export default TabHome;
|
||||
|
||||
|
||||
@@ -26,68 +26,68 @@ import UIWindowManageSessions from '../UIWindowManageSessions.js';
|
||||
const TabSecurity = {
|
||||
id: 'security',
|
||||
label: i18n('security'),
|
||||
icon: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`,
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
|
||||
|
||||
html () {
|
||||
let h = '';
|
||||
let user = window.user;
|
||||
|
||||
h += '<div class="dashboard-tab-content">';
|
||||
|
||||
|
||||
// Section header
|
||||
h += '<div class="dashboard-section-header">';
|
||||
h += `<h2>${i18n('security')}</h2>`;
|
||||
h += '<p>Manage your security settings and sessions</p>';
|
||||
h += `<h2>${i18n('security')}</h2>`;
|
||||
h += '<p>Manage your security settings and sessions</p>';
|
||||
h += '</div>';
|
||||
|
||||
// Security settings cards
|
||||
h += '<div class="dashboard-settings-grid">';
|
||||
|
||||
// Password card (only for non-temp users)
|
||||
if ( !user.is_temp ) {
|
||||
if ( ! user.is_temp ) {
|
||||
h += '<div class="dashboard-card dashboard-settings-card">';
|
||||
h += '<div class="dashboard-settings-card-content">';
|
||||
h += '<div class="dashboard-settings-card-icon">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="dashboard-settings-card-info">';
|
||||
h += `<strong>${i18n('password')}</strong>`;
|
||||
h += '<span>••••••••</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += `<button class="button change-password">${i18n('change_password')}</button>`;
|
||||
h += '<div class="dashboard-settings-card-content">';
|
||||
h += '<div class="dashboard-settings-card-icon">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="dashboard-settings-card-info">';
|
||||
h += `<strong>${i18n('password')}</strong>`;
|
||||
h += '<span>••••••••</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += `<button class="button change-password">${i18n('change_password')}</button>`;
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
// Sessions card
|
||||
h += '<div class="dashboard-card dashboard-settings-card">';
|
||||
h += '<div class="dashboard-settings-card-content">';
|
||||
h += '<div class="dashboard-settings-card-icon">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="dashboard-settings-card-info">';
|
||||
h += `<strong>${i18n('sessions')}</strong>`;
|
||||
h += '<span>Manage active sessions</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += `<button class="button manage-sessions">${i18n('manage_sessions')}</button>`;
|
||||
h += '<div class="dashboard-settings-card-content">';
|
||||
h += '<div class="dashboard-settings-card-icon">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="dashboard-settings-card-info">';
|
||||
h += `<strong>${i18n('sessions')}</strong>`;
|
||||
h += '<span>Manage active sessions</span>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += `<button class="button manage-sessions">${i18n('manage_sessions')}</button>`;
|
||||
h += '</div>';
|
||||
|
||||
// 2FA card (only for non-temp users with confirmed email)
|
||||
if ( !user.is_temp && user.email_confirmed ) {
|
||||
const twoFaStatusClass = user.otp ? 'dashboard-settings-card-success' : 'dashboard-settings-card-warning';
|
||||
h += `<div class="dashboard-card dashboard-settings-card dashboard-settings-card-2fa ${twoFaStatusClass}">`;
|
||||
h += '<div class="dashboard-settings-card-content">';
|
||||
h += '<div class="dashboard-settings-card-icon">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="dashboard-settings-card-info">';
|
||||
h += `<strong>${i18n('two_factor')}</strong>`;
|
||||
h += `<span class="user-otp-state">${i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled')}</span>`;
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += `<button class="button enable-2fa" style="${user.otp ? 'display:none;' : ''}">${i18n('enable_2fa')}</button>`;
|
||||
h += `<button class="button disable-2fa" style="${user.otp ? '' : 'display:none;'}">${i18n('disable_2fa')}</button>`;
|
||||
h += '<div class="dashboard-settings-card-content">';
|
||||
h += '<div class="dashboard-settings-card-icon">';
|
||||
h += '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>';
|
||||
h += '</div>';
|
||||
h += '<div class="dashboard-settings-card-info">';
|
||||
h += `<strong>${i18n('two_factor')}</strong>`;
|
||||
h += `<span class="user-otp-state">${i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled')}</span>`;
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += `<button class="button enable-2fa" style="${user.otp ? 'display:none;' : ''}">${i18n('enable_2fa')}</button>`;
|
||||
h += `<button class="button disable-2fa" style="${user.otp ? '' : 'display:none;'}">${i18n('disable_2fa')}</button>`;
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ const TabSecurity = {
|
||||
h += '</div>';
|
||||
h += '<div style="display: flex; flex-direction: column; gap: 10pt;">';
|
||||
h += '<input type="password" class="password-entry" />';
|
||||
h += '<span class="error-message" style="color: red; display: none;"></span>';
|
||||
h += '<span class="error-message" style="color: var(--dashboard-error-text); display: none;"></span>';
|
||||
h += '</div>';
|
||||
h += '<div style="display: flex; gap: 5pt;">';
|
||||
h += `<button class="button confirm-disable-2fa">${i18n('disable_2fa')}</button>`;
|
||||
@@ -200,7 +200,7 @@ const TabSecurity = {
|
||||
is_resizable: false,
|
||||
body_css: {
|
||||
width: 'initial',
|
||||
'background-color': 'rgb(245 247 249)',
|
||||
'background-color': 'var(--dashboard-input-background)',
|
||||
'backdrop-filter': 'blur(3px)',
|
||||
padding: '20px',
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable no-invalid-this */
|
||||
/* eslint-disable @stylistic/indent */
|
||||
/**
|
||||
* Copyright (C) 2024-present Puter Technologies Inc.
|
||||
*
|
||||
@@ -53,13 +55,22 @@ import TabSecurity from './TabSecurity.js';
|
||||
const builtinTabs = [
|
||||
TabHome,
|
||||
// TabApps,
|
||||
// TabFiles,
|
||||
TabFiles,
|
||||
TabUsage,
|
||||
TabAccount,
|
||||
TabSecurity,
|
||||
];
|
||||
|
||||
// Dynamically load dashboard CSS if not already loaded
|
||||
if ( ! document.querySelector('link[href*="dashboard.css"]') ) {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/css/dashboard.css';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
|
||||
async function UIDashboard (options) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
options = options ?? {};
|
||||
|
||||
// Create mutable tabs array from built-in tabs
|
||||
@@ -71,12 +82,12 @@ async function UIDashboard (options) {
|
||||
let h = '';
|
||||
|
||||
h += '<div class="dashboard">';
|
||||
|
||||
|
||||
// Mobile sidebar toggle
|
||||
h += '<button class="dashboard-sidebar-toggle">';
|
||||
h += '<span></span><span></span><span></span>';
|
||||
h += '</button>';
|
||||
|
||||
|
||||
// Sidebar
|
||||
h += '<div class="dashboard-sidebar hide-scrollbar">';
|
||||
// Navigation items container
|
||||
@@ -84,20 +95,21 @@ async function UIDashboard (options) {
|
||||
for ( let i = 0; i < tabs.length; i++ ) {
|
||||
const tab = tabs[i];
|
||||
const isActive = i === 0 ? ' active' : '';
|
||||
h += `<div class="dashboard-sidebar-item${isActive}" data-section="${tab.id}">`;
|
||||
const isBeta = tab.label === 'Files';
|
||||
h += `<div class="dashboard-sidebar-item${isActive} ${isBeta ? 'beta' : ''}" data-section="${tab.id}">`;
|
||||
h += tab.icon;
|
||||
h += tab.label;
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
|
||||
|
||||
// User options button at bottom
|
||||
h += '<div class="dashboard-user-options hide-scrollbar">';
|
||||
h += `<div class="dashboard-user-btn hide-scrollbar">`;
|
||||
h += '<div class="dashboard-user-btn hide-scrollbar">';
|
||||
h += `<div class="dashboard-user-avatar profile-pic" style="background-image: url(${window.user?.profile?.picture || window.icons['profile.svg']})"></div>`;
|
||||
h += `<span class="dashboard-user-name">${html_encode(window.user?.username || 'User')}</span>`;
|
||||
h += `<svg class="dashboard-user-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>`;
|
||||
h += `</div>`;
|
||||
h += `<span class="dashboard-user-name">${window.html_encode(window.user?.username || 'User')}</span>`;
|
||||
h += '<svg class="dashboard-user-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
@@ -133,6 +145,11 @@ async function UIDashboard (options) {
|
||||
|
||||
const $el_window = $(el_window);
|
||||
|
||||
// Set initial file path BEFORE tabs are initialized (so TabFiles.init() can use it)
|
||||
if ( window.dashboard_initial_route?.tab === 'files' && window.dashboard_initial_route?.path ) {
|
||||
window.dashboard_initial_file_path = window.dashboard_initial_route.path;
|
||||
}
|
||||
|
||||
// Initialize all tabs
|
||||
for ( const tab of tabs ) {
|
||||
if ( tab.init ) {
|
||||
@@ -143,15 +160,225 @@ async function UIDashboard (options) {
|
||||
// Dispatch 'dashboard-ready' event for extensions
|
||||
window.dispatchEvent(new CustomEvent('dashboard-ready', { detail: { window: $el_window } }));
|
||||
|
||||
// =========================================================================
|
||||
// Socket initialization
|
||||
// In dashboard mode, UIDesktop is never loaded, so we create the socket here.
|
||||
// This runs inside the function (not at module level) to ensure window.gui_origin
|
||||
// and window.auth_token are already set.
|
||||
// =========================================================================
|
||||
window.socket = io(`${window.gui_origin}/`, {
|
||||
auth: {
|
||||
auth_token: window.auth_token,
|
||||
},
|
||||
});
|
||||
|
||||
window.socket.on('error', (error) => {
|
||||
console.error('Dashboard Socket Error:', error);
|
||||
});
|
||||
|
||||
window.socket.on('connect', function () {
|
||||
window.socket.emit('puter_is_actually_open');
|
||||
});
|
||||
|
||||
window.socket.on('reconnect', function () {
|
||||
console.log('Dashboard Socket: Reconnected', window.socket.id);
|
||||
});
|
||||
|
||||
window.socket.on('disconnect', () => {
|
||||
console.log('Dashboard Socket: Disconnected');
|
||||
});
|
||||
|
||||
window.socket.on('reconnect_attempt', (attempt) => {
|
||||
console.log('Dashboard Socket: Reconnection Attempt', attempt);
|
||||
});
|
||||
|
||||
window.socket.on('reconnect_error', (error) => {
|
||||
console.log('Dashboard Socket: Reconnection Error', error);
|
||||
});
|
||||
|
||||
window.socket.on('reconnect_failed', () => {
|
||||
console.log('Dashboard Socket: Reconnection Failed');
|
||||
});
|
||||
|
||||
// Upload/download progress tracking
|
||||
window.socket.on('upload.progress', (msg) => {
|
||||
if ( window.progress_tracker[msg.operation_id] ) {
|
||||
window.progress_tracker[msg.operation_id].cloud_uploaded += msg.loaded_diff;
|
||||
if ( window.progress_tracker[msg.operation_id][msg.item_upload_id] ) {
|
||||
window.progress_tracker[msg.operation_id][msg.item_upload_id].cloud_uploaded = msg.loaded;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.socket.on('download.progress', (msg) => {
|
||||
if ( window.progress_tracker[msg.operation_id] ) {
|
||||
if ( window.progress_tracker[msg.operation_id][msg.item_upload_id] ) {
|
||||
window.progress_tracker[msg.operation_id][msg.item_upload_id].downloaded = msg.loaded;
|
||||
window.progress_tracker[msg.operation_id][msg.item_upload_id].total = msg.total;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Trash status updates
|
||||
window.socket.on('trash.is_empty', async (msg) => {
|
||||
// Update sidebar Trash icon
|
||||
const trashIcon = msg.is_empty ? window.icons['trash.svg'] : window.icons['trash-full.svg'];
|
||||
$('.directories [data-folder=\'Trash\'] img').attr('src', trashIcon);
|
||||
|
||||
// If currently viewing trash and it's empty, clear the file list
|
||||
const dashboard = window.dashboard_object;
|
||||
if ( msg.is_empty && dashboard && dashboard.currentPath === window.trash_path ) {
|
||||
$('.files-tab .files').empty();
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// Item event handlers
|
||||
// Incremental DOM updates using UIDashboardFileItem for item creation and
|
||||
// direct jQuery manipulation for removals/updates. Mirrors UIDesktop's
|
||||
// approach but adapted for Dashboard's list-view structure.
|
||||
// =========================================================================
|
||||
|
||||
window.socket.on('item.moved', async (resp) => {
|
||||
if ( resp.original_client_socket_id === window.socket.id ) return;
|
||||
|
||||
// Fade out old item from view
|
||||
$(`.item[data-uid='${resp.uid}']`).fadeOut(150, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
// Create new item at destination if user is viewing that directory
|
||||
if ( window.UIDashboardFileItem ) {
|
||||
window.UIDashboardFileItem(resp);
|
||||
}
|
||||
});
|
||||
|
||||
window.socket.on('item.removed', async (item) => {
|
||||
if ( item.original_client_socket_id === window.socket.id ) return;
|
||||
if ( item.descendants_only ) return;
|
||||
|
||||
$(`.item[data-path='${html_encode(item.path)}']`).fadeOut(150, function () {
|
||||
$(this).remove();
|
||||
});
|
||||
});
|
||||
|
||||
window.socket.on('item.renamed', async (item) => {
|
||||
if ( item.original_client_socket_id === window.socket.id ) return;
|
||||
|
||||
const $el = $(`.item[data-uid='${item.uid}']`);
|
||||
if ( $el.length === 0 ) return;
|
||||
|
||||
// Update data attributes
|
||||
$el.attr('data-name', html_encode(item.name));
|
||||
$el.attr('data-path', html_encode(item.path));
|
||||
|
||||
// Update displayed name
|
||||
$el.find('.item-name').text(item.name);
|
||||
$el.find('.item-name-editor').val(item.name);
|
||||
});
|
||||
|
||||
window.socket.on('item.updated', async (item) => {
|
||||
if ( item.original_client_socket_id === window.socket.id ) return;
|
||||
|
||||
const $el = $(`.item[data-uid='${item.uid}']`);
|
||||
if ( $el.length === 0 ) return;
|
||||
|
||||
// Update data attributes
|
||||
$el.attr('data-name', html_encode(item.name));
|
||||
$el.attr('data-path', html_encode(item.path));
|
||||
$el.attr('data-size', item.size);
|
||||
$el.attr('data-modified', item.modified);
|
||||
$el.attr('data-type', html_encode(item.type));
|
||||
|
||||
// Update displayed name
|
||||
$el.find('.item-name').text(item.name);
|
||||
$el.find('.item-name-editor').val(item.name);
|
||||
});
|
||||
|
||||
window.socket.on('item.added', async (item) => {
|
||||
if ( _.isEmpty(item) ) return;
|
||||
if ( item.original_client_socket_id === window.socket.id ) return;
|
||||
|
||||
if ( window.UIDashboardFileItem ) {
|
||||
window.UIDashboardFileItem(item);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply initial route from URL - activate the correct tab
|
||||
if ( window.dashboard_initial_route ) {
|
||||
const route = window.dashboard_initial_route;
|
||||
|
||||
// Activate the correct tab if not home
|
||||
if ( route.tab && route.tab !== 'home' ) {
|
||||
const tabId = route.tab;
|
||||
const $targetTab = $el_window.find(`.dashboard-sidebar-item[data-section="${tabId}"]`);
|
||||
|
||||
// Only switch if the tab exists
|
||||
if ( $targetTab.length > 0 ) {
|
||||
$el_window.find('.dashboard-sidebar-item').removeClass('active');
|
||||
$targetTab.addClass('active');
|
||||
$el_window.find('.dashboard-section').removeClass('active');
|
||||
$el_window.find(`.dashboard-section[data-section="${tabId}"]`).addClass('active');
|
||||
|
||||
document.querySelector('.dashboard-content').setAttribute('class', 'dashboard-content');
|
||||
document.querySelector('.dashboard-content').classList.add(tabId);
|
||||
|
||||
// Call onActivate if exists
|
||||
const tab = tabs.find(t => t.id === tabId);
|
||||
if ( tab?.onActivate ) {
|
||||
tab.onActivate($el_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser back/forward navigation
|
||||
// This handler is called for both hashchange (manual hash changes) and popstate (back/forward)
|
||||
const handleRouteChange = () => {
|
||||
const route = window.parseDashboardRoute();
|
||||
const tab = route.tab;
|
||||
const filePath = route.path;
|
||||
|
||||
// Switch to correct tab
|
||||
const $targetTab = $el_window.find(`.dashboard-sidebar-item[data-section="${tab}"]`);
|
||||
if ( tab === 'home' ) {
|
||||
// Home tab
|
||||
$el_window.find('.dashboard-sidebar-item').removeClass('active');
|
||||
$el_window.find('.dashboard-sidebar-item').first().addClass('active');
|
||||
$el_window.find('.dashboard-section').removeClass('active');
|
||||
$el_window.find('.dashboard-section').first().addClass('active');
|
||||
document.querySelector('.dashboard-content').setAttribute('class', 'dashboard-content');
|
||||
} else if ( $targetTab.length > 0 ) {
|
||||
$el_window.find('.dashboard-sidebar-item').removeClass('active');
|
||||
$targetTab.addClass('active');
|
||||
$el_window.find('.dashboard-section').removeClass('active');
|
||||
$el_window.find(`.dashboard-section[data-section="${tab}"]`).addClass('active');
|
||||
document.querySelector('.dashboard-content').setAttribute('class', 'dashboard-content');
|
||||
document.querySelector('.dashboard-content').classList.add(tab);
|
||||
}
|
||||
|
||||
// If files tab with path, navigate without adding to history
|
||||
if ( tab === 'files' && filePath ) {
|
||||
const filesTab = tabs.find(t => t.id === 'files');
|
||||
if ( filesTab?.renderDirectory ) {
|
||||
filesTab.renderDirectory(filePath, { skipUrlUpdate: true, skipNavHistory: true });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for both hashchange and popstate to handle all navigation scenarios
|
||||
window.addEventListener('hashchange', handleRouteChange);
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
|
||||
// Sidebar item click handler
|
||||
$el_window.on('click', '.dashboard-sidebar-item', function () {
|
||||
const $this = $(this);
|
||||
const section = $this.attr('data-section');
|
||||
|
||||
|
||||
// Update active sidebar item
|
||||
$el_window.find('.dashboard-sidebar-item').removeClass('active');
|
||||
$this.addClass('active');
|
||||
|
||||
|
||||
// Update active content section
|
||||
$el_window.find('.dashboard-section').removeClass('active');
|
||||
$el_window.find(`.dashboard-section[data-section="${section}"]`).addClass('active');
|
||||
@@ -162,6 +389,16 @@ async function UIDashboard (options) {
|
||||
tab.onActivate($el_window);
|
||||
}
|
||||
|
||||
document.querySelector('.dashboard-content').setAttribute('class', 'dashboard-content');
|
||||
document.querySelector('.dashboard-content').classList.add(section);
|
||||
|
||||
// Update hash to reflect current tab
|
||||
// Note: Files tab updates its own hash with full path via onActivate, so skip it here
|
||||
if ( section !== 'files' ) {
|
||||
const newHash = section === 'home' ? '' : section;
|
||||
history.replaceState(null, '', newHash ? `#${newHash}` : window.location.pathname);
|
||||
}
|
||||
|
||||
// Close sidebar on mobile after selection
|
||||
$el_window.find('.dashboard-sidebar').removeClass('open');
|
||||
$el_window.find('.dashboard-sidebar-toggle').removeClass('open');
|
||||
@@ -173,14 +410,24 @@ async function UIDashboard (options) {
|
||||
$el_window.find('.dashboard-sidebar').toggleClass('open');
|
||||
});
|
||||
|
||||
// Close sidebar when clicking outside
|
||||
$el_window.on('mousedown touchstart', function (e) {
|
||||
if ( !$(e.target).closest('.dashboard-sidebar').length
|
||||
&& !$(e.target).closest('.dashboard-sidebar-toggle').length
|
||||
&& $el_window.find('.dashboard-sidebar').hasClass('open') ) {
|
||||
$el_window.find('.dashboard-sidebar').removeClass('open');
|
||||
$el_window.find('.dashboard-sidebar-toggle').removeClass('open');
|
||||
}
|
||||
});
|
||||
|
||||
// User options button click handler
|
||||
$el_window.on('click', '.dashboard-user-btn', function (e) {
|
||||
$el_window.on('click', '.dashboard-user-btn', function () {
|
||||
const $btn = $(this);
|
||||
const $chevron = $btn.find('.dashboard-user-chevron');
|
||||
const pos = this.getBoundingClientRect();
|
||||
|
||||
|
||||
// Don't open if already open
|
||||
if ($('.context-menu[data-id="dashboard-user-menu"]').length > 0) {
|
||||
if ( $('.context-menu[data-id="dashboard-user-menu"]').length > 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -190,10 +437,10 @@ async function UIDashboard (options) {
|
||||
let items = [];
|
||||
|
||||
// Save Session (if temp user)
|
||||
if (window.user.is_temp) {
|
||||
if ( window.user.is_temp ) {
|
||||
items.push({
|
||||
html: i18n('save_session'),
|
||||
icon: '<svg style="margin-bottom: -4px; width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="#ffbb00"/></svg>',
|
||||
icon: '<svg style="margin-bottom: -4px; width: 16px; height: 16px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="M45.521,39.04L27.527,5.134c-1.021-1.948-3.427-2.699-5.375-1.679-.717,.376-1.303,.961-1.679,1.679L2.479,39.04c-.676,1.264-.635,2.791,.108,4.017,.716,1.207,2.017,1.946,3.42,1.943H41.993c1.403,.003,2.704-.736,3.42-1.943,.743-1.226,.784-2.753,.108-4.017ZM23.032,15h1.937c.565,0,1.017,.467,1,1.031l-.438,14c-.017,.54-.459,.969-1,.969h-1.062c-.54,0-.983-.429-1-.969l-.438-14c-.018-.564,.435-1.031,1-1.031Zm.968,25c-1.657,0-3-1.343-3-3s1.343-3,3-3,3,1.343,3,3-1.343,3-3,3Z" fill="var(--dashboard-warning-icon)"/></svg>',
|
||||
onClick: async function () {
|
||||
UIWindowSaveAccount({
|
||||
send_confirmation_code: false,
|
||||
@@ -212,7 +459,7 @@ async function UIDashboard (options) {
|
||||
}
|
||||
|
||||
// Logged in users
|
||||
if (window.logged_in_users.length > 0) {
|
||||
if ( window.logged_in_users.length > 0 ) {
|
||||
let users_arr = window.logged_in_users;
|
||||
|
||||
// bring logged in user's item to top
|
||||
@@ -226,7 +473,7 @@ async function UIDashboard (options) {
|
||||
html: l_user.username,
|
||||
icon: l_user.username === window.user.username ? '✓' : '',
|
||||
onClick: async function () {
|
||||
if (l_user.username === window.user.username) {
|
||||
if ( l_user.username === window.user.username ) {
|
||||
return;
|
||||
}
|
||||
window.update_auth_data(l_user.auth_token, l_user);
|
||||
@@ -288,7 +535,7 @@ async function UIDashboard (options) {
|
||||
html: i18n('log_out'),
|
||||
onClick: async function () {
|
||||
// Check for open windows
|
||||
if ($('.window-app').length > 0) {
|
||||
if ( $('.window-app').length > 0 ) {
|
||||
const alert_resp = await UIAlert({
|
||||
message: `<p>${i18n('confirm_open_apps_log_out')}</p>`,
|
||||
buttons: [
|
||||
@@ -302,7 +549,7 @@ async function UIDashboard (options) {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (alert_resp === 'close_and_log_out') {
|
||||
if ( alert_resp === 'close_and_log_out' ) {
|
||||
window.logout();
|
||||
}
|
||||
} else {
|
||||
@@ -315,15 +562,15 @@ async function UIDashboard (options) {
|
||||
UIContextMenu({
|
||||
id: 'dashboard-user-menu',
|
||||
parent_element: $btn[0],
|
||||
position: {
|
||||
position: {
|
||||
top: pos.top - 8,
|
||||
left: pos.left
|
||||
left: pos.left,
|
||||
},
|
||||
items: menuItems,
|
||||
onClose: () => {
|
||||
// Rotate chevron back to point downwards
|
||||
$chevron.removeClass('open');
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ const sendSelectionToAIApp = async ($elements) => {
|
||||
}, '*');
|
||||
};
|
||||
|
||||
function UIItem (options) {
|
||||
async function UIItem (options) {
|
||||
const matching_appendto_count = $(options.appendTo).length;
|
||||
if ( matching_appendto_count > 1 ) {
|
||||
$(options.appendTo).each(function () {
|
||||
@@ -138,7 +138,16 @@ function UIItem (options) {
|
||||
options.shortcut_to_path = options.shortcut_to_path ?? '';
|
||||
options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1);
|
||||
options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false);
|
||||
const is_shared_with_me = (options.path && options.path !== `/${window.user.username}` && !options.path.startsWith(`/${window.user.username}/`));
|
||||
const is_shared_with_me = (options.path !== `/${window.user.username}` && !options.path.startsWith(`/${window.user.username}/`));
|
||||
let worker_url;
|
||||
let is_worker;
|
||||
if ( ! options.is_dir ) {
|
||||
const stats = await puter.fs.stat({ path: options.path, returnWorkers: true });
|
||||
is_worker = stats.workers !== undefined && stats.workers.length > 0;
|
||||
if ( is_worker ) {
|
||||
worker_url = stats.workers[0].address;
|
||||
}
|
||||
}
|
||||
|
||||
let website_url = window.determine_website_url(options.path);
|
||||
|
||||
@@ -165,6 +174,8 @@ function UIItem (options) {
|
||||
data-website_url = "${website_url ? html_encode(website_url) : ''}"
|
||||
data-immutable="${options.immutable}"
|
||||
data-is_shortcut = "${options.is_shortcut}"
|
||||
data-is_worker = "${is_worker !== undefined ? 1 : 0}"
|
||||
data-worker_url = "${is_worker !== undefined ? worker_url : 0}"
|
||||
data-shortcut_to = "${html_encode(options.shortcut_to)}"
|
||||
data-shortcut_to_path = "${html_encode(options.shortcut_to_path)}"
|
||||
data-sortable = "${options.sortable ?? 'true'}"
|
||||
@@ -240,7 +251,12 @@ function UIItem (options) {
|
||||
data-item-id="${item_id}"
|
||||
title="${i18n('item_shortcut')}"
|
||||
>`;
|
||||
|
||||
// worker badge
|
||||
h += `<img class="item-badge item-is-worker long-hover"
|
||||
style="background-color: #ffffff; padding: 2px; ${is_worker ? 'display:block;' : ''}"
|
||||
src="${html_encode(window.icons['worker.svg'])}"
|
||||
data-item-id="${item_id}"
|
||||
>`;
|
||||
h += '</div>';
|
||||
|
||||
// divider
|
||||
@@ -1880,6 +1896,50 @@ $(document).on('click', '.website-badge-popover-link', function (e) {
|
||||
$(e.target).closest('.popover').remove();
|
||||
});
|
||||
|
||||
$(document).on('long-hover', '.item-is-worker', function (e) {
|
||||
const worker_url = e.target.parentNode.parentNode.getAttribute('data-worker_url');
|
||||
var box = e.target.getBoundingClientRect();
|
||||
|
||||
var body = document.body;
|
||||
var docEl = document.documentElement;
|
||||
|
||||
var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
|
||||
var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
|
||||
|
||||
var clientTop = docEl.clientTop || body.clientTop || 0;
|
||||
var clientLeft = docEl.clientLeft || body.clientLeft || 0;
|
||||
|
||||
var top = box.top + scrollTop - clientTop;
|
||||
var left = box.left + scrollLeft - clientLeft;
|
||||
|
||||
if ( worker_url ) {
|
||||
let h = '<div class="allow-user-select worker-badge-popover-content">';
|
||||
h += `<div class="worker-badge-popover-title">${i18n('worker')}</div>`;
|
||||
h += `
|
||||
<a class="worker-badge-popover-link" href="${worker_url}" style="font-size:13px;" target="_blank">${worker_url.replace('https://', '')}</a>
|
||||
<br>`;
|
||||
h += '</div>';
|
||||
|
||||
// close other worker popovers
|
||||
$('.worker-badge-popover-content').closest('.popover').remove();
|
||||
|
||||
// show a UIPopover with the worker URL
|
||||
UIPopover({
|
||||
target: e.target,
|
||||
content: h,
|
||||
snapToElement: e.target,
|
||||
parent_element: e.target,
|
||||
top: top - 30,
|
||||
left: left + 20,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.worker-badge-popover-link', function (e) {
|
||||
// remove the parent popover
|
||||
$(e.target).closest('.popover').remove();
|
||||
});
|
||||
|
||||
// removes item(s)
|
||||
$.fn.removeItems = async function (options) {
|
||||
options = options || {};
|
||||
|
||||
@@ -42,6 +42,7 @@ async function UIWindowItemProperties (item_name, item_path, item_uid, left, top
|
||||
h += `<tr><td class="item-prop-label">${i18n('modified')}</td><td class="item-prop-val item-prop-val-modified"></td></tr>`;
|
||||
h += `<tr><td class="item-prop-label">${i18n('created')}</td><td class="item-prop-val item-prop-val-created"></td></tr>`;
|
||||
h += `<tr><td class="item-prop-label">${i18n('versions')}</td><td class="item-prop-val item-prop-val-versions"></td></tr>`;
|
||||
h += `<tr><td class="item-prop-label">${i18n('worker')}</td><td class="item-prop-val item-prop-val-worker">`;
|
||||
h += `<tr><td class="item-prop-label">${i18n('associated_websites')}</td><td class="item-prop-val item-prop-val-websites">`;
|
||||
h += '</td></tr>';
|
||||
h += `<tr><td class="item-prop-label">${i18n('access_granted_to')}</td><td class="item-prop-val item-prop-val-permissions"></td></tr>`;
|
||||
@@ -109,7 +110,7 @@ async function UIWindowItemProperties (item_name, item_path, item_uid, left, top
|
||||
returnVersions: true,
|
||||
returnSize: true,
|
||||
consistency: 'eventual',
|
||||
success: function (fsentry) {
|
||||
success: async function (fsentry) {
|
||||
// hide versions tab if item is a directory
|
||||
if ( fsentry.is_dir ) {
|
||||
$(el_window).find('[data-tab="versions"]').hide();
|
||||
@@ -134,7 +135,6 @@ async function UIWindowItemProperties (item_name, item_path, item_uid, left, top
|
||||
// Ignored
|
||||
}
|
||||
}
|
||||
|
||||
// shortcut to
|
||||
if ( fsentry.shortcut_to && fsentry.shortcut_to_path ) {
|
||||
$(el_window).find('.item-prop-val-shortcut-to').text(fsentry.shortcut_to_path);
|
||||
@@ -168,7 +168,14 @@ async function UIWindowItemProperties (item_name, item_path, item_uid, left, top
|
||||
else {
|
||||
$(el_window).find('.item-props-version-list').append('-');
|
||||
}
|
||||
|
||||
// worker
|
||||
if ( fsentry.path.endsWith('.js') ) {
|
||||
const has_worker = fsentry.workers.length > 0;
|
||||
if ( has_worker ) {
|
||||
const worker_url = fsentry.workers[0].address;
|
||||
$(el_window).find('.item-prop-val-worker').html(`<a target="_blank" href="${html_encode(worker_url)}">${html_encode(worker_url)}</a>`);
|
||||
}
|
||||
}
|
||||
$(el_window).find('.disassociate-website-link').on('click', function (e) {
|
||||
puter.hosting.update($(e.target).attr('data-subdomain'),
|
||||
null).then(() => {
|
||||
@@ -178,7 +185,7 @@ async function UIWindowItemProperties (item_name, item_path, item_uid, left, top
|
||||
// remove the website badge from all instances of the dir
|
||||
$(`.item[data-uid="${item_uid}"]`).find('.item-has-website-badge').fadeOut(200);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -234,7 +234,6 @@ async function UIWindowPublishWebsite (target_dir_uid, target_dir_name, target_d
|
||||
});
|
||||
|
||||
window.update_sites_cache();
|
||||
|
||||
} else if ( publishingType === 'custom' ) {
|
||||
// Handle custom domain publishing with Entri
|
||||
let customDomain = $(el_window).find('.publish-website-custom-domain').val();
|
||||
@@ -279,7 +278,6 @@ async function UIWindowPublishWebsite (target_dir_uid, target_dir_name, target_d
|
||||
});
|
||||
|
||||
window.update_sites_cache();
|
||||
|
||||
$(el_window).close();
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+87
-1237
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,8 @@ window.clipboard = [];
|
||||
window.actions_history = [];
|
||||
window.window_nav_history = {};
|
||||
window.window_nav_history_current_position = {};
|
||||
window.dashboard_nav_history = [];
|
||||
window.dashboard_nav_history_current_position = 0;
|
||||
window.progress_tracker = [];
|
||||
window.upload_item_global_id = 0;
|
||||
window.app_instance_ids = new Set();
|
||||
|
||||
+28
-23
@@ -1694,6 +1694,10 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
suggested_apps: fsentry.suggested_apps,
|
||||
};
|
||||
UIItem(options);
|
||||
// In dashboard mode, also create item via dashboard's renderer
|
||||
if ( window.is_dashboard_mode && window.UIDashboardFileItem ) {
|
||||
window.UIDashboardFileItem(fsentry);
|
||||
}
|
||||
moved_items.push({ 'options': options, 'original_path': $(el_item).attr('data-path') });
|
||||
|
||||
// this operation may have created some missing directories,
|
||||
@@ -1719,6 +1723,10 @@ window.move_items = async function (el_items, dest_path, is_undo = false) {
|
||||
suggested_apps: dir.suggested_apps,
|
||||
});
|
||||
}
|
||||
// In dashboard mode, also create parent dirs via dashboard's renderer
|
||||
if ( window.is_dashboard_mode && window.UIDashboardFileItem ) {
|
||||
window.UIDashboardFileItem(dir);
|
||||
}
|
||||
window.sort_items(item_container);
|
||||
});
|
||||
|
||||
@@ -2846,7 +2854,7 @@ window.rename_file = async (options, new_name, old_name, old_path, el_item, el_i
|
||||
puter.fs.rename({
|
||||
uid: options.uid === 'null' ? null : options.uid,
|
||||
new_name: new_name,
|
||||
excludeSocketID: window.socket.id,
|
||||
excludeSocketID: window.socket?.id,
|
||||
success: async (fsentry) => {
|
||||
// Add action to actions_history for undo ability
|
||||
if ( ! is_undo )
|
||||
@@ -2975,30 +2983,27 @@ window.delete_item_with_path = async function (path) {
|
||||
};
|
||||
|
||||
window.undo_last_action = async () => {
|
||||
if ( window.actions_history.length > 0 ) {
|
||||
const last_action = window.actions_history.pop();
|
||||
if ( window.actions_history.length === 0 ) return;
|
||||
|
||||
// Undo the create file action
|
||||
if ( last_action.operation === 'create_file' || last_action.operation === 'create_folder' ) {
|
||||
const lastCreatedItem = last_action.data;
|
||||
window.undo_create_file_or_folder(lastCreatedItem);
|
||||
} else if ( last_action.operation === 'rename' ) {
|
||||
const { options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url } = last_action.data;
|
||||
const last_action = window.actions_history.pop();
|
||||
const { operation, data } = last_action;
|
||||
|
||||
// Map operations to their undo handlers
|
||||
const undoHandlers = {
|
||||
create_file: () => window.undo_create_file_or_folder(data),
|
||||
create_folder: () => window.undo_create_file_or_folder(data),
|
||||
upload: () => window.undo_upload(data),
|
||||
copy: () => window.undo_copy(data),
|
||||
move: () => window.undo_move(data),
|
||||
delete: () => window.undo_delete(data),
|
||||
rename: () => {
|
||||
const { options, new_name, old_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url } = data;
|
||||
window.rename_file(options, old_name, new_name, old_path, el_item, el_item_name, el_item_icon, el_item_name_editor, website_url, true);
|
||||
} else if ( last_action.operation === 'upload' ) {
|
||||
const files = last_action.data;
|
||||
window.undo_upload(files);
|
||||
} else if ( last_action.operation === 'copy' ) {
|
||||
const files = last_action.data;
|
||||
window.undo_copy(files);
|
||||
} else if ( last_action.operation === 'move' ) {
|
||||
const items = last_action.data;
|
||||
window.undo_move(items);
|
||||
} else if ( last_action.operation === 'delete' ) {
|
||||
const items = last_action.data;
|
||||
window.undo_delete(items);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const handler = undoHandlers[operation];
|
||||
if ( handler ) handler();
|
||||
};
|
||||
|
||||
window.undo_create_file_or_folder = async (item) => {
|
||||
|
||||
@@ -0,0 +1,723 @@
|
||||
/**
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import UIAlert from '../UI/UIAlert.js';
|
||||
import UIWindowShare from '../UI/UIWindowShare.js';
|
||||
import UIWindowPublishWebsite from '../UI/UIWindowPublishWebsite.js';
|
||||
import UIWindowItemProperties from '../UI/UIWindowItemProperties.js';
|
||||
import UIWindowSaveAccount from '../UI/UIWindowSaveAccount.js';
|
||||
import UIWindowEmailConfirmationRequired from '../UI/UIWindowEmailConfirmationRequired.js';
|
||||
import UIWindowPublishWorker from '../UI/UIWindowPublishWorker.js';
|
||||
import open_item from './open_item.js';
|
||||
import launch_app from './launch_app.js';
|
||||
import path from '../lib/path.js';
|
||||
import mime from '../lib/mime.js';
|
||||
|
||||
const AI_APP_NAME = 'ai';
|
||||
|
||||
/**
|
||||
* Parses item metadata for AI payload
|
||||
* @param {string} metadata - JSON string of metadata
|
||||
* @returns {Object|undefined} Parsed metadata or undefined
|
||||
*/
|
||||
const parseItemMetadataForAI = (metadata) => {
|
||||
if ( ! metadata ) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(metadata);
|
||||
} catch ( error ) {
|
||||
console.warn('Failed to parse item metadata for AI payload.', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds AI payload from item elements
|
||||
* @param {jQuery} $elements - jQuery collection of elements
|
||||
* @returns {Array} Array of item data for AI
|
||||
*/
|
||||
const buildAIPayloadFromItems = ($elements) => {
|
||||
return $elements.get().map((element) => {
|
||||
const $element = $(element);
|
||||
return {
|
||||
uid: $element.attr('data-uid'),
|
||||
path: $element.attr('data-path'),
|
||||
name: $element.attr('data-name'),
|
||||
is_dir: $element.attr('data-is_dir') === '1',
|
||||
is_shortcut: $element.attr('data-is_shortcut') === '1',
|
||||
shortcut_to: $element.attr('data-shortcut_to') || undefined,
|
||||
shortcut_to_path: $element.attr('data-shortcut_to_path') || undefined,
|
||||
size: $element.attr('data-size') || undefined,
|
||||
type: $element.attr('data-type') || undefined,
|
||||
modified: $element.attr('data-modified') || undefined,
|
||||
metadata: parseItemMetadataForAI($element.attr('data-metadata')),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensures AI app iframe is available
|
||||
* @returns {Promise<HTMLIFrameElement|null>} AI app iframe or null
|
||||
*/
|
||||
const ensureAIAppIframe = async () => {
|
||||
let $aiWindow = $(`.window[data-app="${AI_APP_NAME}"]`);
|
||||
if ( $aiWindow.length === 0 ) {
|
||||
try {
|
||||
await launch_app({ name: AI_APP_NAME });
|
||||
} catch ( error ) {
|
||||
console.error('Failed to launch AI app.', error);
|
||||
return null;
|
||||
}
|
||||
$aiWindow = $(`.window[data-app="${AI_APP_NAME}"]`);
|
||||
}
|
||||
|
||||
if ( $aiWindow.length === 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$aiWindow.makeWindowVisible();
|
||||
const iframe = $aiWindow.find('.window-app-iframe').get(0);
|
||||
return iframe ?? null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends selection to AI app
|
||||
* @param {jQuery} $elements - jQuery collection of elements
|
||||
*/
|
||||
const sendSelectionToAIApp = async ($elements) => {
|
||||
const items = buildAIPayloadFromItems($elements);
|
||||
if ( items.length === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const aiIframe = await ensureAIAppIframe();
|
||||
if ( !aiIframe || !aiIframe.contentWindow ) {
|
||||
await UIAlert({
|
||||
message: i18n('ai_app_unavailable'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
aiIframe.contentWindow.postMessage({
|
||||
msg: 'ai:openFsEntries',
|
||||
items,
|
||||
source: 'desktop-context-menu',
|
||||
}, '*');
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates context menu items for file/folder operations
|
||||
*
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {HTMLElement} options.element - The DOM element representing the file/folder
|
||||
* @param {Object} options.fsentry - File system entry data (uid, path, name, is_dir, etc.)
|
||||
* @param {boolean} options.is_trash - Whether this is the trash folder
|
||||
* @param {boolean} options.is_trashed - Whether item is in trash
|
||||
* @param {Array} options.suggested_apps - Optional pre-loaded suggested apps
|
||||
* @param {string} options.associated_app_name - Optional associated app
|
||||
* @param {Function} options.onOpen - Optional custom open handler (used by Dashboard)
|
||||
* @returns {Promise<Array>} Array of context menu items
|
||||
*/
|
||||
const generate_file_context_menu = async function (options) {
|
||||
options = options || {};
|
||||
|
||||
const el_item = options.element;
|
||||
const fsentry = options.fsentry || {};
|
||||
const is_trash = options.is_trash ?? false;
|
||||
const is_trashed = options.is_trashed ?? false;
|
||||
const is_worker = options.is_worker ?? false;
|
||||
const onOpen = options.onOpen;
|
||||
|
||||
const is_shared_with_me = (fsentry.path !== `/${window.user.username}` && !fsentry.path.startsWith(`/${window.user.username}/`));
|
||||
|
||||
let menu_items = [];
|
||||
|
||||
// -------------------------------------------
|
||||
// Open
|
||||
// -------------------------------------------
|
||||
if ( ! is_trashed ) {
|
||||
menu_items.push({
|
||||
html: i18n('open'),
|
||||
onClick: () => {
|
||||
if ( onOpen ) {
|
||||
onOpen(el_item, fsentry);
|
||||
} else {
|
||||
open_item({ item: el_item });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------
|
||||
// Separator
|
||||
// -------------------------------------------
|
||||
if ( options.associated_app_name || is_trash ) {
|
||||
menu_items.push('-');
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Open With
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined) ) {
|
||||
const openWithItems = await generateOpenWithItems(el_item, fsentry, options.suggested_apps);
|
||||
menu_items.push({
|
||||
html: i18n('open_with'),
|
||||
items: openWithItems,
|
||||
});
|
||||
|
||||
menu_items.push('-');
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Open in New Window
|
||||
// (only if the item is on a window)
|
||||
// -------------------------------------------
|
||||
if ( $(el_item).closest('.window-body').length > 0 && fsentry.is_dir ) {
|
||||
menu_items.push({
|
||||
html: i18n('open_in_new_window'),
|
||||
onClick: function () {
|
||||
if ( fsentry.is_dir ) {
|
||||
open_item({ item: el_item, new_window: true });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------
|
||||
// Separator
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && !is_trashed && fsentry.is_dir ) {
|
||||
menu_items.push('-');
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Share With…
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && !is_trash ) {
|
||||
menu_items.push({
|
||||
html: i18n('Share With…'),
|
||||
onClick: async function () {
|
||||
if ( window.user.is_temp &&
|
||||
!await UIWindowSaveAccount({
|
||||
send_confirmation_code: true,
|
||||
message: 'Please create an account to proceed.',
|
||||
window_options: {
|
||||
backdrop: true,
|
||||
close_on_backdrop_click: false,
|
||||
},
|
||||
}) ) {
|
||||
return;
|
||||
}
|
||||
else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const icon = $(el_item).find('.icon img').attr('src') || $(el_item).find('img').attr('src');
|
||||
UIWindowShare([{
|
||||
uid: $(el_item).attr('data-uid'),
|
||||
path: $(el_item).attr('data-path'),
|
||||
name: $(el_item).attr('data-name'),
|
||||
icon: icon,
|
||||
}]);
|
||||
},
|
||||
});
|
||||
|
||||
// -------------------------------------------
|
||||
// Open in AI
|
||||
// -------------------------------------------
|
||||
menu_items.push({
|
||||
html: i18n('open_in_ai'),
|
||||
onClick: async function () {
|
||||
await sendSelectionToAIApp($(el_item));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Publish As Website
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && !is_trash && fsentry.is_dir ) {
|
||||
menu_items.push({
|
||||
html: i18n('publish_as_website'),
|
||||
disabled: !fsentry.is_dir || fsentry.has_website,
|
||||
onClick: async function () {
|
||||
if ( window.require_email_verification_to_publish_website ) {
|
||||
if ( window.user.is_temp &&
|
||||
!await UIWindowSaveAccount({
|
||||
send_confirmation_code: true,
|
||||
message: 'Please create an account to proceed.',
|
||||
window_options: {
|
||||
backdrop: true,
|
||||
close_on_backdrop_click: false,
|
||||
},
|
||||
}) ) {
|
||||
return;
|
||||
}
|
||||
else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
UIWindowPublishWebsite(fsentry.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Publish as Worker
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && !is_trash && !fsentry.is_dir && $(el_item).attr('data-name').toLowerCase().endsWith('.js') ) {
|
||||
menu_items.push({
|
||||
html: i18n('publish_as_serverless_worker'),
|
||||
disabled: is_worker,
|
||||
onClick: async function () {
|
||||
if ( window.user.is_temp &&
|
||||
!await UIWindowSaveAccount({
|
||||
send_confirmation_code: true,
|
||||
message: 'Please create an account to proceed.',
|
||||
window_options: {
|
||||
backdrop: true,
|
||||
close_on_backdrop_click: false,
|
||||
},
|
||||
}) ) {
|
||||
return;
|
||||
}
|
||||
else if ( !window.user.email_confirmed && !await UIWindowEmailConfirmationRequired() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIWindowPublishWorker(fsentry.uid, $(el_item).attr('data-name'), $(el_item).attr('data-path'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Deploy As App
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && !is_trash && fsentry.is_dir ) {
|
||||
menu_items.push({
|
||||
html: i18n('deploy_as_app'),
|
||||
disabled: !fsentry.is_dir,
|
||||
onClick: async function () {
|
||||
launch_app({
|
||||
name: 'dev-center',
|
||||
file_path: $(el_item).attr('data-path'),
|
||||
file_uid: $(el_item).attr('data-uid'),
|
||||
params: {
|
||||
source_path: fsentry.path,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
menu_items.push('-');
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Empty Trash
|
||||
// -------------------------------------------
|
||||
if ( is_trash ) {
|
||||
menu_items.push({
|
||||
html: i18n('empty_trash'),
|
||||
onClick: async function () {
|
||||
window.empty_trash();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Download
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && !is_trashed && (options.associated_app_name === null || options.associated_app_name === undefined) ) {
|
||||
menu_items.push({
|
||||
html: i18n('download'),
|
||||
disabled: fsentry.is_dir && !window.feature_flags.download_directory,
|
||||
onClick: async function () {
|
||||
if ( fsentry.is_dir ) {
|
||||
window.zipItems(el_item, path.dirname($(el_item).attr('data-path')), true);
|
||||
}
|
||||
else {
|
||||
window.trigger_download([fsentry.path]);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Set as Wallpaper
|
||||
// -------------------------------------------
|
||||
const mime_type = mime.getType($(el_item).attr('data-name')) ?? 'application/octet-stream';
|
||||
if ( !window.dashboard_object && !is_trashed && !is_trash && !fsentry.is_dir && mime_type.startsWith('image/') ) {
|
||||
menu_items.push({
|
||||
html: i18n('set_as_background'),
|
||||
onClick: async function () {
|
||||
const read_url = await puter.fs.sign(undefined, { uid: $(el_item).attr('data-uid'), action: 'read' });
|
||||
window.set_desktop_background({
|
||||
url: read_url.items.read_url,
|
||||
fit: window.desktop_bg_fit,
|
||||
});
|
||||
try {
|
||||
$.ajax({
|
||||
url: `${window.api_origin}/set-desktop-bg`,
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
url: window.desktop_bg_url,
|
||||
color: window.desktop_bg_color,
|
||||
fit: window.desktop_bg_fit,
|
||||
}),
|
||||
async: true,
|
||||
contentType: 'application/json',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${window.auth_token}`,
|
||||
},
|
||||
statusCode: {
|
||||
401: function () {
|
||||
window.logout();
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch ( err ) {
|
||||
// Ignore
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Zip
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.zip') ) {
|
||||
menu_items.push({
|
||||
html: i18n('zip'),
|
||||
onClick: function () {
|
||||
window.zipItems(el_item, path.dirname($(el_item).attr('data-path')), false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Unzip
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && !is_trashed && $(el_item).attr('data-path').endsWith('.zip') ) {
|
||||
menu_items.push({
|
||||
html: i18n('unzip'),
|
||||
onClick: async function () {
|
||||
let filePath = $(el_item).attr('data-path');
|
||||
window.unzipItem(filePath);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Tar
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.tar') ) {
|
||||
menu_items.push({
|
||||
html: i18n('tar'),
|
||||
onClick: function () {
|
||||
window.tarItems(el_item, path.dirname($(el_item).attr('data-path')), false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Untar
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && !is_trashed && $(el_item).attr('data-path').endsWith('.tar') ) {
|
||||
menu_items.push({
|
||||
html: i18n('untar'),
|
||||
onClick: async function () {
|
||||
let filePath = $(el_item).attr('data-path');
|
||||
window.untarItem(filePath);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Restore
|
||||
// -------------------------------------------
|
||||
if ( is_trashed ) {
|
||||
menu_items.push({
|
||||
html: i18n('restore'),
|
||||
onClick: async function () {
|
||||
await options.onRestore(el_item);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Separator
|
||||
// -------------------------------------------
|
||||
if ( !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined) ) {
|
||||
menu_items.push('-');
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Cut
|
||||
// -------------------------------------------
|
||||
if ( $(el_item).attr('data-immutable') === '0' && !is_shared_with_me ) {
|
||||
menu_items.push({
|
||||
html: i18n('cut'),
|
||||
onClick: function () {
|
||||
window.clipboard_op = 'move';
|
||||
window.clipboard = [fsentry.path];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Copy
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && !is_trash ) {
|
||||
menu_items.push({
|
||||
html: i18n('copy'),
|
||||
onClick: function () {
|
||||
window.clipboard_op = 'copy';
|
||||
window.clipboard = [{ path: fsentry.path }];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Paste Into Folder
|
||||
// -------------------------------------------
|
||||
if ( $(el_item).attr('data-is_dir') === '1' && !is_trashed && !is_trash ) {
|
||||
menu_items.push({
|
||||
html: i18n('paste_into_folder'),
|
||||
disabled: window.clipboard.length > 0 ? false : true,
|
||||
onClick: function () {
|
||||
if ( window.clipboard_op === 'copy' ) {
|
||||
window.copy_clipboard_items($(el_item).attr('data-path'), null);
|
||||
}
|
||||
else if ( window.clipboard_op === 'move' ) {
|
||||
window.move_clipboard_items(null, $(el_item).attr('data-path'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Separator
|
||||
// -------------------------------------------
|
||||
if ( $(el_item).attr('data-immutable') === '0' && !is_trash ) {
|
||||
menu_items.push('-');
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Create Shortcut
|
||||
// -------------------------------------------
|
||||
if ( !is_trashed && window.feature_flags.create_shortcut ) {
|
||||
menu_items.push({
|
||||
html: is_shared_with_me ? i18n('create_desktop_shortcut') : i18n('create_shortcut'),
|
||||
onClick: async function () {
|
||||
let base_dir = path.dirname($(el_item).attr('data-path'));
|
||||
// Trash on Desktop is a special case
|
||||
if ( $(el_item).attr('data-path') && $(el_item).closest('.item-container').attr('data-path') === window.desktop_path ) {
|
||||
base_dir = window.desktop_path;
|
||||
}
|
||||
|
||||
if ( is_shared_with_me ) base_dir = window.desktop_path;
|
||||
|
||||
window.create_shortcut(path.basename($(el_item).attr('data-path')),
|
||||
fsentry.is_dir,
|
||||
base_dir,
|
||||
null, // appendTo - will be determined by create_shortcut
|
||||
fsentry.shortcut_to === '' ? fsentry.uid : fsentry.shortcut_to,
|
||||
fsentry.shortcut_to_path === '' ? fsentry.path : fsentry.shortcut_to_path);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Delete
|
||||
// -------------------------------------------
|
||||
if ( $(el_item).attr('data-immutable') === '0' && !is_trashed && !is_shared_with_me ) {
|
||||
menu_items.push({
|
||||
html: i18n('delete'),
|
||||
onClick: async function () {
|
||||
await window.move_items([el_item], window.trash_path);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Delete Permanently
|
||||
// -------------------------------------------
|
||||
if ( is_trashed ) {
|
||||
menu_items.push({
|
||||
html: i18n('delete_permanently'),
|
||||
onClick: async function () {
|
||||
const alert_resp = await UIAlert({
|
||||
message: i18n('confirm_delete_single_item'),
|
||||
buttons: [
|
||||
{
|
||||
label: i18n('delete'),
|
||||
type: 'primary',
|
||||
},
|
||||
{
|
||||
label: i18n('cancel'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if ( (alert_resp) === 'Delete' ) {
|
||||
await window.delete_item(el_item);
|
||||
// check if trash is empty
|
||||
const trash = await puter.fs.stat({ path: window.trash_path, consistency: 'eventual' });
|
||||
// update other clients
|
||||
if ( window.socket ) {
|
||||
window.socket.emit('trash.is_empty', { is_empty: trash.is_empty });
|
||||
}
|
||||
// update this client
|
||||
if ( trash.is_empty ) {
|
||||
$(`.item[data-path="${window.trash_path}" i], .item[data-shortcut_to_path="${window.trash_path}" i]`).find('.item-icon > img').attr('src', window.icons['trash.svg']);
|
||||
$(`.window[data-path="${window.trash_path}"]`).find('.window-head-icon').attr('src', window.icons['trash.svg']);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Rename
|
||||
// -------------------------------------------
|
||||
if ( $(el_item).attr('data-immutable') === '0' && !is_trashed && !is_trash ) {
|
||||
menu_items.push({
|
||||
html: i18n('rename'),
|
||||
onClick: function () {
|
||||
window.activate_item_name_editor(el_item);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
// Separator
|
||||
// -------------------------------------------
|
||||
menu_items.push('-');
|
||||
|
||||
// -------------------------------------------
|
||||
// Properties
|
||||
// -------------------------------------------
|
||||
menu_items.push({
|
||||
html: i18n('properties'),
|
||||
onClick: function () {
|
||||
let window_height = 500;
|
||||
let window_width = 450;
|
||||
|
||||
let left = $(el_item).position().left + $(el_item).width();
|
||||
left = left > (window.innerWidth - window_width) ? (window.innerWidth - window_width) : left;
|
||||
|
||||
let top = $(el_item).position().top + $(el_item).height();
|
||||
top = top > (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) ? (window.innerHeight - (window_height + window.taskbar_height + window.toolbar_height)) : top;
|
||||
|
||||
UIWindowItemProperties($(el_item).attr('data-name'),
|
||||
$(el_item).attr('data-path'),
|
||||
$(el_item).attr('data-uid'),
|
||||
left,
|
||||
top,
|
||||
window_width,
|
||||
window_height);
|
||||
},
|
||||
});
|
||||
|
||||
return menu_items;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates "Open With" menu items for a file
|
||||
*
|
||||
* @param {HTMLElement} el_item - The DOM element representing the file
|
||||
* @param {Object} fsentry - File system entry data
|
||||
* @param {Array} suggested_apps - Optional pre-loaded suggested apps
|
||||
* @returns {Promise<Array>} Array of menu items for "Open With" submenu
|
||||
*/
|
||||
async function generateOpenWithItems (el_item, fsentry, suggested_apps) {
|
||||
let items = [];
|
||||
|
||||
// Try to find suitable apps if not provided
|
||||
if ( !suggested_apps || suggested_apps.length === 0 ) {
|
||||
const suitable_apps = await window.suggest_apps_for_fsentry({
|
||||
uid: fsentry.uid,
|
||||
path: fsentry.path,
|
||||
});
|
||||
if ( suitable_apps && suitable_apps.length > 0 ) {
|
||||
suggested_apps = suitable_apps;
|
||||
}
|
||||
}
|
||||
|
||||
if ( suggested_apps && suggested_apps.length > 0 ) {
|
||||
for ( let index = 0; index < suggested_apps.length; index++ ) {
|
||||
const suggested_app = suggested_apps[index];
|
||||
if ( ! suggested_app ) {
|
||||
console.warn('suggested_app is null', suggested_apps, index);
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
html: suggested_app.title,
|
||||
icon: `<img src="${html_encode(suggested_app.icon ?? window.icons['app.svg'])}" style="width:16px; height: 16px; margin-bottom: -4px;">`,
|
||||
onClick: async function () {
|
||||
var extension = path.extname($(el_item).attr('data-path')).toLowerCase();
|
||||
if (
|
||||
window.user_preferences[`default_apps${extension}`] !== suggested_app.name
|
||||
&&
|
||||
(
|
||||
(!window.user_preferences[`default_apps${extension}`] && index > 0)
|
||||
||
|
||||
(window.user_preferences[`default_apps${extension}`])
|
||||
)
|
||||
) {
|
||||
const alert_resp = await UIAlert({
|
||||
message: `${i18n('change_always_open_with')} ${html_encode(suggested_app.title)}?`,
|
||||
body_icon: suggested_app.icon,
|
||||
buttons: [
|
||||
{
|
||||
label: i18n('yes'),
|
||||
type: 'primary',
|
||||
value: 'yes',
|
||||
},
|
||||
{
|
||||
label: i18n('no'),
|
||||
},
|
||||
],
|
||||
});
|
||||
if ( (alert_resp) === 'yes' ) {
|
||||
window.user_preferences[`default_apps${extension}`] = suggested_app.name;
|
||||
window.mutate_user_preferences(window.user_preferences);
|
||||
}
|
||||
}
|
||||
launch_app({
|
||||
name: suggested_app.name,
|
||||
file_path: $(el_item).attr('data-path'),
|
||||
window_title: $(el_item).attr('data-name'),
|
||||
file_uid: $(el_item).attr('data-uid'),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
html: i18n('no_suitable_apps_found'),
|
||||
disabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export default generate_file_context_menu;
|
||||
@@ -36,6 +36,15 @@ const get_html_element_from_options = async function (options) {
|
||||
options.immutable = (options.immutable === false || options.immutable === 0 || options.immutable === undefined ? 0 : 1);
|
||||
options.sort_container_after_append = (options.sort_container_after_append !== undefined ? options.sort_container_after_append : false);
|
||||
const is_shared_with_me = (options.path !== `/${window.user.username}` && !options.path.startsWith(`/${window.user.username}/`));
|
||||
let worker_url;
|
||||
let is_worker;
|
||||
if ( ! options.is_dir ) {
|
||||
const stats = await puter.fs.stat({ path: options.path, returnWorkers: true });
|
||||
is_worker = stats.workers !== undefined && stats.workers.length > 0;;
|
||||
if ( is_worker ) {
|
||||
worker_url = stats.workers[0].address;
|
||||
}
|
||||
}
|
||||
|
||||
let website_url = window.determine_website_url(options.path);
|
||||
|
||||
@@ -62,6 +71,8 @@ const get_html_element_from_options = async function (options) {
|
||||
data-website_url = "${website_url ? html_encode(website_url) : ''}"
|
||||
data-immutable="${options.immutable}"
|
||||
data-is_shortcut = "${options.is_shortcut}"
|
||||
data-is_worker = "${is_worker !== undefined ? 1 : 0}"
|
||||
data-worker_url = "${is_worker !== undefined ? worker_url : 0}"
|
||||
data-shortcut_to = "${html_encode(options.shortcut_to)}"
|
||||
data-shortcut_to_path = "${html_encode(options.shortcut_to_path)}"
|
||||
data-sortable = "${options.sortable ?? 'true'}"
|
||||
@@ -137,7 +148,12 @@ const get_html_element_from_options = async function (options) {
|
||||
data-item-id="${item_id}"
|
||||
title="Shortcut"
|
||||
>`;
|
||||
|
||||
// worker badge
|
||||
h += `<img class="item-badge item-is-worker long-hover"
|
||||
style="background-color: #ffffff; padding: 2px; ${is_worker ? 'display:block;' : ''}"
|
||||
src="${html_encode(window.icons['worker.svg'])}"
|
||||
data-item-id="${item_id}"
|
||||
>`;
|
||||
h += '</div>';
|
||||
|
||||
// name
|
||||
|
||||
@@ -170,7 +170,7 @@ router.get('/', ({request}) => {
|
||||
return 'Hello World'; // returns a string
|
||||
});
|
||||
router.get('/api/hello', ({request}) => {
|
||||
return {'msg': 'hello'}; // returns a JSON object
|
||||
return {'msg': 'hello'}; // returns a JSON object
|
||||
});
|
||||
router.get('/*page', ({request, params}) => {
|
||||
return new Response(\`Page \${params.page} not found\`, {status: 404});
|
||||
|
||||
@@ -321,6 +321,7 @@ const en = {
|
||||
taskmgr_header_type: 'Type',
|
||||
terms: 'Terms',
|
||||
text_document: 'Text document',
|
||||
toggle_view: 'Toggle view',
|
||||
'toolbar.enter_fullscreen': 'Enter Full Screen',
|
||||
'toolbar.github': 'GitHub',
|
||||
'toolbar.refer': 'Refer',
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" color="#455a64" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap-icon lucide-zap"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>
|
||||
|
After Width: | Height: | Size: 412 B |
@@ -156,8 +156,31 @@ if ( jQuery ) {
|
||||
// are we in dashboard mode?
|
||||
if ( window.location.pathname === '/dashboard' || window.location.pathname === '/dashboard/' ) {
|
||||
window.is_dashboard_mode = true;
|
||||
window.dashboard_initial_route = parseDashboardRoute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the dashboard URL hash into a route object.
|
||||
* Hash format: #files/username/Documents or #usage or #account etc.
|
||||
* @returns {{ tab: string, path: string|null }} Route object with tab name and optional file path
|
||||
*/
|
||||
function parseDashboardRoute () {
|
||||
const hash = decodeURIComponent(window.location.hash.slice(1)); // Remove '#' and decode URL encoding
|
||||
if ( ! hash ) return { tab: 'home', path: null };
|
||||
|
||||
const parts = hash.split('/').filter(Boolean); // ['files', 'username', 'Documents']
|
||||
const tab = parts[0]; // 'files', 'usage', 'account', 'security'
|
||||
|
||||
if ( tab === 'files' && parts.length > 1 ) {
|
||||
const filePath = `/${parts.slice(1).join('/')}`; // /username/Documents
|
||||
return { tab: 'files', path: filePath };
|
||||
}
|
||||
return { tab: tab || 'home', path: null };
|
||||
}
|
||||
|
||||
// Make parseDashboardRoute available globally for hashchange handler
|
||||
window.parseDashboardRoute = parseDashboardRoute;
|
||||
|
||||
/**
|
||||
* Shows a Turnstile challenge modal for first-time temp user creation
|
||||
* @param {Object} options - Configuration options
|
||||
|
||||
Reference in New Issue
Block a user