diff --git a/src/UI/UIDesktop.js b/src/UI/UIDesktop.js index d73f918c..cab94d9d 100644 --- a/src/UI/UIDesktop.js +++ b/src/UI/UIDesktop.js @@ -35,6 +35,7 @@ import new_context_menu_item from "../helpers/new_context_menu_item.js" import refresh_item_container from "../helpers/refresh_item_container.js" import changeLanguage from "../i18n/i18nChangeLanguage.js" import UIWindowSettings from "./Settings/UIWindowSettings.js" +import UIWindowTaskManager from "./UIWindowTaskManager.js" async function UIDesktop(options){ let h = ''; @@ -1190,7 +1191,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ } }, //-------------------------------------------------- - // Change Password + // Settings //-------------------------------------------------- { html: i18n('settings'), @@ -1199,6 +1200,15 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ } }, //-------------------------------------------------- + // Task Manager + //-------------------------------------------------- + { + html: i18n('task_manager'), + onClick: async function(){ + UIWindowTaskManager(); + } + }, + //-------------------------------------------------- // Contact Us //-------------------------------------------------- { diff --git a/src/UI/UIWindow.js b/src/UI/UIWindow.js index 7274e243..812959f1 100644 --- a/src/UI/UIWindow.js +++ b/src/UI/UIWindow.js @@ -2773,6 +2773,7 @@ window.sidebar_item_droppable = (el_window)=>{ // closes a window $.fn.close = async function(options) { options = options || {}; + console.log(options); $(this).each(async function() { const el_iframe = $(this).find('.window-app-iframe'); const app_uses_sdk = el_iframe.length > 0 && el_iframe.attr('data-appUsesSDK') === 'true'; diff --git a/src/UI/UIWindowTaskManager.js b/src/UI/UIWindowTaskManager.js new file mode 100644 index 00000000..b01737a9 --- /dev/null +++ b/src/UI/UIWindowTaskManager.js @@ -0,0 +1,263 @@ +import { END_HARD, END_SOFT } from "../definitions.js"; +import UIAlert from "./UIAlert.js"; +import UIContextMenu from "./UIContextMenu.js"; +import UIWindow from "./UIWindow.js"; + +const UIWindowTaskManager = async function UIWindowTaskManager () { + const svc_process = globalThis.services.get('process'); + + const w = await UIWindow({ + title: i18n('task_manager'), + icon: globalThis.icons['cog.svg'], + uid: null, + is_dir: false, + message: 'message', + app: 'taskmgr', + // body_icon: options.body_icon, + // backdrop: options.backdrop ?? false, + is_resizable: true, + is_droppable: false, + has_head: true, + selectable_body: true, + draggable_body: false, + allow_context_menu: true, + // allow_native_ctxmenu: true, + show_in_taskbar: true, + dominant: true, + body_content: '', + width: 350, + // parent_uuid: options.parent_uuid, + // ...options.window_options, + window_css:{ + height: 'initial', + }, + body_css: { + width: 'initial', + padding: '20px', + // 'background-color': `hsla( + // var(--primary-hue), + // calc(max(var(--primary-saturation) - 15%, 0%)), + // calc(min(100%,var(--primary-lightness) + 20%)), .91)`, + 'background-color': `hsla( + var(--primary-hue), + var(--primary-saturation), + var(--primary-lightness), + var(--primary-alpha))`, + 'backdrop-filter': 'blur(3px)', + + } + }); + const w_body = w.querySelector('.window-body'); + w_body.classList.add('taskmgr'); + + const Indent = ({ has_trunk, has_branch }) => { + const el = document.createElement('div'); + el.classList.add('taskmgr-indentcell'); + if ( has_trunk ) { + // Add new child element + const el_indentcell_child = document.createElement('div'); + el_indentcell_child.classList.add('taskmgr-indentcell-trunk'); + el.appendChild(el_indentcell_child); + } + if ( has_branch ) { + const el_indentcell_child = document.createElement('div'); + el_indentcell_child.classList.add('taskmgr-indentcell-branch'); + el.appendChild(el_indentcell_child); + } + + return { + appendTo (parent) { + parent.appendChild(el); + return this; + } + }; + }; + + const Task = ({ placement, name }) => { + const { + indent_level, last_item, + parent_last_item, + } = placement; + + const el = document.createElement('div'); + el.classList.add('taskmgr-task'); + + for ( let i=0; i < indent_level; i++ ) { + const last_cell = i === indent_level - 1; + Indent({ + has_trunk: (last_cell && ( ! last_item )) || + (!last_cell && !parent_last_item[i+1]), + has_branch: last_cell + }).appendTo(el); + } + + const el_title = document.createElement('div'); + el_title.classList.add('taskmgr-task-title'); + el_title.innerText = name; + el.appendChild(el_title); + + return { + el () { return el; }, + appendTo (parent) { + parent.appendChild(el); + return this; + } + }; + } + + // https://codepen.io/fomkin/pen/gOgoBVy + const Table = ({ headings }) => { + const el_table = $(` + + + + ${headings.map(heading => + ``).join('')} + + + +
${heading}
+ `)[0]; + + const el_tbody = el_table.querySelector('tbody'); + + return { + el () { return el_table; }, + add (el) { + if ( typeof el.el === 'function' ) el = el.el(); + el_tbody.appendChild(el); + return this; + }, + clear () { + el_tbody.innerHTML = ''; + } + }; + }; + + const Row = () => { + const el_tr = document.createElement('tr'); + return { + attach (parent) { + parent.appendChild(el_tr); + return this; + }, + el () { return el_tr; }, + add (el) { + if ( typeof el.el === 'function' ) el = el.el(); + const el_td = document.createElement('td'); + el_td.appendChild(el); + el_tr.appendChild(el_td); + return this; + } + }; + }; + + const el_taskarea = document.createElement('div'); + el_taskarea.classList.add('taskmgr-taskarea'); + + const tasktable = Table({ + headings: [ + i18n('taskmgr_header_name'), + i18n('taskmgr_header_type'), + i18n('taskmgr_header_status'), + ] + }); + + el_taskarea.appendChild(tasktable.el()); + + const end_process_ = async (process, force) => { + let confirmation; + + if ( process.is_init() ) { + if ( ! force ) { + confirmation = i18n('close_all_windows_confirm'); + } else { + confirmation = i18n('restart_puter_confirm'); + } + } else if ( force ) { + confirmation = i18n('end_process_force_confirm'); + } + + if ( confirmation ) { + const alert_resp = await UIAlert({ + message: confirmation, + buttons:[ + { + label: i18n('yes'), + value: true, + type: 'primary', + }, + { + label: i18n('no'), + value: false, + }, + ] + }) + if ( ! alert_resp ) return; + } + + process.signal(force ? END_HARD : END_SOFT); + } + + const iter_tasks = (items, { indent_level, parent_last_item }) => { + for ( let i=0 ; i < items.length; i++ ) { + const row = Row(); + const item = items[i]; + const last_item = i === items.length - 1; + row.add(Task({ + placement: { + parent_last_item, + indent_level, + last_item, + }, + name: item.name + })); + row.add($(`${i18n('process_type_' + item.type)}`)[0]) + row.add($(`${i18n('process_status_' + item.status.i18n_key)}`)[0]) + tasktable.add(row); + + $(row.el()).on('contextmenu', () => { + UIContextMenu({ + parent_element: $(el_taskarea), + items: [ + { + html: i18n('close'), + onClick: () => { + end_process_(item); + } + }, + { + html: i18n('force_quit'), + onClick: () => { + end_process_(item, true); + } + } + ] + }); + }) + + const children = svc_process.get_children_of(item.uuid); + if ( children ) { + iter_tasks(children, { + indent_level: indent_level + 1, + parent_last_item: + [...parent_last_item, last_item], + }); + } + } + }; + + const interval = setInterval(() => { + tasktable.clear(); + const processes = [svc_process.get_init()]; + iter_tasks(processes, { indent_level: 0, parent_last_item: [] }); + }, 500) + + w.on_close = () => { + clearInterval(interval); + } + + w_body.appendChild(el_taskarea); +} + +export default UIWindowTaskManager; diff --git a/src/UI/UIWindowThemeDialog.js b/src/UI/UIWindowThemeDialog.js index 3932d183..58646a6f 100644 --- a/src/UI/UIWindowThemeDialog.js +++ b/src/UI/UIWindowThemeDialog.js @@ -1,4 +1,5 @@ import UIWindow from "./UIWindow.js"; +import UIWindowColorPicker from "./UIWindowColorPicker.js"; const UIWindowThemeDialog = async function UIWindowThemeDialog (options) { options = options ?? {}; @@ -114,6 +115,12 @@ const UIWindowThemeDialog = async function UIWindowThemeDialog (options) { svc_theme.reset(); }) ; + Button({ label: i18n('reset_colors') }) + .appendTo(w_body) + .onPress(() => { + UIWindowColorPicker(); + }) + ; Slider({ label: i18n('hue'), diff --git a/src/css/style.css b/src/css/style.css index 7096bae2..cabcd58a 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -3781,4 +3781,114 @@ label { flex-direction: row; gap: 10px; justify-content: flex-end; +} + +.taskmgr { + box-sizing: border-box; + /* could have been avoided with box-sizing: border-box */ + height: calc(100% - 30px); + display: flex; + flex-direction: column; + + --scale: 2pt; + --line-color: #6e6e6ebd; +} + +.taskmgr * { + box-sizing: border-box; +} + +.taskmgr table { + border-collapse: collapse; +} + +.taskmgr-taskarea { + flex-grow: 1; + display: flex; + flex-direction: column; + background-color: rgba(255,255,255,0.8); + border: 2px inset rgba(127, 127, 127, 0.3); + overflow: auto; +} + +.taskmgr-taskarea table thead { +} + +.taskmgr-taskarea table th { + -webkit-box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2); + box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2); + backdrop-filter: blur(2px); + position: sticky; + z-index: 100; + padding: 0; + top: 0; + background-color: hsla(0, 0%, 100%, 0.8); + text-align: left; +} + +.taskmgr-taskarea table th > span { + display: inline-block; + width: 100%; + /* we set borders on this span because */ + /* borders fly away from sticky headers */ + border-bottom: 1px solid #e0e0e0; + + /* padding order: top right bottom left */ + padding: + calc(10 * var(--scale)) + calc(2.5 * var(--scale)) + calc(5 * var(--scale)) + calc(2.5 * var(--scale)); +} + +.taskmgr-taskarea table th:not(:last-of-type) > span { + /* we set borders on this span because */ + /* borders fly away from sticky headers */ + border-right: 1px solid #e0e0e0; +} + +.taskmgr-taskarea table td { + border-bottom: 1px solid #e0e0e0; +} + +.taskmgr-taskarea table td > span { + padding: 0 calc(2.5 * var(--scale)); +} + +.taskmgr-indentcell { + position: relative; + align-items: right; + width: calc(10 * var(--scale)); + height: calc(10 * var(--scale)); +} + +.taskmgr-indentcell-trunk { + position: absolute; + top: 0; + left: calc(5 * var(--scale)); + width: calc(5 * var(--scale)); + height: calc(10 * var(--scale)); + border-left: 2px solid var(--line-color); +} + +.taskmgr-indentcell-branch { + position: absolute; + top: 0; + left: calc(5 * var(--scale)); + width: calc(5 * var(--scale)); + height: calc(5 * var(--scale)); + border-left: 2px solid var(--line-color); + border-bottom: 2px solid var(--line-color); + border-radius: 0 0 0 calc(2.5 * var(--scale)); +} + +.taskmgr-task { + display: flex; + height: calc(10 * var(--scale)); + line-height: calc(10 * var(--scale)); +} + +.taskmgr-task-title { + flex-grow: 1; + padding-left: calc(2.5 * var(--scale)); } \ No newline at end of file diff --git a/src/definitions.js b/src/definitions.js index 872e6315..026ace3c 100644 --- a/src/definitions.js +++ b/src/definitions.js @@ -19,3 +19,94 @@ export class Service { // }; + +export const PROCESS_INITIALIZING = { i18n_key: 'initializing' }; +export const PROCESS_RUNNING = { i18n_key: 'running' }; + +// Something is cloning these objects, so '===' checks don't work. +// To work around this, the `i` property is used to compare them. +export const END_SOFT = { i: 0, end: true, i18n_key: 'end_soft' }; +export const END_HARD = { i: 1, end: true, i18n_key: 'end_hard' }; + +export class Process { + constructor ({ uuid, parent, name, meta }) { + this.uuid = uuid; + this.parent = parent; + this.name = name; + this.meta = meta; + this.references = {}; + + this.status = PROCESS_INITIALIZING; + + this._construct(); + } + _construct () {} + + chstatus (status) { + this.status = status; + } + + is_init () {} + + signal (sig) { + this._signal(sig); + } + + get type () { + const _to_type_name = (name) => { + return name.replace(/Process$/, '').toLowerCase(); + }; + return this.type_ || _to_type_name(this.constructor.name) || + 'invalid' + } +}; + +export class InitProcess extends Process { + static created_ = false; + + is_init () { return true; } + + _construct () { + this.name = 'Puter'; + + if (InitProcess.created_) { + throw new Error('InitProccess already created'); + } + + InitProcess.created_ = true; + } + + _signal (sig) { + const svc_process = globalThis.services.get('process'); + for ( const process of svc_process.processes ) { + if ( process === this ) continue; + process.signal(sig); + } + + if ( sig.i !== END_HARD.i ) return; + + // Currently this is the only way to terminate `init`. + window.location.reload(); + } +} + +export class PortalProcess extends Process { + _construct () { this.type_ = 'app' } + _signal (sig) { + if ( sig.end ) { + $(this.references.el_win).close({ + bypass_iframe_messaging: sig.i === END_HARD.i + }); + } + } +}; +export class PseudoProcess extends Process { + _construct () { this.type_ = 'ui' } + _signal (sig) { + if ( sig.end ) { + $(this.references.el_win).close({ + bypass_iframe_messaging: sig.i === END_HARD.i + }); + } + } +}; diff --git a/src/helpers.js b/src/helpers.js index aa23d499..f9504ca4 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -36,6 +36,7 @@ import update_username_in_gui from './helpers/update_username_in_gui.js'; import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js'; import content_type_to_icon from './helpers/content_type_to_icon.js'; import UIWindowDownloadDirProg from './UI/UIWindowDownloadDirProg.js'; +import { PROCESS_RUNNING, PortalProcess, PseudoProcess } from "./definitions.js"; window.is_auth = ()=>{ if(localStorage.getItem("auth_token") === null || auth_token === null) @@ -1680,10 +1681,30 @@ window.launch_app = async (options)=>{ // add file_signature to options file_signature = file_signature.items; } + + // ----------------------------------- + // Create entry to track the "portal" + // (portals are processese in Puter's GUI) + // ----------------------------------- + + let el_win; + let process; + //------------------------------------ // Explorer //------------------------------------ if(options.name === 'explorer'){ + process = new PseudoProcess({ + uuid, + name: 'explorer', + parent: options.parent_instance_id, + meta: { + launch_options: options, + app_info: app_info, + } + }); + const svc_process = globalThis.services.get('process'); + svc_process.register(process); if(options.path === window.home_path){ title = 'Home'; icon = window.icons['folder-home.svg']; @@ -1697,7 +1718,7 @@ window.launch_app = async (options)=>{ title = path.dirname(options.path); // open window - UIWindow({ + el_win = UIWindow({ element_uuid: uuid, icon: icon, path: options.path ?? window.home_path, @@ -1713,6 +1734,18 @@ window.launch_app = async (options)=>{ // All other apps //------------------------------------ else{ + process = new PortalProcess({ + uuid, + name: app_info.name, + parent: options.parent_instance_id, + meta: { + launch_options: options, + app_info: app_info, + } + }); + const svc_process = globalThis.services.get('process'); + svc_process.register(process); + //----------------------------------- // iframe_url //----------------------------------- @@ -1808,7 +1841,7 @@ window.launch_app = async (options)=>{ console.log('backgrounded??', app_info.background); - const el_win = UIWindow({ + el_win = UIWindow({ element_uuid: uuid, title: title, iframe_url: iframe_url.href, @@ -1826,7 +1859,7 @@ window.launch_app = async (options)=>{ is_fullpage: options.is_fullpage, ...window_options, show_in_taskbar: app_info.background ? false : window_options?.show_in_taskbar, - }); + }); if ( ! app_info.background ) { $(el_win).show(); @@ -1859,6 +1892,18 @@ window.launch_app = async (options)=>{ }) } } + + (async () => { + const el = await el_win; + console.log('RESOV', el); + $(el).on('remove', () => { + const svc_process = globalThis.services.get('process'); + svc_process.unregister(process.uuid); + }); + + process.references.el_win = el; + process.chstatus(PROCESS_RUNNING); + })(); } window.open_item = async function(options){ diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js index f5b8eff4..44b82959 100644 --- a/src/i18n/translations/en.js +++ b/src/i18n/translations/en.js @@ -39,7 +39,9 @@ const en = { change_password: "Change Password", change_ui_colors: "Change UI Colors", change_username: "Change Username", + close: 'Close', close_all_windows: "Close All Windows", + close_all_windows_confirm: "Are you sure you want to close all windows?", close_all_windows_and_log_out: 'Close Windows and Log Out', change_always_open_with: "Do you want to always open this type of file with", color: 'Color', @@ -88,11 +90,15 @@ const en = { empty_trash: 'Empty Trash', empty_trash_confirmation: `Are you sure you want to permanently delete the items in Trash?`, emptying_trash: 'Emptying Trash…', + end_hard: "End Hard", + end_process_force_confirm: "Are you sure you want to force-quit this process?", + end_soft: "End Soft", enter_password_to_confirm_delete_user: "Enter your password to confirm account deletion", feedback: "Feedback", feedback_c2a: "Please use the form below to send us your feedback, comments, and bug reports.", feedback_sent_confirmation: "Thank you for contacting us. If you have an email associated with your account, you will hear back from us as soon as possible.", fit: "Fit", + force_quit: 'Force Quit', forgot_pass_c2a: "Forgot password?", from: "From", general: "General", @@ -154,6 +160,11 @@ const en = { privacy: "Privacy", proceed_to_login: 'Proceed to login', proceed_with_account_deletion: "Proceed with Account Deletion", + process_status_initializing: "Initializing", + process_status_running: "Running", + process_type_app: 'App', + process_type_init: 'Init', + process_type_ui: 'UI', properties: "Properties", publish: "Publish", publish_as_website: 'Publish as website', @@ -172,6 +183,7 @@ const en = { replace_all: 'Replace All', resend_confirmation_code: "Re-send Confirmation Code", reset_colors: "Reset Colors", + restart_puter_confirm: "Are you sure you want to restart Puter?", restore: "Restore", saturation: 'Saturation', save_account: 'Save account', @@ -203,6 +215,10 @@ const en = { storage_usage: "Storage Usage", storage_puter_used: 'used by Puter', taking_longer_than_usual: 'Taking a little longer than usual. Please wait...', + task_manager: "Task Manager", + taskmgr_header_name: "Name", + taskmgr_header_status: "Status", + taskmgr_header_type: "Type", terms: "Terms", text_document: 'Text document', tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's {{link=terms}}Terms of Service{{/link}} and {{link=privacy}}Privacy Policy{{/link}}.`, diff --git a/src/initgui.js b/src/initgui.js index 9bf42784..3d16c69b 100644 --- a/src/initgui.js +++ b/src/initgui.js @@ -36,6 +36,9 @@ import PuterDialog from './UI/PuterDialog.js'; import determine_active_container_parent from './helpers/determine_active_container_parent.js'; import { ThemeService } from './services/ThemeService.js'; import { BroadcastService } from './services/BroadcastService.js'; +import UIWindowTaskManager from './UI/UIWindowTaskManager.js'; +import { ProcessService } from './services/ProcessService.js'; +import { PROCESS_RUNNING } from './definitions.js'; const launch_services = async function () { const services_l_ = []; @@ -51,10 +54,17 @@ const launch_services = async function () { register('broadcast', new BroadcastService()); register('theme', new ThemeService()); + register('process', new ProcessService()) for (const [_, instance] of services_l_) { await instance._init(); } + + // Set init process status + { + const svc_process = globalThis.services.get('process'); + svc_process.get_init().chstatus(PROCESS_RUNNING); + } }; window.initgui = async function(){ diff --git a/src/services/ProcessService.js b/src/services/ProcessService.js new file mode 100644 index 00000000..8047083c --- /dev/null +++ b/src/services/ProcessService.js @@ -0,0 +1,70 @@ +import { InitProcess, Service } from "../definitions.js"; + +// The NULL UUID is also the UUID for the init process. +const NULL_UUID = '00000000-0000-0000-0000-000000000000'; + +export class ProcessService extends Service { + async _init () { + this.processes = []; + this.processes_map = new Map(); + this.uuid_to_treelist = new Map(); + + const root = new InitProcess({ + uuid: NULL_UUID, + }); + this.register_(root); + } + + get_init () { + return this.processes_map.get(NULL_UUID); + } + + get_children_of (uuid) { + if ( ! uuid ) { + uuid = NULL_UUID; + } + + return this.uuid_to_treelist.get(uuid); + } + + register (process) { + this.register_(process); + this.attach_to_parent_(process); + } + + register_ (process) { + this.processes.push(process); + this.processes_map.set(process.uuid, process); + this.uuid_to_treelist.set(process.uuid, []); + } + + attach_to_parent_ (process) { + process.parent = process.parent ?? NULL_UUID; + const parent_list = this.uuid_to_treelist.get(process.parent); + parent_list.push(process); + } + + unregister (uuid) { + const process = this.processes_map.get(uuid); + if ( ! process ) { + throw new Error(`Process with uuid ${uuid} not found`); + } + + this.processes_map.delete(uuid); + this.processes.splice(this.processes.indexOf(process), 1); + + const parent_list = this.uuid_to_treelist.get(process.parent); + parent_list.splice(parent_list.indexOf(process), 1); + + const children = this.uuid_to_treelist.get(process.uuid); + + delete this.uuid_to_treelist[process.uuid]; + this.processes.splice(this.processes.indexOf(process), 1); + + // Transfer children to init process + for ( const child of children ) { + child.parent = NULL_UUID; + this.attach_to_parent_(child); + } + } +}