diff --git a/.github/workflows/docker-image.yaml b/.github/workflows/docker-image.yaml new file mode 100644 index 00000000..ba1aa436 --- /dev/null +++ b/.github/workflows/docker-image.yaml @@ -0,0 +1,79 @@ +# +name: Docker Image CI + +# Configures this workflow to run every time a change is pushed to the +# branch called `main`. +on: + push: + branches: ['main'] + + +# Defines two custom environment variables for the workflow. These are used +# for the Container registry domain, and a name for the Docker image that +# this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the +# latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions + # in this job. + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Uses the `docker/login-action` action to log in to the Container + # registry using the account and password that will publish the packages. + # Once published, the packages are scoped to the account defined here. + - name: Log in to GitHub Package Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) + # to extract tags and labels that will be applied to the specified image. + # The `id` "meta" allows the output of this step to be referenced in + # a subsequent step. The `images` value provides the base name for the + # tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=schedule + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} + type=sha + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + + # This step uses the `docker/build-push-action` action to build the + # image, based on your repository's `Dockerfile`. If the build succeeds, + # it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the + # set of files located in the specified path. For more information, see + # "[Usage](https://github.com/docker/build-push-action#usage)" in the + # README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image + # with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/dev-server.js b/dev-server.js index 30f149d1..2add5966 100644 --- a/dev-server.js +++ b/dev-server.js @@ -10,15 +10,14 @@ let port = process.env.PORT ?? 4000; // Starting port const maxAttempts = 10; // Maximum number of ports to try const env = argv[2] ?? "dev"; -const startServer = (attempt) => { +const startServer = (attempt, useAnyFreePort = false) => { if (attempt > maxAttempts) { - console.error(chalk.red(`ERROR: Unable to find an available port after ${maxAttempts} attempts.`)); - return; + useAnyFreePort = true; // Use any port that is free } - app.listen(port, () => { + const server = app.listen(useAnyFreePort ? 0 : port, () => { console.log("\n-----------------------------------------------------------\n"); - console.log(`Puter is now live at: `, chalk.underline.blue(`http://localhost:${port}`)); + console.log(`Puter is now live at: `, chalk.underline.blue(`http://localhost:${server.address().port}`)); console.log("\n-----------------------------------------------------------\n"); }).on('error', (err) => { if (err.code === 'EADDRINUSE') { // Check if the error is because the port is already in use diff --git a/incubator/x86emu/README.md b/incubator/x86emu/README.md new file mode 100644 index 00000000..e74e444d --- /dev/null +++ b/incubator/x86emu/README.md @@ -0,0 +1,17 @@ +# Research + Planning for x86 Emulation in Puter + +## Resources +- [copy.sh/v86 docs](https://github.com/copy/v86/blob/master/docs) +- [greenfield github](https://github.com/udevbe/greenfield) + +## TODO + +### Documents to Write + +- [ ] specification for Puter network driver +- [ ] specification for Puter network relay + +### Things to Try + +- [ ] greenfield/wayland/arch/v86 +- [ ] puter-fuse in v86 diff --git a/src/IPC.js b/src/IPC.js index 18335d90..5520ee00 100644 --- a/src/IPC.js +++ b/src/IPC.js @@ -333,7 +333,6 @@ window.addEventListener('message', async (event) => { initiating_app_uuid: app_uuid, }); } - //-------------------------------------------------------- // setWindowTitle //-------------------------------------------------------- @@ -347,6 +346,98 @@ window.addEventListener('message', async (event) => { }, '*'); } //-------------------------------------------------------- + // setWindowWidth + //-------------------------------------------------------- + else if(event.data.msg === 'setWindowWidth' && event.data.width !== undefined){ + event.data.width = parseFloat(event.data.width); + // must be at least 200 + if(event.data.width < 200) + event.data.width = 200; + // set window width + $($el_parent_window).css('width', event.data.width); + // send confirmation to requester window + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + }, '*'); + } + //-------------------------------------------------------- + // setWindowHeight + //-------------------------------------------------------- + else if(event.data.msg === 'setWindowHeight' && event.data.height !== undefined){ + event.data.height = parseFloat(event.data.height); + // must be at least 200 + if(event.data.height < 200) + event.data.height = 200; + + // convert to number and set + $($el_parent_window).css('height', event.data.height); + + // send confirmation to requester window + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + }, '*'); + } + //-------------------------------------------------------- + // setWindowSize + //-------------------------------------------------------- + else if(event.data.msg === 'setWindowSize' && (event.data.width !== undefined || event.data.height !== undefined)){ + // convert to number and set + if(event.data.width !== undefined){ + event.data.width = parseFloat(event.data.width); + // must be at least 200 + if(event.data.width < 200) + event.data.width = 200; + $($el_parent_window).css('width', event.data.width); + } + + if(event.data.height !== undefined){ + event.data.height = parseFloat(event.data.height); + // must be at least 200 + if(event.data.height < 200) + event.data.height = 200; + $($el_parent_window).css('height', event.data.height); + } + + // send confirmation to requester window + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + }, '*'); + } + //-------------------------------------------------------- + // setWindowPosition + //-------------------------------------------------------- + else if(event.data.msg === 'setWindowPosition' && (event.data.x !== undefined || event.data.y !== undefined)){ + // convert to number and set + if(event.data.x !== undefined){ + event.data.x = parseFloat(event.data.x); + // we don't want the window to go off the left edge of the screen + if(event.data.x < 0) + event.data.x = 0; + // we don't want the window to go off the right edge of the screen + if(event.data.x > window.innerWidth - 100) + event.data.x = window.innerWidth - 100; + // set window left + $($el_parent_window).css('left', parseFloat(event.data.x)); + } + + if(event.data.y !== undefined){ + event.data.y = parseFloat(event.data.y); + // we don't want the window to go off the top edge of the screen + if(event.data.y < window.taskbar_height) + event.data.y = window.taskbar_height; + // we don't want the window to go off the bottom edge of the screen + if(event.data.y > window.innerHeight - 100) + event.data.y = window.innerHeight - 100; + // set window top + $($el_parent_window).css('top', parseFloat(event.data.y)); + } + + // send confirmation to requester window + target_iframe.contentWindow.postMessage({ + original_msg_id: msg_id, + }, '*'); + } + //-------------------------------------------------------- // watchItem //-------------------------------------------------------- else if(event.data.msg === 'watchItem' && event.data.item_uid !== undefined){ diff --git a/src/UI/PuterDialog.js b/src/UI/PuterDialog.js index 648ad0c2..21ffc822 100644 --- a/src/UI/PuterDialog.js +++ b/src/UI/PuterDialog.js @@ -26,11 +26,11 @@ async function PuterDialog(options) {

This website uses Puter to bring you safe, secure, and private AI and Cloud features.

- - + +
-

Powered by Puter.js

-

By clicking 'Continue' you agree to Puter's Terms of Service and Privacy Policy.

+

${i18n('powered_by_puter_js')}

+

${i18n('tos_fineprint')}

`; const el_window = await UIWindow({ diff --git a/src/UI/UIContextMenu.js b/src/UI/UIContextMenu.js index a6168eda..20491ed3 100644 --- a/src/UI/UIContextMenu.js +++ b/src/UI/UIContextMenu.js @@ -145,48 +145,70 @@ function UIContextMenu(options){ $(contextMenu).remove(); }); } - return false; }); - // when mouse is over an item - $(contextMenu).find('.context-menu-item').on('mouseover', function (e) { - // mark other items as inactive - $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); - // mark this item as active - $(this).addClass('context-menu-item-active'); - // close any submenu that doesn't belong to this item - $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); - // mark this context menu as active - $(contextMenu).addClass('context-menu-active'); - }) + // initialize menuAim plugin (../libs/jquery.menu-aim.js) + $(contextMenu).menuAim({ + submenuDirection: function(){ + //if not submenu + if(!options.is_submenu){ + // if submenu left postiton is greater than main menu left position + if($(contextMenu).offset().left + 2 * $(contextMenu).width() + 15 < window.innerWidth ){ + return "right"; + } else { + return "left"; + } + } + }, + //activates item when mouse enters depending in mouse position and direction + activate: function (e) { - // open submenu if applicable - $(`#context-menu-${menu_id} > li.context-menu-item-submenu`).on('mouseover', function (e) { - - // open submenu only if it's not already open - if($(`.context-menu[data-id="${menu_id}-${$(this).attr('data-action')}"]`).length === 0){ - let item_rect_box = this.getBoundingClientRect(); - - // close other submenus - $(`.context-menu[parent-element-id="${menu_id}"]`).remove(); + //activate items + let item = $(e).closest('.context-menu-item'); - // open the new submenu - UIContextMenu({ - items: options.items[parseInt($(this).attr('data-action'))].items, - parent_id: menu_id, - is_submenu: true, - id: menu_id + '-' + $(this).attr('data-action'), - position:{ - top: item_rect_box.top - 5, - left: x_pos + item_rect_box.width + 15, - } - }) + // mark other items as inactive + $(contextMenu).find('.context-menu-item').removeClass('context-menu-item-active'); + // mark this item as active + $(item).addClass('context-menu-item-active'); + // close any submenu that doesn't belong to this item + $(`.context-menu[data-parent-id="${menu_id}"]`).remove(); + // mark this context menu as active + $(contextMenu).addClass('context-menu-active'); + + + // activate submenu + // open submenu if applicable + if($(e).hasClass('context-menu-item-submenu')){ + let item_rect_box = e.getBoundingClientRect(); + // open submenu only if it's not already open + if($(`.context-menu[data-id="${menu_id}-${$(e).attr('data-action')}"]`).length === 0){ + // close other submenus + $(`.context-menu[parent-element-id="${menu_id}"]`).remove(); + // open the new submenu + UIContextMenu({ + items: options.items[parseInt($(e).attr('data-action'))].items, + parent_id: menu_id, + is_submenu: true, + id: menu_id + '-' + $(e).attr('data-action'), + position:{ + top: item_rect_box.top - 5, + left: x_pos + item_rect_box.width + 15, + } + }) + } + } + }, + //deactivates row when mouse leavess + deactivate: function (e) { + //deactivate submenu + if($(e).hasClass('context-menu-item-submenu')){ + $(`.context-menu[data-id="${menu_id}-${$(e).attr('data-action')}"]`).remove(); + } } - return false; }); - - // useful in cases such as where a menue item is over a window, this prevents from the mousedown event + + // useful in cases such as where a menu item is over a window, this prevents from the mousedown event // reaching the window underneath $(`#context-menu-${menu_id} > li:not(.context-menu-item-disabled)`).on('mousedown', function (e) { e.preventDefault(); @@ -227,4 +249,6 @@ window.select_ctxmenu_item = function ($ctxmenu_item){ $($ctxmenu_item).addClass('context-menu-item-active'); } -export default UIContextMenu; \ No newline at end of file +export default UIContextMenu; + + diff --git a/src/UI/UIDesktop.js b/src/UI/UIDesktop.js index adc48ed0..c5732a8a 100644 --- a/src/UI/UIDesktop.js +++ b/src/UI/UIDesktop.js @@ -33,6 +33,7 @@ import UIWindowLogin from "./UIWindowLogin.js" import UIWindowQR from "./UIWindowQR.js" import UIWindowRefer from "./UIWindowRefer.js" import UITaskbar from "./UITaskbar.js" +import new_context_menu_item from "../helpers/new_context_menu_item.js" async function UIDesktop(options){ let h = ''; @@ -621,7 +622,7 @@ async function UIDesktop(options){ // Sort by // ------------------------------------------- { - html: "Sort by", + html: i18n('sort_by'), items: [ { html: `Auto Arrange`, @@ -645,6 +646,7 @@ async function UIDesktop(options){ { html: `Name`, disabled: !is_auto_arrange_enabled, + html: i18n('name'), icon: $(el_desktop).attr('data-sort_by') === 'name' ? '✓' : '', onClick: async function(){ sort_items(el_desktop, 'name', $(el_desktop).attr('data-sort_order')); @@ -654,6 +656,7 @@ async function UIDesktop(options){ { html: `Date modified`, disabled: !is_auto_arrange_enabled, + html: i18n('date_modified'), icon: $(el_desktop).attr('data-sort_by') === 'modified' ? '✓' : '', onClick: async function(){ sort_items(el_desktop, 'modified', $(el_desktop).attr('data-sort_order')); @@ -663,6 +666,7 @@ async function UIDesktop(options){ { html: `Type`, disabled: !is_auto_arrange_enabled, + html: i18n('type'), icon: $(el_desktop).attr('data-sort_by') === 'type' ? '✓' : '', onClick: async function(){ sort_items(el_desktop, 'type', $(el_desktop).attr('data-sort_order')); @@ -672,6 +676,7 @@ async function UIDesktop(options){ { html: `Size`, disabled: !is_auto_arrange_enabled, + html: i18n('size'), icon: $(el_desktop).attr('data-sort_by') === 'size' ? '✓' : '', onClick: async function(){ sort_items(el_desktop, 'size', $(el_desktop).attr('data-sort_order')); @@ -685,6 +690,7 @@ async function UIDesktop(options){ { html: `Ascending`, disabled: !is_auto_arrange_enabled, + html: i18n('ascending'), icon: $(el_desktop).attr('data-sort_order') === 'asc' ? '✓' : '', onClick: async function(){ const sort_by = $(el_desktop).attr('data-sort_by') @@ -695,6 +701,7 @@ async function UIDesktop(options){ { html: `Descending`, disabled: !is_auto_arrange_enabled, + html: i18n('descending'), icon: $(el_desktop).attr('data-sort_order') === 'desc' ? '✓' : '', onClick: async function(){ const sort_by = $(el_desktop).attr('data-sort_by') @@ -708,7 +715,7 @@ async function UIDesktop(options){ // Refresh // ------------------------------------------- { - html: "Refresh", + html: i18n('refresh'), onClick: function(){ refresh_item_container(el_desktop); } @@ -717,7 +724,8 @@ async function UIDesktop(options){ // Show/Hide hidden files // ------------------------------------------- { - html: `${window.user_preferences.show_hidden_files ? 'Hide' : 'Show'} hidden files`, + html: i18n('show_hidden'), + icon: window.user_preferences.show_hidden_files ? '✓' : '', onClick: function(){ window.mutate_user_preferences({ show_hidden_files : !window.user_preferences.show_hidden_files, @@ -732,7 +740,7 @@ async function UIDesktop(options){ // ------------------------------------------- // New File // ------------------------------------------- - window.new_context_menu_item(desktop_path, el_desktop), + new_context_menu_item(desktop_path, el_desktop), // ------------------------------------------- // - // ------------------------------------------- @@ -741,7 +749,7 @@ async function UIDesktop(options){ // Paste // ------------------------------------------- { - html: "Paste", + html: i18n('paste'), disabled: clipboard.length > 0 ? false : true, onClick: function(){ if(clipboard_op === 'copy') @@ -754,7 +762,7 @@ async function UIDesktop(options){ // Undo // ------------------------------------------- { - html: "Undo", + html: i18n('undo'), disabled: actions_history.length > 0 ? false : true, onClick: function(){ undo_last_action(); @@ -764,21 +772,12 @@ async function UIDesktop(options){ // Upload Here // ------------------------------------------- { - html: "Upload Here", + html: i18n('upload_here'), onClick: function(){ init_upload_using_dialog(el_desktop); } }, // ------------------------------------------- - // Request Files - // ------------------------------------------- - // { - // html: "Request Files", - // onClick: function(){ - // UIWindowRequestFiles({dir_path: desktop_path}) - // } - // }, - // ------------------------------------------- // - // ------------------------------------------- '-', @@ -786,7 +785,7 @@ async function UIDesktop(options){ // Change Desktop Background… // ------------------------------------------- { - html: "Change Desktop Background…", + html: i18n('change_desktop_background'), onClick: function(){ UIWindowDesktopBGSettings(); } @@ -955,6 +954,7 @@ async function UIDesktop(options){ name: app_launched_from_url, readURL: qparams.get('readURL'), maximized: qparams.get('maximized'), + params: app_query_params ?? [], is_fullpage: window.is_fullpage_mode, window_options: { stay_on_top: false, @@ -1159,7 +1159,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ // My Websites //-------------------------------------------------- { - html: "My Websites", + html: i18n('my_websites'), onClick: async function(){ UIWindowMyWebsites(); } @@ -1168,7 +1168,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ // Change Username //-------------------------------------------------- { - html: "Change Username", + html: i18n('change_username'), onClick: async function(){ UIWindowChangeUsername(); } @@ -1178,7 +1178,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ // Change Password //-------------------------------------------------- { - html: "Change Password", + html: i18n('change_password'), onClick: async function(){ UIWindowChangePassword(); } @@ -1187,7 +1187,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ // Contact Us //-------------------------------------------------- { - html: "Contact Us", + html: i18n('contact_us'), onClick: async function(){ UIWindowFeedback(); } @@ -1201,7 +1201,7 @@ $(document).on('click', '.user-options-menu-btn', async function(e){ // Log Out //-------------------------------------------------- { - html: "Log Out", + html: i18n('log_out'), onClick: async function(){ // see if there are any open windows, if yes notify user if($('.window-app').length > 0){ diff --git a/src/UI/UIItem.js b/src/UI/UIItem.js index 343ae553..6780145f 100644 --- a/src/UI/UIItem.js +++ b/src/UI/UIItem.js @@ -760,7 +760,7 @@ function UIItem(options){ // ------------------------------------------- if(are_trashed){ menu_items.push({ - html: "Restore", + html: i18n('restore'), onClick: function(){ $selected_items.each(function() { const ell = this; @@ -779,7 +779,7 @@ function UIItem(options){ // Donwload // ------------------------------------------- menu_items.push({ - html: 'Download', + html: i18n('Download'), onClick: async function(){ let items = []; for (let index = 0; index < $selected_items.length; index++) { @@ -793,7 +793,7 @@ function UIItem(options){ // Zip // ------------------------------------------- menu_items.push({ - html: 'Zip', + html: i18n('zip'), onClick: async function(){ let items = []; for (let index = 0; index < $selected_items.length; index++) { @@ -812,7 +812,7 @@ function UIItem(options){ // Cut // ------------------------------------------- menu_items.push({ - html: "Cut", + html: i18n('cut'), onClick: function(){ window.clipboard_op= 'move'; window.clipboard = []; @@ -828,7 +828,7 @@ function UIItem(options){ // ------------------------------------------- if(!are_trashed){ menu_items.push({ - html: "Copy", + html: i18n('copy'), onClick: function(){ window.clipboard_op= 'copy'; window.clipboard = []; @@ -848,7 +848,7 @@ function UIItem(options){ // ------------------------------------------- if(are_trashed){ menu_items.push({ - html: 'Delete Permanently', + html: i18n('delete_permanently'), onClick: async function(){ const alert_resp = await UIAlert({ message: `Are you sure you want to permanently delete these items?`, @@ -887,7 +887,7 @@ function UIItem(options){ // ------------------------------------------- if(!are_trashed && window.feature_flags.create_shortcut){ menu_items.push({ - html: 'Create Shortcut', + html: i18n('create_shortcut'), onClick: async function(){ $selected_items.each(function() { let base_dir = path.dirname($(this).attr('data-path')); @@ -913,7 +913,7 @@ function UIItem(options){ // ------------------------------------------- if(!are_trashed){ menu_items.push({ - html: 'Delete', + html: i18n('delete'), onClick: async function(){ move_items($selected_items, trash_path); } @@ -933,7 +933,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trashed){ menu_items.push({ - html: 'Open', + html: i18n('open'), onClick: function(){ open_item({item: el_item}); } @@ -989,7 +989,7 @@ function UIItem(options){ } // add all suitable apps menu_items.push({ - html: 'Open With', + html: i18n('open_with'), items: items, }); @@ -1005,7 +1005,7 @@ function UIItem(options){ // ------------------------------------------- if($(el_item).closest('.window-body').length > 0 && options.is_dir){ menu_items.push({ - html: 'Open in New Window', + html: i18n('open_in_new_window'), onClick: function(){ if(options.is_dir){ open_item({item: el_item, new_window: true}) @@ -1024,7 +1024,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trashed && !is_trash && options.is_dir){ menu_items.push({ - html: 'Publish As Website', + html: i18n('publish_as_website'), disabled: !options.is_dir, onClick: async function () { if(window.require_email_verification_to_publish_website){ @@ -1051,7 +1051,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trashed && !is_trash && options.is_dir){ menu_items.push({ - html: 'Deploy As App', + html: i18n('deploy_as_app'), disabled: !options.is_dir, onClick: async function () { launch_app({ @@ -1073,19 +1073,18 @@ function UIItem(options){ // ------------------------------------------- if(is_trash){ menu_items.push({ - html: 'Empty Trash', + html: i18n('empty_trash'), onClick: async function(){ empty_trash(); } }); - } // ------------------------------------------- // Donwload // ------------------------------------------- if(!is_trash && !is_trashed && (options.associated_app_name === null || options.associated_app_name === undefined)){ menu_items.push({ - html: 'Download', + html: i18n('Download'), disabled: options.is_dir && !window.feature_flags.download_directory, onClick: async function(){ if(options.is_dir) @@ -1102,11 +1101,11 @@ function UIItem(options){ // ------------------------------------------- if(!is_trashed && !is_trash && (options.associated_app_name === null || options.associated_app_name === undefined)){ menu_items.push({ - html: 'Get Copy Link', + html: i18n('get_copy_link'), onClick: async function(){ if(window.user.is_temp && !await UIWindowSaveAccount({ - message: 'Please create an account to proceed.', + message: i18n('save_account_to_get_copy_link'), send_confirmation_code: true, window_options: { backdrop: true, @@ -1131,7 +1130,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trash && !is_trashed && !$(el_item).attr('data-path').endsWith('.zip')){ menu_items.push({ - html: "Zip", + html: i18n('zip'), onClick: function(){ zipItems(el_item, path.dirname($(el_item).attr('data-path')), false); } @@ -1142,7 +1141,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trash && !is_trashed && $(el_item).attr('data-path').endsWith('.zip')){ menu_items.push({ - html: "Unzip", + html: i18n('unzip'), onClick: async function(){ const zip = new JSZip(); let filPath = $(el_item).attr('data-path'); @@ -1170,7 +1169,7 @@ function UIItem(options){ // ------------------------------------------- if(is_trashed){ menu_items.push({ - html: 'Restore', + html: i18n('restore'), onClick: async function(){ let metadata = $(el_item).attr('data-metadata') === '' ? {} : JSON.parse($(el_item).attr('data-metadata')) move_items([el_item], path.dirname(metadata.original_path)); @@ -1187,7 +1186,7 @@ function UIItem(options){ // ------------------------------------------- if($(el_item).attr('data-immutable') === '0'){ menu_items.push({ - html: "Cut", + html: i18n('cut'), onClick: function(){ window.clipboard_op= 'move'; window.clipboard= [options.path]; @@ -1199,7 +1198,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trashed && !is_trash){ menu_items.push({ - html: "Copy", + html: i18n('copy'), onClick: function(){ window.clipboard_op= 'copy'; window.clipboard= [{path: options.path}]; @@ -1211,7 +1210,7 @@ function UIItem(options){ // ------------------------------------------- if($(el_item).attr('data-is_dir') === '1' && !is_trashed && !is_trash){ menu_items.push({ - html: "Paste Into Folder", + html: i18n('paste_into_folder'), disabled: clipboard.length > 0 ? false : true, onClick: function(){ if(clipboard_op === 'copy') @@ -1232,7 +1231,7 @@ function UIItem(options){ // ------------------------------------------- if(!is_trashed && window.feature_flags.create_shortcut){ menu_items.push({ - html: 'Create Shortcut', + html: i18n('create_shortcut'), onClick: async function(){ let base_dir = path.dirname($(el_item).attr('data-path')); // Trash on Desktop is a special case @@ -1256,7 +1255,7 @@ function UIItem(options){ // ------------------------------------------- if($(el_item).attr('data-immutable') === '0' && !is_trashed){ menu_items.push({ - html: 'Delete', + html: i18n('delete'), onClick: async function(){ move_items([el_item], trash_path); } @@ -1267,7 +1266,7 @@ function UIItem(options){ // ------------------------------------------- if(is_trashed){ menu_items.push({ - html: 'Delete Permanently', + html: i18n('delete_permanently'), onClick: async function(){ const alert_resp = await UIAlert({ message: `Are you sure you want to permanently delete this item?`, @@ -1304,7 +1303,7 @@ function UIItem(options){ // ------------------------------------------- if($(el_item).attr('data-immutable') === '0' && !is_trashed && !is_trash){ menu_items.push({ - html: "Rename", + html: i18n('rename'), onClick: function(){ activate_item_name_editor(el_item) } @@ -1318,7 +1317,7 @@ function UIItem(options){ // Properties // ------------------------------------------- menu_items.push({ - html: "Properties", + html: i18n('properties'), onClick: function(){ let window_height = 500; let window_width = 450; @@ -1411,8 +1410,8 @@ $(document).on('contextmenu', '.item-has-website-url-badge', async function(e){ items: [ // Open { - html: `Open in New Tab ` , - html_active: `Open in New Tab ` , + html: `${i18n('open_in_new_tab')} ` , + html_active: `${i18n('open_in_new_tab')} ` , onClick: function(){ const website_url = $(e.target).closest('.item').attr('data-website_url'); if(website_url){ @@ -1422,7 +1421,7 @@ $(document).on('contextmenu', '.item-has-website-url-badge', async function(e){ }, // Copy Link { - html: 'Copy Link', + html: i18n('copy_link'), onClick: async function(){ const website_url = $(e.target).closest('.item').attr('data-website_url'); if(website_url){ @@ -1523,7 +1522,7 @@ window.activate_item_name_editor= function(el_item){ } // files in trash cannot be renamed, user should be notified with an Alert. else if(path.dirname($(el_item).attr('data-path')) === window.trash_path){ - UIAlert(`This item can't be renamed because it's in the trash. To rename this item, first drag it out of the Trash.`) + UIAlert(i18n('items_in_trash_cannot_be_renamed')); return; } diff --git a/src/UI/UIPrompt.js b/src/UI/UIPrompt.js index c517b427..333234ae 100644 --- a/src/UI/UIPrompt.js +++ b/src/UI/UIPrompt.js @@ -37,8 +37,8 @@ function UIPrompt(options){ // provide an 'OK' button if no buttons are provided if(!options.buttons || options.buttons.length === 0){ options.buttons = [ - {label: 'Cancel', value: false, type: 'default'}, - {label: 'OK', value: true, type: 'primary'}, + {label: i18n('Cancel'), value: false, type: 'default'}, + {label: i18n('OK'), value: true, type: 'primary'}, ] } @@ -52,8 +52,8 @@ function UIPrompt(options){ // buttons if(options.buttons && options.buttons.length > 0){ h += `
`; - h += ``; - h += ``; + h += ``; + h += ``; h += `
`; } diff --git a/src/UI/UITaskbar.js b/src/UI/UITaskbar.js index 0ab89bb8..9c2a5ac6 100644 --- a/src/UI/UITaskbar.js +++ b/src/UI/UITaskbar.js @@ -50,7 +50,7 @@ async function UITaskbar(options){ //--------------------------------------------- UITaskbarItem({ icon: window.icons['start.svg'], - name: 'Start', + name: i18n('start'), sortable: false, keep_in_taskbar: true, disable_context_menu: true, @@ -95,7 +95,7 @@ async function UITaskbar(options){ // ------------------------------------------- if(launch_apps.recent.length > 0){ // heading - apps_str += `

Recent

`; + apps_str += `

${i18n('recent')}

`; // apps apps_str += `
`; @@ -116,7 +116,7 @@ async function UITaskbar(options){ if(launch_apps.recommended.length > 0){ // heading apps_str += `

Recommended

`; - + // apps apps_str += ``; const el_window = await UIWindow({ @@ -93,7 +93,7 @@ async function UIWindowChangePassword(){ return; } else if(new_password !== confirm_new_password){ - $(el_window).find('.form-error-msg').html('`New Password` and `Confirm New Password` do not match.'); + $(el_window).find('.form-error-msg').html(i18n('passwords_do_not_match')); $(el_window).find('.form-error-msg').fadeIn(); return; } @@ -113,7 +113,7 @@ async function UIWindowChangePassword(){ new_pass: new_password, }), success: function (data){ - $(el_window).find('.form-success-msg').html('Password changed successfully.'); + $(el_window).find('.form-success-msg').html(i18n('password_changed')); $(el_window).find('.form-success-msg').fadeIn(); $(el_window).find('input').val(''); }, diff --git a/src/UI/UIWindowChangeUsername.js b/src/UI/UIWindowChangeUsername.js index 817bfef8..4fa2bb7e 100644 --- a/src/UI/UIWindowChangeUsername.js +++ b/src/UI/UIWindowChangeUsername.js @@ -30,16 +30,16 @@ async function UIWindowChangeUsername(){ h += `
`; // new username h += `
`; - h += ``; + h += ``; h += ``; h += `
`; // Change Username - h += ``; + h += ``; h += `
`; const el_window = await UIWindow({ - title: 'Change Username', + title: i18n('change_username'), app: 'change-username', single_instance: true, icon: null, @@ -78,7 +78,7 @@ async function UIWindowChangeUsername(){ const new_username = $(el_window).find('.new-username').val(); if(!new_username){ - $(el_window).find('.form-error-msg').html('All fields are required.'); + $(el_window).find('.form-error-msg').html(i18n('all_fields_required')); $(el_window).find('.form-error-msg').fadeIn(); return; } @@ -102,7 +102,7 @@ async function UIWindowChangeUsername(){ new_username: new_username, }), success: function (data){ - $(el_window).find('.form-success-msg').html('Username updated successfully.'); + $(el_window).find('.form-success-msg').html(i18n('username_changed')); $(el_window).find('.form-success-msg').fadeIn(); $(el_window).find('input').val(''); // update auth data diff --git a/src/UI/UIWindowClaimReferral.js b/src/UI/UIWindowClaimReferral.js index d7715f7c..68f558e1 100644 --- a/src/UI/UIWindowClaimReferral.js +++ b/src/UI/UIWindowClaimReferral.js @@ -26,9 +26,9 @@ async function UIWindowClaimReferral(options){ h += `
`; h += `
×
`; h += ``; - h += `

You have been referred to Puter by a friend!

`; - h += `

Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.

`; - h += ``; + h += `

${i18n('you_have_been_referred_to_puter_by_a_friend')}

`; + h += `

${i18n('confirm_account_for_free_referral_storage_c2a')}

`; + h += ``; h += `
`; const el_window = await UIWindow({ diff --git a/src/UI/UIWindowColorPicker.js b/src/UI/UIWindowColorPicker.js index a8d35121..400adbb3 100644 --- a/src/UI/UIWindowColorPicker.js +++ b/src/UI/UIWindowColorPicker.js @@ -42,13 +42,13 @@ async function UIWindowColorPicker(options){ h += ``; // Select button - h += `` + h += `` h += ``; h += ``; h += ``; const el_window = await UIWindow({ - title: 'Select color…', + title: i18n('select_color'), app: 'color-picker', single_instance: true, icon: null, diff --git a/src/UI/UIWindowConfirmDownload.js b/src/UI/UIWindowConfirmDownload.js index b3814260..b9605ad2 100644 --- a/src/UI/UIWindowConfirmDownload.js +++ b/src/UI/UIWindowConfirmDownload.js @@ -34,17 +34,17 @@ async function UIWindowConfirmDownload(options){ // Item information h += `
`; // Name - h += `

Name: ${options.name ?? options.url}

`; + h += `

${i18n('name')}: ${options.name ?? options.url}

`; // Type - h += `

Type: ${options.is_dir === '1' || options.is_dir === 'true' ? 'Folder' : options.type ?? 'Unknown File Type'}

`; + h += `

${i18n('type')}: ${options.is_dir === '1' || options.is_dir === 'true' ? 'Folder' : options.type ?? 'Unknown File Type'}

`; // Source - h += `

From: ${options.source}

`; + h += `

${i18n('from')}: ${options.source}

`; h += `
`; h += ``; // Download - h += ``; + h += ``; // Cancel - h += ``; + h += ``; h +=``; const el_window = await UIWindow({ diff --git a/src/UI/UIWindowCopyProgress.js b/src/UI/UIWindowCopyProgress.js index 738da14b..d40bda97 100644 --- a/src/UI/UIWindowCopyProgress.js +++ b/src/UI/UIWindowCopyProgress.js @@ -29,7 +29,7 @@ async function UIWindowCopyProgress(options){ // Progress report h +=`
`; // msg - h += `Copying `; + h += `${i18n('copying')} `; h += ``; h += `
`; // progress @@ -42,7 +42,7 @@ async function UIWindowCopyProgress(options){ h += ``; const el_window = await UIWindow({ - title: `Copying`, + title: i18n('copying'), icon: window.icons[`app-icon-copying.svg`], uid: null, is_dir: false, diff --git a/src/UI/UIWindowDesktopBGSettings.js b/src/UI/UIWindowDesktopBGSettings.js index 37c38318..3b0c9838 100644 --- a/src/UI/UIWindowDesktopBGSettings.js +++ b/src/UI/UIWindowDesktopBGSettings.js @@ -30,28 +30,28 @@ async function UIWindowDesktopBGSettings(){ h += `
`; // type - h += ``; + h += ``; h += ``; // Picture h += `
`; - h += ``; - h += ``; - h += ``; + h += ``; + h += ``; + h += ``; h += ``; h += `
` // Color h += `
`; - h += ``; + h += ``; h += `
`; h += `
`; h += `
`; @@ -69,14 +69,14 @@ async function UIWindowDesktopBGSettings(){ h += `
`; h += `
` - h += ``; - h += ``; + h += ``; + h += ``; h += `
`; h += `
`; const el_window = await UIWindow({ - title: 'Change Desktop Background…', + title: i18n('change_desktop_background'), icon: null, uid: null, is_dir: false, diff --git a/src/UI/UIWindowDownloadDirProg.js b/src/UI/UIWindowDownloadDirProg.js index c90f1e69..8a9ba5c9 100644 --- a/src/UI/UIWindowDownloadDirProg.js +++ b/src/UI/UIWindowDownloadDirProg.js @@ -25,7 +25,7 @@ async function UIWindowDownloadDirProg(options){ let h = ''; // Loading spinner h +=`circle anim`; - h += `

${options.defaultText ?? 'Preparing...'}

`; + h += `

${options.defaultText ?? i18n('preparing')}

`; const el_window = await UIWindow({ title: 'Instant Login!', diff --git a/src/UI/UIWindowDownloadProgress.js b/src/UI/UIWindowDownloadProgress.js index 8bfd1cf0..5503a5bf 100644 --- a/src/UI/UIWindowDownloadProgress.js +++ b/src/UI/UIWindowDownloadProgress.js @@ -29,7 +29,7 @@ async function UIWindowDownloadProgress(options){ // Progress report h +=`
`; // msg - h += `Downloading ${options.item_name ?? ''}`; + h += `${i18n('downloading')} ${options.item_name ?? ''}`; h += `
`; // Progress h += `
`; diff --git a/src/UI/UIWindowEmailConfirmationRequired.js b/src/UI/UIWindowEmailConfirmationRequired.js index 14b7aaf5..88eb9ca7 100644 --- a/src/UI/UIWindowEmailConfirmationRequired.js +++ b/src/UI/UIWindowEmailConfirmationRequired.js @@ -47,10 +47,10 @@ function UIWindowEmailConfirmationRequired(options){ h += ``; h += ``; h += `
`; - h += `Re-send Confirmation Code`; + h += `${i18n('resend_confirmation_code')}`; if(options.logout_in_footer){ h += ` • `; - h += `Log Out`; + h += `${i18n('log_out')}`; } h += `
`; h += `
`; diff --git a/src/UI/UIWindowFeedback.js b/src/UI/UIWindowFeedback.js index aaba86ce..5e0b18f7 100644 --- a/src/UI/UIWindowFeedback.js +++ b/src/UI/UIWindowFeedback.js @@ -28,18 +28,18 @@ async function UIWindowQR(options){ // success h += `
`; h += ``; - h += `

Thank you for contacting us. If you have an email associated with your account, you will hear back from us as soon as possible.

`; + h += `

${i18n('feedback_sent_confirmation')}

`; h+= `
`; // form h += `
`; - h += `

Please use the form below to send us your feedback, comments, and bug reports.

`; + h += `

${i18n('feedback_c2a')}

`; h += ``; - h += ``; + h += ``; h += `
`; h += `
`; const el_window = await UIWindow({ - title: 'Contact Us', + title: i18n('contact_us'), app: 'feedback', single_instance: true, icon: null, diff --git a/src/UI/UIWindowFontPicker.js b/src/UI/UIWindowFontPicker.js index 5e1b23b8..f4208b14 100644 --- a/src/UI/UIWindowFontPicker.js +++ b/src/UI/UIWindowFontPicker.js @@ -60,7 +60,7 @@ async function UIWindowFontPicker(options){ h += ``; // Select - h += `` + h += `` h += ``; h += ``; h += ``; diff --git a/src/UI/UIWindowGetCopyLink.js b/src/UI/UIWindowGetCopyLink.js index 8f81790e..769383f2 100644 --- a/src/UI/UIWindowGetCopyLink.js +++ b/src/UI/UIWindowGetCopyLink.js @@ -69,7 +69,11 @@ async function UIWindowGetCopyLink(options){ $(el_window).find('.window-body .downloadable-link').val(url); $(el_window).find('.window-body .share-copy-link-on-social').on('click', function(e){ - const social_links = socialLink({url: url, title: `Get a copy of '${options.name}' on Puter.com!`, description: `Get a copy of '${options.name}' on Puter.com!`}); + const social_links = socialLink({ + url: url, + title: i18n('get_a_copy_of_on_puter', options.name, false), + description: i18n('get_a_copy_of_on_puter', options.name, false), + }); let social_links_html = ``; social_links_html += `
`; diff --git a/src/UI/UIWindowItemProperties.js b/src/UI/UIWindowItemProperties.js index db9aaafe..821483b1 100644 --- a/src/UI/UIWindowItemProperties.js +++ b/src/UI/UIWindowItemProperties.js @@ -25,8 +25,8 @@ async function UIWindowItemProperties(item_name, item_path, item_uid, left, top, h += `
`; // tabs h += `
`; - h += `
General
`; - h += `
Versions
`; + h += `
${i18n('general')}
`; + h += `
${i18n('versions')}
`; h += `
`; h+= `
`; @@ -44,7 +44,7 @@ async function UIWindowItemProperties(item_name, item_path, item_uid, left, top, h += `Versions`; h += `Associated Websites`; h += ``; - h += `Access Granted To`; + h += `${i18n('access_granted_to')}`; h += ``; h += `
`; diff --git a/src/UI/UIWindowLogin.js b/src/UI/UIWindowLogin.js index c7937884..145a1f84 100644 --- a/src/UI/UIWindowLogin.js +++ b/src/UI/UIWindowLogin.js @@ -43,12 +43,12 @@ async function UIWindowLogin(options){ h += ``; // username/email h += `
`; - h += ``; + h += ``; h += ``; h += `
`; // password with conditional type based based on options.show_password h += `
`; - h += ``; + h += ``; h += ``; // show/hide icon h += ` @@ -56,15 +56,15 @@ async function UIWindowLogin(options){ `; h += `
`; // login - h += ``; + h += ``; // password recovery - h += `

Forgot password?

`; + h += `

${i18n('forgot_pass_c2a')}

`; h += ``; h += `
`; // create account link if(options.show_signup_button === undefined || options.show_signup_button){ h += `
`; - h += ``; + h += ``; h += `
`; } h += `
`; diff --git a/src/UI/UIWindowMoveProgress.js b/src/UI/UIWindowMoveProgress.js index 18cb7f9d..2b492b33 100644 --- a/src/UI/UIWindowMoveProgress.js +++ b/src/UI/UIWindowMoveProgress.js @@ -29,7 +29,7 @@ async function UIWindowMoveProgress(options){ // Progress report h +=`
`; // msg - h += `Moving `; + h += `${i18n('moving')} `; h += ``; h += `
`; // progress diff --git a/src/UI/UIWindowMyWebsites.js b/src/UI/UIWindowMyWebsites.js index e1a6c076..54397920 100644 --- a/src/UI/UIWindowMyWebsites.js +++ b/src/UI/UIWindowMyWebsites.js @@ -90,10 +90,10 @@ async function UIWindowMyWebsites(options){ h += `

`; h += `

`; h += ``; - h += `Disassociate Folder`; + h += `${i18n('disassociate_dir')}`; h += `

`; } - h += `

No directory associated with this address.

`; + h += `

${i18n('no_dir_associated_with_site')}

`; h += ``; } $(el_window).find('.window-body').html(h); @@ -105,7 +105,7 @@ async function UIWindowMyWebsites(options){ margin-bottom: 50px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - color: #596c7c;">You haven't published any websites!

`); + color: #596c7c;">${i18n('no_websites_published')}

`); } }, Date.now() - init_ts < 1000 ? 0 : 2000); }) @@ -136,10 +136,11 @@ $(document).on('click', '.mywebsites-site-setting', function(e){ html: `Release Address`, onClick: async function(){ const alert_resp = await UIAlert({ - message: `Are you sure you want to release this address?`, + message: i18n('release_address_confirmation'), buttons:[ { - label: 'Yes, Release It', + label: i18n('yes_release_it'), + value: 'yes', type: 'primary', }, { @@ -147,7 +148,7 @@ $(document).on('click', '.mywebsites-site-setting', function(e){ }, ] }) - if(alert_resp !== 'Yes, Release It'){ + if(alert_resp !== 'yes'){ return; } diff --git a/src/UI/UIWindowNewFolderProgress.js b/src/UI/UIWindowNewFolderProgress.js index 3f01e8af..0f04d7db 100644 --- a/src/UI/UIWindowNewFolderProgress.js +++ b/src/UI/UIWindowNewFolderProgress.js @@ -29,7 +29,7 @@ async function UIWindowNewFolderProgress(options){ // message h +=`
`; // text - h += `Taking a little longer than usual. Please wait...`; + h += `${i18n('taking_longer_than_usual')}`; h += `
`; h +=``; h += ``; diff --git a/src/UI/UIWindowNewPassword.js b/src/UI/UIWindowNewPassword.js index 15dc3b0a..204c0771 100644 --- a/src/UI/UIWindowNewPassword.js +++ b/src/UI/UIWindowNewPassword.js @@ -34,17 +34,17 @@ async function UIWindowNewPassword(options){ h += `
`; // new password h += `
`; - h += ``; + h += ``; h += ``; h += `
`; // confirm new password h += `
`; - h += ``; + h += ``; h += ``; h += `
`; // Change Password - h += ``; + h += ``; h += ``; const el_window = await UIWindow({ diff --git a/src/UI/UIWindowProgressEmptyTrash.js b/src/UI/UIWindowProgressEmptyTrash.js index e771fe78..b8efabd7 100644 --- a/src/UI/UIWindowProgressEmptyTrash.js +++ b/src/UI/UIWindowProgressEmptyTrash.js @@ -29,13 +29,13 @@ async function UIWindowProgressEmptyTrash(options){ // message h +=`
`; // text - h += `Emptying the Trash...`; + h += `${i18n('emptying_trash')}`; h += `
`; h +=``; h += ``; const el_window = await UIWindow({ - title: `Creating New Folder`, + title: i18n('emptying_trash'), icon: window.icons[`app-icon-newfolder.svg`], uid: null, is_dir: false, diff --git a/src/UI/UIWindowPublishWebsite.js b/src/UI/UIWindowPublishWebsite.js index f5a0ffa4..a0dbb9cc 100644 --- a/src/UI/UIWindowPublishWebsite.js +++ b/src/UI/UIWindowPublishWebsite.js @@ -26,7 +26,7 @@ async function UIWindowPublishWebsite(target_dir_uid, target_dir_name, target_di // success h += `
`; h += ``; - h += `

${target_dir_name} has been published to:

`; + h += `

${i18n('dir_published_as_website', `${target_dir_name}`)}

`; h += `

`; h += ``; h+= `
`; @@ -36,13 +36,13 @@ async function UIWindowPublishWebsite(target_dir_uid, target_dir_name, target_di h += `
`; // subdomain h += `
`; - h += ``; + h += ``; h += `
https://.${window.hosting_domain}
`; h += `
`; // uid h += ``; // Publish - h += `` + h += `` h += ``; h += ``; diff --git a/src/UI/UIWindowQR.js b/src/UI/UIWindowQR.js index b66136f7..b69c7a9e 100644 --- a/src/UI/UIWindowQR.js +++ b/src/UI/UIWindowQR.js @@ -27,7 +27,7 @@ async function UIWindowQR(options){ // close button containing the multiplication sign h += `
×
`; h += `
`; - h += `

Scan the code below to log into this session from other devices

`; + h += `

${i18n('scan_qr_c2a')}

`; h += `
`; const el_window = await UIWindow({ diff --git a/src/UI/UIWindowRecoverPassword.js b/src/UI/UIWindowRecoverPassword.js index 8e37acef..fd6591f5 100644 --- a/src/UI/UIWindowRecoverPassword.js +++ b/src/UI/UIWindowRecoverPassword.js @@ -26,13 +26,13 @@ function UIWindowRecoverPassword(options){ let h = ''; h += `
`; - h += `

Recover Password

`; + h += `

${i18n('recover_password')}

`; h += `
`; h += `

`; h += `
`; - h += ``; + h += ``; h += ``; - h += ``; + h += ``; h += `
`; h += `
`; diff --git a/src/UI/UIWindowRefer.js b/src/UI/UIWindowRefer.js index b2fa78bd..aba0bc0e 100644 --- a/src/UI/UIWindowRefer.js +++ b/src/UI/UIWindowRefer.js @@ -29,8 +29,8 @@ async function UIWindowRefer(options){ h += `
`; h += `
×
`; h += ``; - h += `

Get 1 GB for every friend who creates and confirms an account on Puter. Your friend will get 1 GB too!

`; - h += ``; + h += `

${i18n('refer_friends_c2a')}

`; + h += ``; h += ``; h += `` h += ``; @@ -72,11 +72,11 @@ async function UIWindowRefer(options){ $(el_window).find('.window-body .downloadable-link').val(url); $(el_window).find('.window-body .share-copy-link-on-social').on('click', function(e){ - const social_links = socialLink({url: url, title: `Get 1 GB of free storage on Puter.com!`, description: `Get 1 GB of free storage on Puter.com!`}); + const social_links = socialLink({url: url, title: i18n('refer_friends_social_media_c2a'), description: i18n('refer_friends_social_media_c2a')}); let social_links_html = ``; social_links_html += `
`; - social_links_html += `

Share to

` + social_links_html += `

${i18n('share_to')}

` social_links_html += `` social_links_html += `` social_links_html += `` diff --git a/src/UI/UIWindowSaveAccount.js b/src/UI/UIWindowSaveAccount.js index 2c5a367c..3a57ba9d 100644 --- a/src/UI/UIWindowSaveAccount.js +++ b/src/UI/UIWindowSaveAccount.js @@ -32,39 +32,39 @@ async function UIWindowSaveAccount(options){ // success h += ``; // form h += ``; h += `
`; diff --git a/src/UI/UIWindowSessionList.js b/src/UI/UIWindowSessionList.js index 640567fc..fb128452 100644 --- a/src/UI/UIWindowSessionList.js +++ b/src/UI/UIWindowSessionList.js @@ -28,16 +28,16 @@ async function UIWindowSessionList(options){ return new Promise(async (resolve) => { let h = ''; h += `
`; - h += `
Signing in...
` + h += `
${i18n('signing_in')}
` h += `
`; - h += `

Sign in with Puter

` + h += `

${i18n('sign_in_with_puter')}

` for (let index = 0; index < logged_in_users.length; index++) { const l_user = logged_in_users[index]; h += `
${l_user.username}
`; } h += `
`; - h += `
`; + h += `
`; h += `
`; const el_window = await UIWindow({ diff --git a/src/UI/UIWindowSignup.js b/src/UI/UIWindowSignup.js index 5c0e25c4..fce5df86 100644 --- a/src/UI/UIWindowSignup.js +++ b/src/UI/UIWindowSignup.js @@ -40,39 +40,39 @@ function UIWindowSignup(options){ // Form h += `
`; // title - h += `

Create Free Account

`; + h += `

${i18n('create_free_account')}

`; // signup form h += ``; h += `
`; // login link // create account link h += `
`; - h += ``; + h += ``; h += `
`; h += `
`; diff --git a/src/UI/UIWindowUploadProgress.js b/src/UI/UIWindowUploadProgress.js index 1612c93b..e436fc74 100644 --- a/src/UI/UIWindowUploadProgress.js +++ b/src/UI/UIWindowUploadProgress.js @@ -29,19 +29,19 @@ async function UIWindowUploadProgress(options){ // Progress report h +=`
`; // msg - h += `Preparing for upload...`; + h += `${i18n('preparing_for_upload')}`; h += `
`; // progress h += `
`; h += `
`; h += `
`; // cancel - h += ``; + h += ``; h +=``; h += ``; const el_window = await UIWindow({ - title: `Upload`, + title: i18n('Upload'), icon: window.icons[`app-icon-uploader.svg`], uid: null, is_dir: false, diff --git a/src/globals.js b/src/globals.js index 4f7864e6..e0fed446 100644 --- a/src/globals.js +++ b/src/globals.js @@ -141,12 +141,6 @@ if (window.location !== window.parent.location) { window.desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height; window.desktop_width = window.innerWidth; -// recalculate desktop height and width on window resize -$( window ).on( "resize", function() { - window.desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height; - window.desktop_width = window.innerWidth; -}); - // for now `active_element` is basically the last element that was clicked, // later on though (todo) `active_element` will also be set by keyboard movements // such as arrow keys, tab key, ... and when creating new windows... diff --git a/src/helpers.js b/src/helpers.js index 55133cff..fede2949 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -409,17 +409,17 @@ window.globToRegExp = function (glob, opts) { */ window.validate_fsentry_name = function(name){ if(!name) - throw {message: 'Name cannot be empty.'} + throw {message: i18n('name_cannot_be_empty')} else if(!isString(name)) - throw {message: "Name can only be a string."} + throw {message: i18n('name_must_be_string')} else if(name.includes('/')) - throw {message: "Name cannot contain the '/' character."} + throw {message: i18n('name_cannot_contain_slash')} else if(name === '.') - throw {message: "Name can not be the '.' character."}; + throw {message: i18n('name_cannot_contain_period')}; else if(name === '..') - throw {message: "Name can not be the '..' character."}; + throw {message: i18n('name_cannot_contain_double_period')}; else if(name.length > window.max_item_name_length) - throw {message: `Name can not be longer than ${config.max_item_name_length} characters`} + throw {message: i18n('name_too_long', config.max_item_name_length)} else return true } @@ -1906,6 +1906,7 @@ window.launch_app = async (options)=>{ // add app_instance_id to URL iframe_url.searchParams.append('puter.app_instance_id', uuid); + // add app_id to URL iframe_url.searchParams.append('puter.app.id', app_info.uuid); @@ -1941,6 +1942,7 @@ window.launch_app = async (options)=>{ else if(options.token){ iframe_url.searchParams.append('puter.auth.token', options.token); } + // Try to acquire app token from the server else{ let response = await fetch(window.api_origin + "/auth/get-user-app-token", { @@ -1979,13 +1981,6 @@ window.launch_app = async (options)=>{ window_class: 'window-app', update_window_url: true, app_uuid: app_info.uuid ?? app_info.uid, - // has_head: options.has_head ?? true, - // top: options.top ?? undefined, - // left: options.left ?? undefined, - // width: options.width ?? undefined, - // height: options.height ?? undefined, - // is_resizable: options.is_resizable ?? undefined, - // window_css: options.window_css ?? undefined, top: options.maximized ? 0 : undefined, left: options.maximized ? 0 : undefined, height: options.maximized ? `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)` : undefined, @@ -2243,63 +2238,6 @@ window.open_item = async function(options){ } } -/** - * Returns a context menu item to create a new file/folder. - * - * @param {string} dirname - The directory path to create the item in - * @param {HTMLElement} append_to_element - Element to append the new item to - * @returns {Object} The context menu item object - */ - -window.new_context_menu_item = function(dirname, append_to_element){ - return { - html: "New", - items: [ - // New Folder - { - html: "New Folder", - icon: ``, - onClick: function(){ - create_folder(dirname, append_to_element); - } - }, - // divider - '-', - // Text Document - { - html: `Text Document`, - icon: ``, - onClick: async function(){ - create_file({dirname: dirname, append_to_element: append_to_element, name: 'New File.txt'}); - } - }, - // HTML Document - { - html: `HTML Document`, - icon: ``, - onClick: async function(){ - create_file({dirname: dirname, append_to_element: append_to_element, name: 'New File.html'}); - } - }, - // JPG Image - { - html: `JPG Image`, - icon: ``, - onClick: async function(){ - var canvas = document.createElement("canvas"); - - canvas.width = 800; - canvas.height = 600; - - canvas.toBlob((blob) =>{ - create_file({dirname: dirname, append_to_element: append_to_element, name: 'New Image.jpg', content: blob}); - }); - } - }, - ] - } -} - /** * Moves the given items to the destination path. * @@ -3127,29 +3065,6 @@ window.getUsage = () => { } -window.determine_active_container_parent = function(){ - // the container is either an ancestor of active element... - let parent_container = $(active_element).closest('.item-container'); - // ... or a descendant of it... - if(parent_container.length === 0){ - parent_container = $(active_element).find('.item-container'); - } - // ... or siblings or cousins - if(parent_container.length === 0){ - parent_container = $(active_element).closest('.window').find('.item-container'); - } - // ... or the active element itself (if it's a container) - if(parent_container.length === 0 && active_element && $(active_element).hasClass('item-container')){ - parent_container = $(active_element); - } - // ... or if there is no active element, the selected item that is not blurred - if(parent_container.length === 0 && active_item_container){ - parent_container = active_item_container; - } - - return parent_container; -} - window.getAppUIDFromOrigin = async function(origin) { try { const response = await fetch(window.api_origin + "/auth/app-uid-from-origin", { diff --git a/src/helpers/determine_active_container_parent.js b/src/helpers/determine_active_container_parent.js new file mode 100644 index 00000000..32a8388d --- /dev/null +++ b/src/helpers/determine_active_container_parent.js @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2024 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 . + */ + +const determine_active_container_parent = function(){ + // the container is either an ancestor of active element... + let parent_container = $(active_element).closest('.item-container'); + // ... or a descendant of it... + if(parent_container.length === 0){ + parent_container = $(active_element).find('.item-container'); + } + // ... or siblings or cousins + if(parent_container.length === 0){ + parent_container = $(active_element).closest('.window').find('.item-container'); + } + // ... or the active element itself (if it's a container) + if(parent_container.length === 0 && active_element && $(active_element).hasClass('item-container')){ + parent_container = $(active_element); + } + // ... or if there is no active element, the selected item that is not blurred + if(parent_container.length === 0 && active_item_container){ + parent_container = active_item_container; + } + + return parent_container; +} + +export default determine_active_container_parent; \ No newline at end of file diff --git a/src/helpers/new_context_menu_item.js b/src/helpers/new_context_menu_item.js new file mode 100644 index 00000000..05a7eeaf --- /dev/null +++ b/src/helpers/new_context_menu_item.js @@ -0,0 +1,78 @@ +/** + * Copyright (C) 2024 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 . + */ + + +/** + * Returns a context menu item to create a new folder and a variety of file types. + * + * @param {string} dirname - The directory path to create the item in + * @param {HTMLElement} append_to_element - Element to append the new item to + * @returns {Object} The context menu item object + */ + +const new_context_menu_item = function(dirname, append_to_element){ + return { + html: i18n('new'), + items: [ + // New Folder + { + html: i18n('new_folder'), + icon: ``, + onClick: function(){ + create_folder(dirname, append_to_element); + } + }, + // divider + '-', + // Text Document + { + html: i18n('text_document'), + icon: ``, + onClick: async function(){ + create_file({dirname: dirname, append_to_element: append_to_element, name: 'New File.txt'}); + } + }, + // HTML Document + { + html: i18n('html_document'), + icon: ``, + onClick: async function(){ + create_file({dirname: dirname, append_to_element: append_to_element, name: 'New File.html'}); + } + }, + // JPG Image + { + html: i18n('jpeg_image'), + icon: ``, + onClick: async function(){ + var canvas = document.createElement("canvas"); + + canvas.width = 800; + canvas.height = 600; + + canvas.toBlob((blob) =>{ + create_file({dirname: dirname, append_to_element: append_to_element, name: 'New Image.jpg', content: blob}); + }); + } + }, + ] + } +} + +export default new_context_menu_item; \ No newline at end of file diff --git a/src/i18n/i18n.js b/src/i18n/i18n.js new file mode 100644 index 00000000..167500bd --- /dev/null +++ b/src/i18n/i18n.js @@ -0,0 +1,617 @@ +window.locale = 'en'; + +window.i18n = function (key, replacements = [], encode_html = true) { + if(typeof replacements === 'boolean' && encode_html === undefined){ + encode_html = replacements; + replacements = []; + }else if(Array.isArray(replacements) === false){ + replacements = [replacements]; + } + + // if locale is not set, default to en + if(!window.translations[window.locale]) + window.locale = 'en'; + + let str = window.translations[window.locale][key]; + + if (!str) { + str = key; + } + str = encode_html ? html_encode(str) : str; + // replace %% occurrences with the values in replacements + // %% is for simple text replacements + // %strong% is for tags + // e.g. "Hello, %strong%" => "Hello, World" + // e.g. "Hello, %%" => "Hello, World" + // e.g. "Hello, %strong%, %%!" => "Hello, World, Universe!" + for (let i = 0; i < replacements.length; i++) { + // sanitize the replacement + replacements[i] = encode_html ? html_encode(replacements[i]) : replacements[i]; + // find first occurrence of %strong% + let index = str.indexOf('%strong%'); + // find first occurrence of %% + let index2 = str.indexOf('%%'); + // decide which one to replace + if (index === -1 && index2 === -1) { + break; + } else if (index === -1) { + str = str.replace('%%', replacements[i]); + } else if (index2 === -1) { + str = str.replace('%strong%', '' + replacements[i] + ''); + } else if (index < index2) { + str = str.replace('%strong%', '' + replacements[i] + ''); + } else { + str = str.replace('%%', replacements[i]); + } + } + return str; +} + +window.translations = { + en: { + access_granted_to: "Access Granted To", + add_existing_account: "Add Existing Account", + all_fields_required: 'All fields are required.', + apply: "Apply", + ascending: 'Ascending', + background: "Background", + browse: "Browse", + cancel: 'Cancel', + center: 'Center', + change_desktop_background: 'Change desktop background…', + change_password: "Change Password", + change_username: "Change Username", + close_all_windows: "Close All Windows", + color: 'Color', + confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.', + confirm_new_password: "Confirm New Password", + contact_us: "Contact Us", + contain: 'Contain', + continue: "Continue", + copy: 'Copy', + copy_link: "Copy Link", + copying: "Copying", + cover: 'Cover', + create_account: "Create Account", + create_free_account: "Create Free Account", + create_shortcut: "Create Shortcut", + current_password: "Current Password", + cut: 'Cut', + date_modified: 'Date modified', + delete: 'Delete', + delete_permanently: "Delete Permanently", + deploy_as_app: 'Deploy as app', + descending: 'Descending', + desktop_background_fit: "Fit", + dir_published_as_website: `%strong% has been published to:`, + disassociate_dir: "Disassociate Directory", + download: 'Download', + downloading: "Downloading", + email: "Email", + email_or_username: "Email or Username", + empty_trash: 'Empty Trash', + empty_trash_confirmation: `Are you sure you want to permanently delete the items in Trash?`, + emptying_trash: 'Emptying Trash…', + 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.", + forgot_pass_c2a: "Forgot password?", + from: "From", + general: "General", + get_a_copy_of_on_puter: `Get a copy of '%%' on Puter.com!`, + get_copy_link: 'Get Copy Link', + hide_all_windows: "Hide All Windows", + html_document: 'HTML document', + image: 'Image', + invite_link: "Invite Link", + items_in_trash_cannot_be_renamed: `This item can't be renamed because it's in the trash. To rename this item, first drag it out of the Trash.`, + jpeg_image: 'JPEG image', + keep_in_taskbar: 'Keep in Taskbar', + log_in: "Log In", + log_out: 'Log Out', + move: 'Move', + moving: "Moving", + my_websites: "My Websites", + name: 'Name', + name_cannot_be_empty: 'Name cannot be empty.', + name_cannot_contain_double_period: "Name can not be the '..' character.", + name_cannot_contain_period: "Name can not be the '.' character.", + name_cannot_contain_slash: "Name cannot contain the '/' character.", + name_must_be_string: "Name can only be a string.", + name_too_long: `Name can not be longer than %% characters.`, + new: 'New', + new_folder: 'New folder', + new_password: "New Password", + new_username: "New Username", + no_dir_associated_with_site: 'No directory associated with this address.', + no_websites_published: "You have not published any websites yet.", + ok: 'OK', + open: "Open", + open_in_new_tab: "Open in New Tab", + open_in_new_window: "Open in New Window", + open_with: "Open With", + password: "Password", + password_changed: "Password changed.", + passwords_do_not_match: '`New Password` and `Confirm New Password` do not match.', + paste: 'Paste', + paste_into_folder: "Paste Into Folder", + pick_name_for_website: "Pick a name for your website:", + picture: "Picture", + powered_by_puter_js: `Powered by Puter.js`, + preparing: "Preparing...", + preparing_for_upload: "Preparing for upload...", + properties: "Properties", + publish: "Publish", + publish_as_website: 'Publish as website', + recent: "Recent", + recover_password: "Recover Password", + refer_friends_c2a: "Get 1 GB for every friend who creates and confirms an account on Puter. Your friend will get 1 GB too!", + refer_friends_social_media_c2a: `Get 1 GB of free storage on Puter.com!`, + refresh: 'Refresh', + release_address_confirmation: `Are you sure you want to release this address?`, + remove_from_taskbar:'Remove from Taskbar', + rename: 'Rename', + repeat: 'Repeat', + resend_confirmation_code: "Re-send Confirmation Code", + restore: "Restore", + save_account_to_get_copy_link: "Please create an account to proceed.", + save_account_to_publish: 'Please create an account to proceed.', + save_session_c2a: 'Create an account to save your current session and avoid losing your work.', + scan_qr_c2a: 'Scan the code below to log into this session from other devices', + select: "Select", + select_color: 'Select color…', + send: "Send", + send_password_recovery_email: "Send Password Recovery Email", + session_saved: "Thank you for creating an account. This session has been saved.", + set_new_password: "Set New Password", + share_to: "Share to", + show_all_windows: "Show All Windows", + show_hidden: 'Show hidden', + sign_in_with_puter: "Sign in with Puter", + sign_up: "Sign Up", + signing_in: "Signing in…", + size: 'Size', + sort_by: 'Sort by', + start: 'Start', + taking_longer_than_usual: 'Taking a little longer than usual. Please wait...', + text_document: 'Text document', + tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's Terms of Service and Privacy Policy.`, + trash: 'Trash', + type: 'Type', + undo: 'Undo', + unzip: "Unzip", + upload: 'Upload', + upload_here: 'Upload here', + username: "Username", + username_changed: 'Username updated successfully.', + versions: "Versions", + yes_release_it: 'Yes, Release It', + you_have_been_referred_to_puter_by_a_friend: "You have been referred to Puter by a friend!", + zip: "Zip", + }, + // farsi + fa: { + access_granted_to: "دسترسی داده شده به", + add_existing_account: "افزودن حساب کاربری موجود", + all_fields_required: 'تمامی فیلدها الزامی هستند.', + apply: "اعمال", + ascending: 'صعودی', + background: "پس زمینه", + browse: "مرور", + cancel: 'لغو', + center: 'مرکز', + change_desktop_background: 'تغییر پس زمینه دسکتاپ…', + change_password: "تغییر رمز عبور", + change_username: "تغییر نام کاربری", + close_all_windows: "بستن همه پنجره ها", + color: 'رنگ', + confirm_account_for_free_referral_storage_c2a: 'حساب کاربری خود را ایجاد کرده و آدرس ایمیل خود را تأیید کنید تا 1 گیگابایت فضای ذخیره سازی رایگان دریافت کنید. دوست شما هم 1 گیگابایت فضای ذخیره سازی رایگان دریافت خواهد کرد.', + confirm_new_password: "تأیید رمز عبور جدید", + contact_us: "تماس با ما", + contain: 'شامل', + continue: "ادامه", + copy: 'کپی', + copy_link: "کپی لینک", + copying: "کپی", + cover: 'جلد', + create_account: "ایجاد حساب کاربری", + create_free_account: "ایجاد حساب کاربری رایگان", + create_shortcut: "ایجاد میانبر", + current_password: "رمز عبور فعلی", + cut: 'برش', + date_modified: 'تاریخ تغییر', + delete: 'حذف', + delete_permanently: "حذف دائمی", + deploy_as_app: 'نصب به عنوان برنامه', + descending: 'نزولی', + desktop_background_fit: "متناسب", + dir_published_as_website: `%strong% منتشر شده به:`, + disassociate_dir: "قطع ارتباط دایرکتوری", + download: 'دانلود', + downloading: "دانلود", + email: "ایمیل", + email_or_username: "ایمیل یا نام کاربری", + empty_trash: 'خالی کردن سطل زباله', + empty_trash_confirmation: `آیا از حذف دائمی موارد در سطل زباله مطمئن هستید؟`, + emptying_trash: 'خالی کردن سطل زباله…', + feedback: "بازخورد", + feedback_c2a: "لطفا از فرم زیر برای ارسال بازخورد، نظرات و گزارش خطا استفاده کنید.", + feedback_sent_confirmation: "با تشکر از تماس شما. اگر ایمیلی به حساب کاربری شما متصل است، در اسرع وقت پاسخ خواهیم داد.", + forgot_pass_c2a: "رمز عبور را فراموش کرده اید؟", + from: "از", + general: "عمومی", + get_a_copy_of_on_puter: `یک نسخه از '%%' را در Puter.com بگیرید!`, + get_copy_link: 'گرفتن لینک کپی', + hide_all_windows: "پنهان کردن همه پنجره ها", + html_document: 'سند HTML', + image: 'تصویر', + invite_link: "لینک دعوت", + items_in_trash_cannot_be_renamed: `این مورد نمی تواند تغییر نام دهد زیرا در سطل زباله است. برای تغییر نام این مورد، ابتدا آن را از سطل زباله بیرون بکشید.`, + jpeg_image: 'تصویر JPEG', + keep_in_taskbar: 'در نوار وظایف نگه دارید', + log_in: "ورود", + log_out: 'خروج', + move: 'انتقال', + moving: "انتقال", + my_websites: "وبسایت های من", + name: 'نام', + name_cannot_be_empty: 'نام نمی تواند خالی باشد.', + name_cannot_contain_double_period: "نام نمی تواند شامل '..' باشد.", + name_cannot_contain_period: "نام نمی تواند شامل '.' باشد.", + name_cannot_contain_slash: "نام نمی تواند شامل '/' باشد.", + name_must_be_string: "نام فقط می تواند یک رشته باشد.", + name_too_long: `نام نمی تواند بیشتر از %% کاراکتر باشد.`, + new: 'جدید', + new_folder: 'پوشه جدید', + new_password: "رمز عبور جدید", + new_username: "نام کاربری جدید", + no_dir_associated_with_site: 'هیچ دایرکتوری مرتبط با این آدرس وجود ندارد.', + no_websites_published: "هنوز هیچ وبسایتی منتشر نکرده اید.", + ok: 'خوب', + open: "باز کردن", + open_in_new_tab: "در تب جدید باز کن", + open_in_new_window: "در پنجره جدید باز کن", + open_with: "باز کردن با", + password: "رمز عبور", + password_changed: "رمز عبور تغییر یافت.", + passwords_do_not_match: '`رمز عبور جدید` و `تأیید رمز عبور جدید` مطابقت ندارند.', + paste: 'چسباندن', + paste_into_folder: "چسباندن در پوشه", + pick_name_for_website: "یک نام برای وبسایت خود انتخاب کنید:", + picture: "تصویر", + powered_by_puter_js: `پشتیبانی شده توسط Puter.js`, + preparing: "در حال آماده سازی...", + preparing_for_upload: "آماده سازی برای بارگذاری...", + properties: "ویژگی ها", + publish: "انتشار", + publish_as_website: 'انتشار به عنوان وبسایت', + recent: "اخیر", + recover_password: "بازیابی رمز عبور", + refer_friends_c2a: "برای هر دوستی که حساب کاربری Puter ایجاد و تأیید کند، 1 گیگابایت دریافت کنید. دوست شما هم 1 گیگابایت دریافت خواهد کرد!", + refer_friends_social_media_c2a: `1 گیگابایت فضای ذخیره سازی رایگان را در Puter.com بگیرید!`, + refresh: 'تازه کردن', + release_address_confirmation: `آیا مطمئن هستید که می خواهید این آدرس را آزاد کنید؟`, + remove_from_taskbar:'از نوار وظایف حذف کن', + rename: 'تغییر نام', + repeat: 'تکرار', + resend_confirmation_code: "ارسال مجدد کد تأیید", + restore: "بازیابی", + save_account_to_get_copy_link: "لطفا برای ادامه یک حساب کاربری ایجاد کنید.", + save_account_to_publish: 'لطفا برای ادامه یک حساب کاربری ایجاد کنید.', + save_session_c2a: 'برای ذخیره جلسه فعلی و جلوگیری از از دست دادن کار خود یک حساب کاربری ایجاد کنید.', + scan_qr_c2a: 'کد زیر را از دستگاه های دیگر اسکن کنید تا به این جلسه وارد شوید', + select: "انتخاب", + select_color: 'انتخاب رنگ…', + send: "ارسال", + send_password_recovery_email: "ارسال ایمیل بازیابی رمز عبور", + session_saved: "با تشکر از ایجاد حساب کاربری. این جلسه ذخیره شده است.", + set_new_password: "تنظیم رمز عبور جدید", + share_to: "اشتراک گذاری به", + show_all_windows: "نمایش همه پنجره ها", + show_hidden: 'نمایش مخفی', + sign_in_with_puter: "ورود با Puter", + sign_up: "ثبت نام", + signing_in: "ورود…", + size: 'اندازه', + sort_by: 'مرتب سازی بر اساس', + start: 'شروع', + taking_longer_than_usual: 'کمی بیشتر از معمول طول می کشد. لطفا صبر کنید...', + text_document: 'سند متنی', + tos_fineprint: `با کلیک بر روی 'ایجاد حساب کاربری رایگان' شما با شرایط خدمات و سیاست حفظ حریم خصوصی Puter موافقت می کنید.`, + trash: 'سطل زباله', + type: 'نوع', + undo: 'بازگشت', + unzip: "باز کردن فایل فشرده", + upload: 'بارگذاری', + upload_here: 'اینجا بارگذاری کنید', + username: "نام کاربری", + username_changed: 'نام کاربری با موفقیت به روز شد.', + versions: "نسخه ها", + yes_release_it: 'بله، آن را آزاد کن', + you_have_been_referred_to_puter_by_a_friend: "شما توسط یک دوست به Puter معرفی شده اید!", + zip: "فشرده سازی", + }, + // korean + ko: { + access_granted_to: "접근 권한 부여", + add_existing_account: "기존 계정 추가", + all_fields_required: '모든 필드는 필수입니다.', + apply: "적용", + ascending: '오름차순', + background: "배경", + browse: "찾아보기", + cancel: '취소', + center: '중앙', + change_desktop_background: '바탕 화면 배경 변경…', + change_password: "비밀번호 변경", + change_username: "사용자 이름 변경", + close_all_windows: "모든 창 닫기", + color: '색상', + confirm_account_for_free_referral_storage_c2a: '계정을 생성하고 이메일 주소를 확인하여 1GB의 무료 저장 공간을 받으십시오. 친구도 1GB의 무료 저장 공간을 받게 됩니다.', + confirm_new_password: "새 비밀번호 확인", + contact_us: "문의하기", + contain: '포함', + continue: "계속", + copy: '복사', + copy_link: "링크 복사", + copying: "복사 중", + cover: '표지', + create_account: "계정 생성", + create_free_account: "무료 계정 생성", + create_shortcut: "바로 가기 만들기", + current_password: "현재 비밀번호", + cut: '잘라내기', + date_modified: '수정한 날짜', + delete: '삭제', + delete_permanently: "영구 삭제", + deploy_as_app: '앱으로 배포', + descending: '내림차순', + desktop_background_fit: "맞추기", + dir_published_as_website: `%strong% 다음에 게시되었습니다:`, + disassociate_dir: "디렉토리 연결 해제", + download: '다운로드', + downloading: "다운로드 중", + email: "이메일", + email_or_username: "이메일 또는 사용자 이름", + empty_trash: '휴지통 비우기', + empty_trash_confirmation: `휴지통의 항목을 영구적으로 삭제하시겠습니까?`, + emptying_trash: '휴지통 비우는 중…', + feedback: "피드백", + feedback_c2a: "아래 양식을 사용하여 피드백, 의견 및 버그 보고를 보내십시오.", + feedback_sent_confirmation: "문의해 주셔서 감사합니다. 계정에 이메일이 연결되어 있으면 가능한 빨리 회신 드리겠습니다.", + forgot_pass_c2a: "비밀번호를 잊으셨나요?", + from: "보낸 사람", + general: "일반", + get_a_copy_of_on_puter: `Puter.com에서 '%%'의 사본을 받으세요!`, + get_copy_link: '링크 복사', + hide_all_windows: "모든 창 숨기기", + html_document: 'HTML 문서', + image: '이미지', + invite_link: "초대 링크", + items_in_trash_cannot_be_renamed: `이 항목은 휴지통에 있기 때문에 이름을 바꿀 수 없습니다. 이 항목의 이름을 바꾸려면 먼저 휴지통에서 끌어내십시오.`, + jpeg_image: 'JPEG 이미지', + keep_in_taskbar: '작업 표시줄에 유지', + log_in: "로그인", + log_out: '로그아웃', + move: '이동', + moving: "이동 중", + my_websites: "내 웹사이트", + name: '이름', + name_cannot_be_empty: '이름은 비워둘 수 없습니다.', + name_cannot_contain_double_period: "이름은 '..' 문자일 수 없습니다.", + name_cannot_contain_period: "이름은 '.' 문자일 수 없습니다.", + name_cannot_contain_slash: "이름에 '/' 문자를 포함할 수 없습니다.", + name_must_be_string: "이름은 문자열만 가능합니다.", + name_too_long: `이름은 %%자보다 길 수 없습니다.`, + new: '새로운', + new_folder: '새 폴더', + new_password: "새 비밀번호", + new_username: "새 사용자 이름", + no_dir_associated_with_site: '이 주소에 연결된 디렉토리가 없습니다.', + no_websites_published: "아직 웹사이트를 게시하지 않았습니다.", + ok: '확인', + open: "열기", + open_in_new_tab: "새 탭에서 열기", + open_in_new_window: "새 창에서 열기", + open_with: "열기 방법", + password: "비밀번호", + password_changed: "비밀번호가 변경되었습니다.", + passwords_do_not_match: '`새 비밀번호`와 `새 비밀번호 확인`이 일치하지 않습니다.', + paste: '붙여넣기', + paste_into_folder: "폴더에 붙여넣기", + pick_name_for_website: "웹사이트 이름을 선택하세요:", + picture: "사진", + powered_by_puter_js: `Powered by Puter.js`, + preparing: "준비 중...", + preparing_for_upload: "업로드 준비 중...", + properties: "속성", + publish: "게시", + publish_as_website: '웹사이트로 게시', + recent: "최근", + recover_password: "비밀번호 찾기", + refer_friends_c2a: "Puter에서 계정을 생성하고 확인한 친구마다 1GB를 받으십시오. 친구도 1GB를 받게 됩니다!", + refer_friends_social_media_c2a: `Puter.com에서 1GB의 무료 저장 공간을 받으십시오!`, + refresh: '새로 고침', + release_address_confirmation: `이 주소를 해제하시겠습니까?`, + remove_from_taskbar:'작업 표시줄에서 제거', + rename: '이름 바꾸기', + repeat: '반복', + resend_confirmation_code: "확인 코드 다시 보내기", + restore: "복원", + save_account_to_get_copy_link: "계속하려면 계정을 생성하십시오.", + save_account_to_publish: '계속하려면 계정을 생성하십시오.', + save_session_c2a: '현재 세션을 저장하고 작업을 잃지 않으려면 계정을 생성하십시오.', + scan_qr_c2a: '다른 기기에서 이 세션으로 로그인하려면 아래 코드를 스캔하십시오', + select: "선택", + select_color: '색상 선택…', + send: "보내기", + send_password_recovery_email: "비밀번호 복구 이메일 보내기", + session_saved: "계정을 생성해 주셔서 감사합니다. 이 세션이 저장되었습니다.", + set_new_password: "새 비밀번호 설정", + share_to: "공유", + show_all_windows: "모든 창 표시", + show_hidden: '숨김 항목 표시', + sign_in_with_puter: "Puter로 로그인", + sign_up: "가입", + signing_in: "로그인 중…", + size: '크기', + sort_by: '정렬 기준', + start: '시작', + taking_longer_than_usual: '보통보다 조금 더 오래 걸립니다. 잠시만 기다려 주십시오...', + text_document: '텍스트 문서', + tos_fineprint: `무료 계정 생성을 클릭하면 Puter의 서비스 약관개인정보 보호정책에 동의하는 것입니다.`, + trash: '휴지통', + type: '유형', + undo: '실행 취소', + unzip: "압축 해제", + upload: '업로드', + upload_here: '여기에 업로드', + username: "사용자 이름", + username_changed: '사용자 이름이 성공적으로 업데이트되었습니다.', + versions: "버전", + yes_release_it: '예, 해제합니다', + you_have_been_referred_to_puter_by_a_friend: "친구가 Puter로 추천했습니다!", + zip: "압축", + }, + zh: { + access_granted_to: "访问授权给", + add_existing_account: "添加现有帐户", + all_fields_required: '所有字段都是必需的。', + apply: "应用", + ascending: '升序', + background: "背景", + browse: "浏览", + cancel: '取消', + center: '中心', + change_desktop_background: '更改桌面背景…', + change_password: "更改密码", + change_username: "更改用户名", + close_all_windows: "关闭所有窗口", + color: '颜色', + confirm_account_for_free_referral_storage_c2a: '创建帐户并确认您的电子邮件地址,以获得1 GB的免费存储空间。您的朋友也将获得1 GB的免费存储空间。', + confirm_new_password: "确认新密码", + contact_us: "联系我们", + contain: '包含', + continue: "继续", + copy: '复制', + copy_link: "复制链接", + copying: "复制", + cover: '封面', + create_account: "创建帐户", + create_free_account: "创建免费帐户", + create_shortcut: "创建快捷方式", + current_password: "当前密码", + cut: '剪切', + date_modified: '修改日期', + delete: '删除', + delete_permanently: "永久删除", + deploy_as_app: '部署为应用', + descending: '降序', + desktop_background_fit: "适合", + dir_published_as_website: `%strong% 已发布到:`, + disassociate_dir: "取消关联目录", + download: '下载', + downloading: "下载", + email: "电子邮件", + email_or_username: "电子邮件或用户名", + empty_trash: '清空回收站', + empty_trash_confirmation: `您确定要永久删除回收站中的项目吗?`, + emptying_trash: '清空回收站…', + feedback: "反馈", + feedback_c2a: "请使用下面的表格向我们发送您的反馈、评论和错误报告。", + feedback_sent_confirmation: "感谢您与我们联系。如果您的帐户关联有电子邮件,我们会尽快回复您。", + forgot_pass_c2a: "忘记密码?", + from: "从", + general: "一般", + get_a_copy_of_on_puter: `在 Puter.com 上获取 '%%' 的副本!`, + get_copy_link: '获取复制链接', + hide_all_windows: "隐藏所有窗口", + html_document: 'HTML 文档', + image: '图像', + invite_link: "邀请链接", + items_in_trash_cannot_be_renamed: `此项目无法重命名,因为它在回收站中。要重命名此项目,请先将其拖出回收站。`, + jpeg_image: 'JPEG 图像', + keep_in_taskbar: '保持在任务栏', + log_in: "登录", + log_out: '登出', + move: '移动', + moving: "移动", + my_websites: "我的网站", + name: '名称', + name_cannot_be_empty: '名称不能为空。', + name_cannot_contain_double_period: "名称不能是'..'字符。", + name_cannot_contain_period: "名称不能是'.'字符。", + name_cannot_contain_slash: "名称不能包含'/'字符。", + name_must_be_string: "名称只能是字符串。", + name_too_long: `名称不能超过 %% 个字符。`, + new: '新', + new_folder: '新文件夹', + new_password: "新密码", + new_username: "新用户名", + no_dir_associated_with_site: '此地址没有关联的目录。', + no_websites_published: "您尚未发布任何网站。", + ok: '好的', + open: "打开", + open_in_new_tab: "在新标签页中打开", + open_in_new_window: "在新窗口中打开", + open_with: "打开方式", + password: "密码", + password_changed: "密码已更改。", + passwords_do_not_match: '`新密码` 和 `确认新密码` 不匹配。', + paste: '粘贴', + paste_into_folder: "粘贴到文件夹", + pick_name_for_website: "为您的网站选择一个名称:", + picture: "图片", + powered_by_puter_js: `由 Puter.js 提供支持`, + preparing: "准备中...", + preparing_for_upload: "准备上传...", + properties: "属性", + publish: "发布", + publish_as_website: '发布为网站', + recent: "最近", + recover_password: "找回密码", + refer_friends_c2a: "每个创建并确认 Puter 帐户的朋友都会为您获得 1 GB。您的朋友也将获得 1 GB!", + refer_friends_social_media_c2a: `在 Puter.com 上获取 1 GB 的免费存储空间!`, + refresh: '刷新', + release_address_confirmation: `您确定要释放此地址吗?`, + remove_from_taskbar:'从任务栏中删除', + rename: '重命名', + repeat: '重复', + resend_confirmation_code: "重新发送确认码", + restore: "还原", + save_account_to_get_copy_link: "请创建帐户以继续。", + save_account_to_publish: '请创建帐户以继续。', + save_session_c2a: '创建帐户以保存当前会话,避免丢失工作。', + scan_qr_c2a: '扫描下面的代码以从其他设备登录此会话', + select: "选择", + select_color: '选择颜色…', + send: "发送", + send_password_recovery_email: "发送密码恢复电子邮件", + session_saved: "感谢您创建帐户。此会话已保存。", + set_new_password: "设置新密码", + share_to: "分享到", + show_all_windows: "显示所有窗口", + show_hidden: '显示隐藏', + sign_in_with_puter: "使用 Puter 登录", + sign_up: "注册", + signing_in: "登录中…", + size: '大小', + sort_by: '排序方式', + start: '开始', + taking_longer_than_usual: '需要的时间比平时长一点。请稍等...', + text_document: '文本文档', + tos_fineprint: `点击“创建免费帐户”即表示您同意 Puter 的 服务条款隐私政策。`, + trash: '回收站', + type: '类型', + undo: '撤销', + unzip: "解压缩", + upload: '上传', + upload_here: '在此上传', + username: "用户名", + username_changed: '用户名已成功更新。', + versions: "版本", + yes_release_it: '是的,释放它', + you_have_been_referred_to_puter_by_a_friend: "您已经被朋友推荐到 Puter!", + zip: "压缩", + }, +} \ No newline at end of file diff --git a/src/initgui.js b/src/initgui.js index f8a01cae..5da6b391 100644 --- a/src/initgui.js +++ b/src/initgui.js @@ -33,6 +33,7 @@ import UIWindowChangeUsername from './UI/UIWindowChangeUsername.js'; import update_last_touch_coordinates from './helpers/update_last_touch_coordinates.js'; import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js'; import PuterDialog from './UI/PuterDialog.js'; +import determine_active_container_parent from './helpers/determine_active_container_parent.js'; window.initgui = async function(){ let url = new URL(window.location); @@ -45,6 +46,10 @@ window.initgui = async function(){ if(window.api_origin && puter.APIOrigin !== window.api_origin) puter.setAPIOrigin(api_origin); + // determine locale + const userLang = navigator.language || navigator.userLanguage || 'en'; + window.locale = userLang?.split('-')[0] ?? 'en'; + // Checks the type of device the user is on (phone, tablet, or desktop). // Depending on the device type, it sets a class attribute on the body tag // to style or script the page differently for each device type. @@ -76,6 +81,13 @@ window.initgui = async function(){ const url_paths = window.location.pathname.split('/').filter(element => element); if(url_paths[0]?.toLocaleLowerCase() === 'app' && url_paths[1]){ window.app_launched_from_url = url_paths[1]; + + // get query params, any param that doesn't start with 'puter.' will be passed to the app + window.app_query_params = {}; + for (let [key, value] of url_query_params) { + if(!key.startsWith('puter.')) + app_query_params[key] = value; + } } //-------------------------------------------------------------------------------------- @@ -1978,4 +1990,32 @@ function requestOpenerOrigin() { $(document).on('click', '.generic-close-window-button', function(e){ $(this).closest('.window').close(); +}); + +// Re-calculate desktop height and width on window resize and re-position the login and signup windows +$(window).on("resize", function () { + // If host env is popup, don't continue because the popup window has its own resize requirements. + if (window.embedded_in_popup) + return; + + const ratio = window.desktop_width / window.innerWidth; + + window.desktop_height = window.innerHeight - window.toolbar_height - window.taskbar_height; + window.desktop_width = window.innerWidth; + + // Re-center the login window + const top = $(".window-login").position()?.top; + const width = $(".window-login").width(); + $(".window-login").css({ + left: (window.desktop_width - width) / 2, + top: top / ratio, + }); + + // Re-center the create account window + const top2 = $(".window-signup").position()?.top; + const width2 = $(".window-signup").width(); + $(".window-signup").css({ + left: (window.desktop_width - width2) / 2, + top: top2 / ratio, + }); }); \ No newline at end of file diff --git a/src/lib/jquery.menu-aim.js b/src/lib/jquery.menu-aim.js new file mode 100644 index 00000000..47105340 --- /dev/null +++ b/src/lib/jquery.menu-aim.js @@ -0,0 +1,323 @@ +/** + * menu-aim is a jQuery plugin for dropdown menus that can differentiate + * between a user trying hover over a dropdown item vs trying to navigate into + * a submenu's contents. + * + * menu-aim assumes that you have are using a menu with submenus that expand + * to the menu's right. It will fire events when the user's mouse enters a new + * dropdown item *and* when that item is being intentionally hovered over. + * + * __________________________ + * | Monkeys >| Gorilla | + * | Gorillas >| Content | + * | Chimps >| Here | + * |___________|____________| + * + * In the above example, "Gorillas" is selected and its submenu content is + * being shown on the right. Imagine that the user's cursor is hovering over + * "Gorillas." When they move their mouse into the "Gorilla Content" area, they + * may briefly hover over "Chimps." This shouldn't close the "Gorilla Content" + * area. + * + * This problem is normally solved using timeouts and delays. menu-aim tries to + * solve this by detecting the direction of the user's mouse movement. This can + * make for quicker transitions when navigating up and down the menu. The + * experience is hopefully similar to amazon.com/'s "Shop by Department" + * dropdown. + * + * Use like so: + * + * $("#menu").menuAim({ + * activate: $.noop, // fired on row activation + * deactivate: $.noop // fired on row deactivation + * }); + * + * ...to receive events when a menu's row has been purposefully (de)activated. + * + * The following options can be passed to menuAim. All functions execute with + * the relevant row's HTML element as the execution context ('this'): + * + * .menuAim({ + * // Function to call when a row is purposefully activated. Use this + * // to show a submenu's content for the activated row. + * activate: function() {}, + * + * // Function to call when a row is deactivated. + * deactivate: function() {}, + * + * // Function to call when mouse enters a menu row. Entering a row + * // does not mean the row has been activated, as the user may be + * // mousing over to a submenu. + * enter: function() {}, + * + * // Function to call when mouse exits a menu row. + * exit: function() {}, + * + * // Selector for identifying which elements in the menu are rows + * // that can trigger the above events. Defaults to "> li". + * rowSelector: "> li", + * + * // You may have some menu rows that aren't submenus and therefore + * // shouldn't ever need to "activate." If so, filter submenu rows w/ + * // this selector. Defaults to "*" (all elements). + * submenuSelector: "*", + * + * // Direction the submenu opens relative to the main menu. Can be + * // left, right, above, or below. Defaults to "right". + * submenuDirection: "right" + * }); + * + * https://github.com/kamens/jQuery-menu-aim +*/ +(function($) { + + $.fn.menuAim = function(opts) { + // Initialize menu-aim for all elements in jQuery collection + this.each(function() { + init.call(this, opts); + }); + + return this; + }; + + function init(opts) { + var $menu = $(this), + activeRow = null, + mouseLocs = [], + lastDelayLoc = null, + timeoutId = null, + options = $.extend({ + rowSelector: "> li", + submenuSelector: "*", + submenuDirection: $.noop, + tolerance: 75, // bigger = more forgivey when entering submenu + enter: $.noop, + exit: $.noop, + activate: $.noop, + deactivate: $.noop, + exitMenu: $.noop + }, opts); + + var MOUSE_LOCS_TRACKED = 3, // number of past mouse locations to track + DELAY = 300; // ms delay when user appears to be entering submenu + + /** + * Keep track of the last few locations of the mouse. + */ + var mousemoveDocument = function(e) { + mouseLocs.push({x: e.pageX, y: e.pageY}); + + if (mouseLocs.length > MOUSE_LOCS_TRACKED) { + mouseLocs.shift(); + } + }; + + /** + * Cancel possible row activations when leaving the menu entirely + */ + var mouseleaveMenu = function() { + if (timeoutId) { + clearTimeout(timeoutId); + } + + // If exitMenu is supplied and returns true, deactivate the + // currently active row on menu exit. + if (options.exitMenu(this)) { + if (activeRow) { + options.deactivate(activeRow); + } + + activeRow = null; + } + }; + + /** + * Trigger a possible row activation whenever entering a new row. + */ + var mouseenterRow = function() { + if (timeoutId) { + // Cancel any previous activation delays + clearTimeout(timeoutId); + } + + options.enter(this); + possiblyActivate(this); + }, + mouseleaveRow = function() { + options.exit(this); + }; + + /* + * Immediately activate a row if the user clicks on it. + */ + var clickRow = function() { + activate(this); + }; + + /** + * Activate a menu row. + */ + var activate = function(row) { + if (row == activeRow) { + return; + } + + if (activeRow) { + options.deactivate(activeRow); + } + + + options.activate(row); + activeRow = row; + }; + + /** + * Possibly activate a menu row. If mouse movement indicates that we + * shouldn't activate yet because user may be trying to enter + * a submenu's content, then delay and check again later. + */ + var possiblyActivate = function(row) { + var delay = activationDelay(); + + if (delay) { + timeoutId = setTimeout(function() { + possiblyActivate(row); + }, delay); + } else { + activate(row); + } + }; + + /** + * Return the amount of time that should be used as a delay before the + * currently hovered row is activated. + * + * Returns 0 if the activation should happen immediately. Otherwise, + * returns the number of milliseconds that should be delayed before + * checking again to see if the row should be activated. + */ + var activationDelay = function() { + if (!activeRow || !$(activeRow).is(options.submenuSelector)) { + // If there is no other submenu row already active, then + // go ahead and activate immediately. + return 0; + } + + var offset = $menu.offset(), + upperLeft = { + x: offset.left, + y: offset.top - options.tolerance + }, + upperRight = { + x: offset.left + $menu.outerWidth(), + y: upperLeft.y + }, + lowerLeft = { + x: offset.left, + y: offset.top + $menu.outerHeight() + options.tolerance + }, + lowerRight = { + x: offset.left + $menu.outerWidth(), + y: lowerLeft.y + }, + loc = mouseLocs[mouseLocs.length - 1], + prevLoc = mouseLocs[0]; + + if (!loc) { + return 0; + } + + if (!prevLoc) { + prevLoc = loc; + } + + if (prevLoc.x < offset.left || prevLoc.x > lowerRight.x || + prevLoc.y < offset.top || prevLoc.y > lowerRight.y) { + // If the previous mouse location was outside of the entire + // menu's bounds, immediately activate. + return 0; + } + + if (lastDelayLoc && + loc.x == lastDelayLoc.x && loc.y == lastDelayLoc.y) { + // If the mouse hasn't moved since the last time we checked + // for activation status, immediately activate. + return 0; + } + + // Detect if the user is moving towards the currently activated + // submenu. + // + // If the mouse is heading relatively clearly towards + // the submenu's content, we should wait and give the user more + // time before activating a new row. If the mouse is heading + // elsewhere, we can immediately activate a new row. + // + // We detect this by calculating the slope formed between the + // current mouse location and the upper/lower right points of + // the menu. We do the same for the previous mouse location. + // If the current mouse location's slopes are + // increasing/decreasing appropriately compared to the + // previous's, we know the user is moving toward the submenu. + // + // Note that since the y-axis increases as the cursor moves + // down the screen, we are looking for the slope between the + // cursor and the upper right corner to decrease over time, not + // increase (somewhat counterintuitively). + function slope(a, b) { + return (b.y - a.y) / (b.x - a.x); + }; + + var decreasingCorner = upperRight, + increasingCorner = lowerRight; + + // Our expectations for decreasing or increasing slope values + // depends on which direction the submenu opens relative to the + // main menu. By default, if the menu opens on the right, we + // expect the slope between the cursor and the upper right + // corner to decrease over time, as explained above. If the + // submenu opens in a different direction, we change our slope + // expectations. + if (options.submenuDirection() == "left") { + decreasingCorner = lowerLeft; + increasingCorner = upperLeft; + } else if (options.submenuDirection() == "below") { + decreasingCorner = lowerRight; + increasingCorner = lowerLeft; + } else if (options.submenuDirection() == "above") { + decreasingCorner = upperLeft; + increasingCorner = upperRight; + } + + var decreasingSlope = slope(loc, decreasingCorner), + increasingSlope = slope(loc, increasingCorner), + prevDecreasingSlope = slope(prevLoc, decreasingCorner), + prevIncreasingSlope = slope(prevLoc, increasingCorner); + + if (decreasingSlope < prevDecreasingSlope && + increasingSlope > prevIncreasingSlope) { + // Mouse is moving from previous location towards the + // currently activated submenu. Delay before activating a + // new menu row, because user may be moving into submenu. + lastDelayLoc = loc; + return DELAY; + } + + lastDelayLoc = null; + return 0; + }; + + /** + * Hook up initial menu events + */ + $menu + .mouseleave(mouseleaveMenu) + .find(options.rowSelector) + .mouseenter(mouseenterRow) + .mouseleave(mouseleaveRow) + .click(clickRow); + + $(document).mousemove(mousemoveDocument); + + }; +})(jQuery); \ No newline at end of file diff --git a/src/static-assets.js b/src/static-assets.js index 32e5cf7e..481d2270 100644 --- a/src/static-assets.js +++ b/src/static-assets.js @@ -27,11 +27,13 @@ const lib_paths =[ `/lib/jquery-ui-1.13.2/jquery-ui.min.js`, `/lib/lodash@4.17.21.min.js`, `/lib/jquery.dragster.js`, + '/lib/jquery.menu-aim.js', `/lib/html-entities.js`, `/lib/timeago.min.js`, `/lib/iro.min.js`, `/lib/isMobile.min.js`, `/lib/jszip-3.10.1.min.js`, + `/i18n/i18n.js`, ] // Ordered list of CSS stylesheets