feat: Settings and modal UI/UX improvements (#1828)

* feat: comprehensive settings and modal UI/UX improvements

Redesigned settings interface and modal windows with improved visual
hierarchy, spacing, and modern styling throughout the application.

Settings UI improvements:
- Enhanced section headers with proper flexbox layout and centering
- Simplified storage usage display to single percentage with clearer info
- Improved settings cards with consistent spacing and hover states
- Redesigned session manager with horizontal card layout
- Reordered account settings (username → email → password)
- Removed duplicate password option from security tab

Form and input improvements:
- Increased container padding (20px → 24px) for better breathing room
- Tightened label-to-input gap (8px → 6px) for better visual grouping
- Enhanced labels with bolder weight (500 → 600) and refined color
- Larger input fields (12px 14px padding) with smoother corners (6px)
- Added hover states with border color transitions
- Removed distracting box-shadow focus rings across all inputs/selects

Button improvements:
- Modernized primary buttons with solid colors (removed gradients)
- Added flexbox centering for consistent text alignment
- Reduced button text size (13px) for cleaner appearance
- Improved default button styling with refined gray palette
- Better active/hover state transitions

Select dropdown improvements:
- Custom SVG chevron arrow with proper 12px right spacing
- Fixed select resizing issue on focus (removed global border/padding override)
- Full-width selects in modals with consistent styling
- Smooth hover states without size changes

Modal windows:
- Consistent 380px width for form modals, 400px for desktop bg settings
- Unified .settings-form-container class across all modals
- Proper ARIA labels and semantic HTML structure
- Error/success messages with refined colors and better contrast

Desktop background settings:
- Color blocks increased to 32px for easier interaction
- Fixed focus outline clipping with proper overflow handling
- Removed scale effects for cleaner interactions
- Flexbox layout for color grid with proper spacing
- Fixed palette icon display with inline background image

Window chrome:
- Increased default shadow (0 12px 24px, 0.12 opacity)
- Enhanced active window shadow (0 16px 40px, 0.18 opacity)
- Better visual depth and window hierarchy

File organization:
- Moved UIWindowManageSessions to Settings folder
- Created helpers/build_settings_card.js for reusable components
- Updated all imports to reflect new structure

* bring back shadow

* improve 2fa + fix theme not changing on cancel bug + fix couple more issues

* style polish

* remove unused styles

* i18n

* fix search bar clipping

* group reset & customize color buttons

* more polish

* enforce min width/height on resizable

* resizable settings and show full name on hover

* style tweaks
This commit is contained in:
Utku
2025-11-06 21:14:32 +03:00
committed by GitHub
parent ac47753879
commit 191916321b
31 changed files with 4381 additions and 1720 deletions

View File

@@ -63,7 +63,7 @@ export default def(class QRCodeView extends Component {
on_ready ({ listen }) {
listen('value', value => {
// $(this.dom_).find('.qr-code').empty();
$(this.dom_).find('.qr-code').empty();
new QRCode($(this.dom_).find('.qr-code').get(0), {
text: value,
// TODO: dynamic size

View File

@@ -7,12 +7,12 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -30,33 +30,33 @@ export default {
<p class="description">${i18n('puter_description')}</p>
<p class="links">
<a href="mailto:hey@puter.com" target="_blank">hey@puter.com</a>
<span style="color: #CCC;">•</span>
<span class="about-link-separator">•</span>
<a href="https://docs.puter.com" target="_blank">${i18n('developers')}</a>
<span style="color: #CCC;">•</span>
<span class="about-link-separator">•</span>
<a href="https://status.puter.com" target="_blank">${i18n('status')}</a>
<span style="color: #CCC;">•</span>
<span class="about-link-separator">•</span>
<a href="https://puter.com/terms" target="_blank">${i18n('terms')}</a>
<span style="color: #CCC;">•</span>
<span class="about-link-separator">•</span>
<a href="https://puter.com/privacy" target="_blank">${i18n('privacy')}</a>
<span style="color: #CCC;">•</span>
<span class="about-link-separator">•</span>
<a href="#" class="show-credits">${i18n('credits')}</a>
</p>
<div class="social-links">
<a href="https://twitter.com/HeyPuter/" target="_blank">
<a href="https://twitter.com/HeyPuter/" target="_blank" rel="noopener noreferrer" aria-label="Follow Puter on Twitter">
<svg viewBox="0 0 24 24" aria-hidden="true" style="opacity: 0.7;"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>
</a>
<a href="https://github.com/HeyPuter/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48">
<a href="https://github.com/HeyPuter/" target="_blank" rel="noopener noreferrer" aria-label="Visit Puter on GitHub">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48" aria-hidden="true">
<g transform="translate(0, 0)">
<path fill-rule="evenodd" clip-rule="evenodd" fill="#5a606b" d="M24,0.6c-13.3,0-24,10.7-24,24c0,10.6,6.9,19.6,16.4,22.8 c1.2,0.2,1.6-0.5,1.6-1.2c0-0.6,0-2.1,0-4.1c-6.7,1.5-8.1-3.2-8.1-3.2c-1.1-2.8-2.7-3.5-2.7-3.5c-2.2-1.5,0.2-1.5,0.2-1.5 c2.4,0.2,3.7,2.5,3.7,2.5c2.1,3.7,5.6,2.6,7,2c0.2-1.6,0.8-2.6,1.5-3.2c-5.3-0.6-10.9-2.7-10.9-11.9c0-2.6,0.9-4.8,2.5-6.4 c-0.2-0.6-1.1-3,0.2-6.4c0,0,2-0.6,6.6,2.5c1.9-0.5,4-0.8,6-0.8c2,0,4.1,0.3,6,0.8c4.6-3.1,6.6-2.5,6.6-2.5c1.3,3.3,0.5,5.7,0.2,6.4 c1.5,1.7,2.5,3.8,2.5,6.4c0,9.2-5.6,11.2-11,11.8c0.9,0.7,1.6,2.2,1.6,4.4c0,3.2,0,5.8,0,6.6c0,0.6,0.4,1.4,1.7,1.2 C41.1,44.2,48,35.2,48,24.6C48,11.3,37.3,0.6,24,0.6z">
</path>
</g>
</svg>
</a>
<a href="https://discord.gg/PQcx7Teh8u" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48"><g transform="translate(0, 0)"><path d="M19.837,20.3a2.562,2.562,0,0,0,0,5.106,2.562,2.562,0,0,0,0-5.106Zm8.4,0a2.562,2.562,0,1,0,2.346,2.553A2.45,2.45,0,0,0,28.232,20.3Z" fill="#444444" data-color="color-2"></path> <path d="M39.41,1H8.59A4.854,4.854,0,0,0,4,6V37a4.482,4.482,0,0,0,4.59,4.572H34.672l-1.219-4.255L36.4,40.054,39.18,42.63,44,47V6A4.854,4.854,0,0,0,39.41,1ZM30.532,31.038s-.828-.989-1.518-1.863a7.258,7.258,0,0,0,4.163-2.737A13.162,13.162,0,0,1,30.532,27.8a15.138,15.138,0,0,1-3.335.989,16.112,16.112,0,0,1-5.957-.023,19.307,19.307,0,0,1-3.381-.989,13.112,13.112,0,0,1-2.622-1.357,7.153,7.153,0,0,0,4.025,2.714c-.69.874-1.541,1.909-1.541,1.909-5.083-.161-7.015-3.5-7.015-3.5a30.8,30.8,0,0,1,3.312-13.409,11.374,11.374,0,0,1,6.463-2.415l.23.276a15.517,15.517,0,0,0-6.049,3.013s.506-.276,1.357-.667a17.272,17.272,0,0,1,5.221-1.449,2.266,2.266,0,0,1,.391-.046,19.461,19.461,0,0,1,4.646-.046A18.749,18.749,0,0,1,33.2,15.007a15.307,15.307,0,0,0-5.727-2.921l.322-.368a11.374,11.374,0,0,1,6.463,2.415A30.8,30.8,0,0,1,37.57,27.542S35.615,30.877,30.532,31.038Z" fill="#444444"></path></g></svg> </a>
<a href="https://www.linkedin.com/company/puter/" target="_blank">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48">
<a href="https://discord.gg/PQcx7Teh8u" target="_blank" rel="noopener noreferrer" aria-label="Join Puter on Discord">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48" aria-hidden="true"><g transform="translate(0, 0)"><path d="M19.837,20.3a2.562,2.562,0,0,0,0,5.106,2.562,2.562,0,0,0,0-5.106Zm8.4,0a2.562,2.562,0,1,0,2.346,2.553A2.45,2.45,0,0,0,28.232,20.3Z" fill="#444444" data-color="color-2"></path> <path d="M39.41,1H8.59A4.854,4.854,0,0,0,4,6V37a4.482,4.482,0,0,0,4.59,4.572H34.672l-1.219-4.255L36.4,40.054,39.18,42.63,44,47V6A4.854,4.854,0,0,0,39.41,1ZM30.532,31.038s-.828-.989-1.518-1.863a7.258,7.258,0,0,0,4.163-2.737A13.162,13.162,0,0,1,30.532,27.8a15.138,15.138,0,0,1-3.335.989,16.112,16.112,0,0,1-5.957-.023,19.307,19.307,0,0,1-3.381-.989,13.112,13.112,0,0,1-2.622-1.357,7.153,7.153,0,0,0,4.025,2.714c-.69.874-1.541,1.909-1.541,1.909-5.083-.161-7.015-3.5-7.015-3.5a30.8,30.8,0,0,1,3.312-13.409,11.374,11.374,0,0,1,6.463-2.415l.23.276a15.517,15.517,0,0,0-6.049,3.013s.506-.276,1.357-.667a17.272,17.272,0,0,1,5.221-1.449,2.266,2.266,0,0,1,.391-.046,19.461,19.461,0,0,1,4.646-.046A18.749,18.749,0,0,1,33.2,15.007a15.307,15.307,0,0,0-5.727-2.921l.322-.368a11.374,11.374,0,0,1,6.463,2.415A30.8,30.8,0,0,1,37.57,27.542S35.615,30.877,30.532,31.038Z" fill="#444444"></path></g></svg> </a>
<a href="https://www.linkedin.com/company/puter/" target="_blank" rel="noopener noreferrer" aria-label="Connect with Puter on LinkedIn">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" height="48px" viewBox="0 0 48 48" aria-hidden="true">
<g transform="translate(0, 0)">
<path fill="#5a606b" d="M46,0H2C0.9,0,0,0.9,0,2v44c0,1.1,0.9,2,2,2h44c1.1,0,2-0.9,2-2V2C48,0.9,47.1,0,46,0z M14.2,40.9H7.1V18 h7.1V40.9z M10.7,14.9c-2.3,0-4.1-1.8-4.1-4.1c0-2.3,1.8-4.1,4.1-4.1c2.3,0,4.1,1.8,4.1,4.1C14.8,13,13,14.9,10.7,14.9z M40.9,40.9 h-7.1V29.8c0-2.7,0-6.1-3.7-6.1c-3.7,0-4.3,2.9-4.3,5.9v11.3h-7.1V18h6.8v3.1h0.1c0.9-1.8,3.3-3.7,6.7-3.7c7.2,0,8.5,4.7,8.5,10.9 V40.9z">
</path>
@@ -69,9 +69,9 @@ export default {
<dialog class="credits">
<div class="credit-content">
<p style="margin: 0; font-size: 18px; text-align: center;">${i18n('oss_code_and_content')}</p>
<div style="max-height: 300px; overflow-y: scroll;">
<ul style="padding-left: 25px; padding-top:15px;">
<p class="credits-header">${i18n('oss_code_and_content')}</p>
<div class="credits-list-wrapper">
<ul class="credits-list">
<li>FileSaver.js <a target="_blank" href="https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md">${i18n('license')}</a></li>
<li>html-entities <a target="_blank" href="https://github.com/mdevils/html-entities/blob/master/LICENSE">${i18n('license')}</a></li>
<li>iro.js <a target="_blank" href="https://github.com/jaames/iro.js/blob/master/LICENSE.txt">${i18n('license')}</a></li>
@@ -95,24 +95,24 @@ export default {
init: ($el_window) => {
// server and version infomration
puter.os.version()
.then(res => {
const deployed_date = new Date(res.deploy_timestamp).toLocaleString();
$el_window.find('.version').html(`Version: ${html_encode(res.version)} &bull; Server: ${html_encode(res.location)} &bull; Deployed: ${html_encode(deployed_date)}`);
})
.catch(error => {
console.error("Failed to fetch server info:", error);
$el_window.find('.version').html("Failed to load version information.");
});
.then(res => {
const deployed_date = new Date(res.deploy_timestamp).toLocaleString();
$el_window.find('.version').html(`Version: ${html_encode(res.version)} &bull; Server: ${html_encode(res.location)} &bull; Deployed: ${html_encode(deployed_date)}`);
})
.catch(error => {
console.error('Failed to fetch server info:', error);
$el_window.find('.version').html('Failed to load version information.');
});
$el_window.find('.credits').on('click', function (e) {
if($(e.target).hasClass('credits')){
$el_window.find('.credits').on('click', function(e) {
if ( $(e.target).hasClass('credits') ){
$('.credits').get(0).close();
}
});
$el_window.find('.show-credits').on('click', function (e) {
$el_window.find('.show-credits').on('click', function(e) {
$('.credits').get(0).showModal();
})
});
},
};

View File

@@ -21,114 +21,246 @@ import UIWindowChangePassword from '../UIWindowChangePassword.js';
import UIWindowChangeEmail from './UIWindowChangeEmail.js';
import UIWindowChangeUsername from '../UIWindowChangeUsername.js';
import UIWindowConfirmUserDeletion from './UIWindowConfirmUserDeletion.js';
import UIWindowManageSessions from '../UIWindowManageSessions.js';
import UIWindowManageSessions from './UIWindowManageSessions.js';
import UIWindow from '../UIWindow.js';
import build_settings_card from './helpers/build_settings_card.js';
import TeePromise from '../../util/TeePromise.js';
import UIComponentWindow from '../UIComponentWindow.js';
import UIWindow2FASetup from '../UIWindow2FASetup.js';
// About
export default {
id: 'account',
title_i18n_key: 'account',
icon: 'user.svg',
html: () => {
let h = '';
// profile picture
h += `<div style="overflow: visible; display: flex; margin-bottom: 20px; flex-direction: column; align-items: center;">`;
h += `<div class="profile-picture change-profile-picture" style="background-image: url('${html_encode(window.user?.profile?.picture ?? window.icons['profile.svg'])}');">`;
h += `</div>`;
h += `</div>`;
const passwordCard = !window.user.is_temp ? build_settings_card({
label: i18n('password'),
control: `<button class="button change-password" aria-label="${i18n('change_password')}">${i18n('change_password')}</button>`,
}) : '';
// change password button
if(!window.user.is_temp){
h += `<div class="settings-card">`;
h += `<strong>${i18n('password')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-password" style="float:right;">${i18n('change_password')}</button>`;
h += `</div>`;
h += `</div>`;
}
const emailCard = window.user.email ? build_settings_card({
label: i18n('email'),
description: `<span class="user-email">${html_encode(window.user.email)}</span>`,
control: `<button class="button change-email" aria-label="${i18n('change_email')}">${i18n('change_email')}</button>`,
}) : '';
// change username button
h += `<div class="settings-card">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('username')}</strong>`;
h += `<span class="username" style="display:block; margin-top:5px;">${html_encode(window.user.username)}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-username" style="float:right;">${i18n('change_username')}</button>`;
h += `</div>`
h += `</div>`;
// change email button
if(window.user.email){
h += `<div class="settings-card">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('email')}</strong>`;
h += `<span class="user-email" style="display:block; margin-top:5px;">${html_encode(window.user.email)}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-email" style="float:right;">${i18n('change_email')}</button>`;
h += `</div>`;
h += `</div>`;
}
// 'Delete Account' button
h += `<div class="settings-card settings-card-danger">`;
h += `<strong style="display: inline-block;">${i18n("delete_account")}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button button-danger delete-account" style="float:right;">${i18n("delete_account")}</button>`;
h += `</div>`;
h += `</div>`;
return h;
const twoFactorEnabled = window.user.otp;
const twoFactorCard = !window.user.is_temp && window.user.email_confirmed ? build_settings_card({
label: i18n('two_factor'),
description: `<span class="user-otp-state">${i18n(twoFactorEnabled ? 'two_factor_enabled' : 'two_factor_disabled')}</span>`,
variant: twoFactorEnabled ? 'success' : 'warning',
className: 'settings-card-security',
control: `
<button class="button enable-2fa ${twoFactorEnabled ? 'hidden' : ''}" aria-label="${i18n('enable_2fa')}">${i18n('enable_2fa')}</button>
<button class="button disable-2fa ${twoFactorEnabled ? '' : 'hidden'}" aria-label="${i18n('disable_2fa')}">${i18n('disable_2fa')}</button>
`,
}) : '';
return `
<h1 class="settings-section-header">${i18n('account')}</h1>
<div class="settings-profile-picture-container">
<div class="profile-picture change-profile-picture" role="button" tabindex="0" aria-label="${i18n('change')} ${i18n('picture')}" style="background-image: url('${html_encode(window.user?.profile?.picture ?? window.icons['profile.svg'])}');"></div>
</div>
${passwordCard}
${build_settings_card({
label: i18n('username'),
description: html_encode(window.user.username),
control: `<button class="button change-username" aria-label="${i18n('change_username')}">${i18n('change_username')}</button>`,
})}
${emailCard}
${build_settings_card({
label: i18n('sessions'),
control: `<button class="button manage-sessions" aria-label="${i18n('manage_sessions')}">${i18n('manage_sessions')}</button>`,
})}
${twoFactorCard}
${build_settings_card({
label: i18n('delete_account'),
variant: 'danger',
control: `<button class="button button-danger delete-account" aria-label="${i18n('delete_account')}">${i18n('delete_account')}</button>`,
})}
`;
},
init: ($el_window) => {
$el_window.find('.change-password').on('click', function (e) {
$el_window.find('.change-password').on('click', function(e) {
UIWindowChangePassword({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
$el_window.find('.change-username').on('click', function (e) {
$el_window.find('.change-username').on('click', function(e) {
UIWindowChangeUsername({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
$el_window.find('.change-email').on('click', function (e) {
$el_window.find('.change-email').on('click', function(e) {
UIWindowChangeEmail({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
$el_window.find('.manage-sessions').on('click', function (e) {
$el_window.find('.manage-sessions').on('click', function(e) {
UIWindowManageSessions({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
$el_window.find('.delete-account').on('click', function (e) {
$el_window.find('.enable-2fa').on('click', async function(e) {
const { promise } = await UIWindow2FASetup();
const tfa_was_enabled = await promise;
if ( tfa_was_enabled ) {
$el_window.find('.enable-2fa').addClass('hidden');
$el_window.find('.disable-2fa').removeClass('hidden');
$el_window.find('.user-otp-state').text(i18n('two_factor_enabled'));
$el_window.find('.settings-card-security').removeClass('settings-card-warning');
$el_window.find('.settings-card-security').addClass('settings-card-success');
}
return;
});
$el_window.find('.disable-2fa').on('click', async function(e) {
let win, password_entry;
const password_confirm_promise = new TeePromise();
const try_password = async () => {
const value = password_entry.get('value');
const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
password: value,
}),
});
if ( resp.status !== 200 ) {
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
let message; try {
message = (await resp.json()).message;
} catch(e) {
}
message = message || i18n('error_unknown_cause');
password_entry.set('error', message);
return;
}
password_confirm_promise.resolve(true);
$(win).close();
};
const h = `
<div class="security-modal-content">
<div>
<h3 class="security-modal-header">${i18n('disable_2fa_confirm')}</h3>
<p class="security-modal-description">${i18n('disable_2fa_instructions')}</p>
</div>
<div class="security-modal-inputs">
<div class="password-input-wrapper">
<input type="password" class="password-entry form-input" />
<button type="button" class="password-toggle-btn" aria-label="${i18n('toggle_password_visibility')}">
<svg class="eye-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
<button class="button confirm-disable-2fa">${i18n('disable_2fa')}</button>
<button class="button secondary cancel-disable-2fa">${i18n('cancel')}</button>
</div>
</div>
`;
win = await UIComponentWindow({
html: h,
width: 500,
backdrop: true,
is_resizable: false,
body_css: {
width: 'initial',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
padding: '20px',
},
});
// Set up event listeners
const $win = $(win);
const $password_entry = $win.find('.password-entry');
// Password toggle button
$win.on('click', '.password-toggle-btn', function(e){
e.preventDefault();
const $input = $(this).siblings('input');
const type = $input.attr('type');
$input.attr('type', type === 'password' ? 'text' : 'password');
const $svg = $(this).find('svg');
if (type === 'password') {
// Show eye-off icon
$svg.html(`
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
`);
} else {
// Show eye icon
$svg.html(`
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
`);
}
});
$password_entry.on('keypress', (e) => {
if ( e.which === 13 ) { // Enter key
try_password();
}
});
$win.find('.confirm-disable-2fa').on('click', () => {
try_password();
});
$win.find('.cancel-disable-2fa').on('click', () => {
password_confirm_promise.resolve(false);
$win.close();
});
$password_entry.focus();
const ok = await password_confirm_promise;
if ( ! ok ) return;
$el_window.find('.enable-2fa').removeClass('hidden');
$el_window.find('.disable-2fa').addClass('hidden');
$el_window.find('.user-otp-state').text(i18n('two_factor_disabled'));
$el_window.find('.settings-card-security').removeClass('settings-card-success');
$el_window.find('.settings-card-security').addClass('settings-card-warning');
});
$el_window.find('.delete-account').on('click', function(e) {
UIWindowConfirmUserDeletion({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
$el_window.find('.change-profile-picture').on('click', async function (e) {
// open dialog
$el_window.find('.change-profile-picture').on('click', async function(e) {
UIWindow({
path: '/' + window.user.username + '/Desktop',
// this is the uuid of the window to which this dialog will return
path: `/${window.user.username}/Desktop`,
parent_uuid: $el_window.attr('data-element_uuid'),
allowed_file_types: ['.png', '.jpg', '.jpeg'],
show_maximize_button: false,
@@ -137,12 +269,18 @@ export default {
is_dir: true,
is_openFileDialog: true,
selectable_body: false,
});
})
});
});
$el_window.find('.change-profile-picture').on('keydown', function(e) {
if(e.key === 'Enter' || e.key === ' '){
e.preventDefault();
$(this).trigger('click');
}
});
$el_window.on('file_opened', async function(e){
let selected_file = Array.isArray(e.detail) ? e.detail[0] : e.detail;
// set profile picture
const profile_pic = await puter.fs.read(selected_file.path)
const profile_pic = await puter.fs.read(selected_file.path);
// blob to base64
const reader = new FileReader();
reader.readAsDataURL(profile_pic);
@@ -158,13 +296,13 @@ export default {
ctx.drawImage(img, 0, 0, 150, 150);
const base64data = canvas.toDataURL('image/png');
// update profile picture
$el_window.find('.profile-picture').css('background-image', 'url(' + html_encode(base64data) + ')');
$('.profile-image').css('background-image', 'url(' + html_encode(base64data) + ')');
$el_window.find('.profile-picture').css('background-image', `url(${html_encode(base64data)})`);
$('.profile-image').css('background-image', `url(${html_encode(base64data)})`);
$('.profile-image').addClass('profile-image-has-picture');
// update profile picture
update_profile(window.user.username, {picture: base64data})
}
}
})
update_profile(window.user.username, { picture: base64data });
};
};
});
},
};

View File

@@ -25,21 +25,23 @@ export default {
title_i18n_key: 'language',
icon: 'language.svg',
html: () => {
let h = `<h1>${i18n('language')}</h1>`;
// search
h += `<div class="search-container" style="margin-bottom: 10px;">
<input type="text" class="search search-language" placeholder="${i18n('search')}">
</div>`;
// list of languages
const available_languages = window.listSupportedLanguages();
h += `<div class="language-list">`;
for (let lang of available_languages) {
h += `<div class="language-item ${window.locale === lang.code ? 'active': ''}" data-lang="${lang.code}" data-english-name="${html_encode(lang.english_name)}">${html_encode(lang.name)}<img class="checkmark" src="${window.icons['checkmark.svg']}"></div>`;
}
h += `</div>`;
return h;
const languageItems = available_languages.map(lang => `
<div class="language-item ${window.locale === lang.code ? 'active': ''}" data-lang="${lang.code}" data-english-name="${html_encode(lang.english_name)}">
${html_encode(lang.name)}
<img class="checkmark" src="${window.icons['checkmark.svg']}">
</div>
`).join('');
return `
<h1 class="settings-section-header">${i18n('language')}</h1>
<div class="search-container">
<input type="text" class="search search-language" placeholder="${i18n('search')}">
</div>
<div class="language-list">
${languageItems}
</div>
`;
},
init: ($el_window) => {
$el_window.on('click', '.language-item', function(){

View File

@@ -19,54 +19,52 @@
import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
import UIWindowDesktopBGSettings from '../UIWindowDesktopBGSettings.js';
import build_settings_card from './helpers/build_settings_card.js';
// About
export default {
id: 'personalization',
title_i18n_key: 'personalization',
icon: 'palette-outline.svg',
html: () => {
return `
<h1>${i18n('personalization')}</h1>
<div class="settings-card">
<strong>${i18n('background')}</strong>
<div style="flex-grow:1;">
<button class="button change-background" style="float:right;">${i18n('change')}</button>
</div>
</div>
<div class="settings-card">
<strong>${i18n('ui_colors')}</strong>
<div style="flex-grow:1;">
<button class="button change-ui-colors" style="float:right;">${i18n('change')}</button>
</div>
</div>
<div class="settings-card">
<strong style="flex-grow:1;">${i18n('clock_visibility')}</strong>
<select class="change-clock-visible" style="margin-left: 10px; max-width: 300px;">
<option value="auto">${i18n('clock_visible_auto')}</option>
<option value="hide">${i18n('clock_visible_hide')}</option>
<option value="show">${i18n('clock_visible_show')}</option>
</select>
</div>
`;
<h1 class="settings-section-header">${i18n('personalization')}</h1>
${build_settings_card({
label: i18n('background'),
control: `<button class="button change-background" aria-label="${i18n('change')} ${i18n('background')}">${i18n('change')}</button>`,
})}
${build_settings_card({
label: i18n('ui_colors'),
control: `<button class="button change-ui-colors" aria-label="${i18n('change')} ${i18n('ui_colors')}">${i18n('change')}</button>`,
})}
${build_settings_card({
label: i18n('clock_visibility'),
control: `
<select class="change-clock-visible">
<option value="auto">${i18n('option_auto')}</option>
<option value="hide">${i18n('option_hide')}</option>
<option value="show">${i18n('option_show')}</option>
</select>
`,
})}
`;
},
init: ($el_window) => {
$el_window.find('.change-ui-colors').on('click', function (e) {
$el_window.find('.change-ui-colors').on('click', function(e) {
UIWindowThemeDialog({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
$el_window.find('.change-background').on('click', function (e) {
$el_window.find('.change-background').on('click', function(e) {
UIWindowDesktopBGSettings({
window_options:{
window_options: {
parent_uuid: $el_window.attr('data-element_uuid'),
disable_parent_window: true,
parent_center: true,
}
},
});
});
@@ -74,6 +72,10 @@ export default {
window.change_clock_visible(this.value);
});
// Set initial select value
const currentClockSetting = window.user_preferences?.clock_visible || 'auto';
$el_window.find('select.change-clock-visible').val(currentClockSetting);
window.change_clock_visible();
},
};

View File

@@ -1,169 +0,0 @@
/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import TeePromise from "../../util/TeePromise.js";
import UIComponentWindow from "../UIComponentWindow.js";
import UIWindow2FASetup from "../UIWindow2FASetup.js";
export default {
id: 'security',
title_i18n_key: 'security',
icon: 'shield.svg',
html: () => {
let h = `<h1>${i18n('security')}</h1>`;
let user = window.user;
// change password button
if(!user.is_temp){
h += `<div class="settings-card">`;
h += `<strong>${i18n('password')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button change-password" style="float:right;">${i18n('change_password')}</button>`;
h += `</div>`;
h += `</div>`;
}
// session manager
h += `<div class="settings-card">`;
h += `<strong>${i18n('sessions')}</strong>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button manage-sessions" style="float:right;">${i18n('manage_sessions')}</button>`;
h += `</div>`;
h += `</div>`;
// configure 2FA
if(!user.is_temp && user.email_confirmed){
h += `<div class="settings-card settings-card-security ${user.otp ? 'settings-card-success' : 'settings-card-warning'}">`;
h += `<div>`;
h += `<strong style="display:block;">${i18n('two_factor')}</strong>`;
h += `<span class="user-otp-state" style="display:block; margin-top:5px;">${
i18n(user.otp ? 'two_factor_enabled' : 'two_factor_disabled')
}</span>`;
h += `</div>`;
h += `<div style="flex-grow:1;">`;
h += `<button class="button enable-2fa" style="float:right;${user.otp ? 'display:none;' : ''}">${i18n('enable_2fa')}</button>`;
h += `<button class="button disable-2fa" style="float:right;${user.otp ? '' : 'display:none;'}">${i18n('disable_2fa')}</button>`;
h += `</div>`;
h += `</div>`;
}
return h;
},
init: ($el_window) => {
$el_window.find('.enable-2fa').on('click', async function (e) {
const { promise } = await UIWindow2FASetup();
const tfa_was_enabled = await promise;
if ( tfa_was_enabled ) {
$el_window.find('.enable-2fa').hide();
$el_window.find('.disable-2fa').show();
$el_window.find('.user-otp-state').text(i18n('two_factor_enabled'));
$el_window.find('.settings-card-security').removeClass('settings-card-warning');
$el_window.find('.settings-card-security').addClass('settings-card-success');
}
return;
});
$el_window.find('.disable-2fa').on('click', async function (e) {
let win, password_entry;
const password_confirm_promise = new TeePromise();
const try_password = async () => {
const value = password_entry.get('value');
const resp = await fetch(`${window.api_origin}/user-protected/disable-2fa`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
password: value,
}),
});
if ( resp.status !== 200 ) {
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
let message; try {
message = (await resp.json()).message;
} catch (e) {}
message = message || i18n('error_unknown_cause');
password_entry.set('error', message);
return;
}
password_confirm_promise.resolve(true);
$(win).close();
}
let h = '';
h += `<div style="display: flex; flex-direction: column; gap: 20pt; justify-content: center;">`;
h += `<div>`;
h += `<h3 style="text-align:center; font-weight: 500; font-size: 20px;">${i18n('disable_2fa_confirm')}</h3>`;
h += `<p style="text-align:center; padding: 0 20px;">${i18n('disable_2fa_instructions')}</p>`;
h += `</div>`;
h += `<div style="display: flex; gap: 5pt;">`;
h += `<input type="password" class="password-entry" />`;
h += `<button class="button confirm-disable-2fa">${i18n('disable_2fa')}</button>`;
h += `<button class="button secondary cancel-disable-2fa">${i18n('cancel')}</button>`;
h += `</div>`;
h += `</div>`;
win = await UIComponentWindow({
html: h,
width: 500,
backdrop: true,
is_resizable: false,
body_css: {
width: 'initial',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
padding: '20px',
},
});
// Set up event listeners
const $win = $(win);
const $password_entry = $win.find('.password-entry');
$password_entry.on('keypress', (e) => {
if(e.which === 13) { // Enter key
try_password();
}
});
$win.find('.confirm-disable-2fa').on('click', () => {
try_password();
});
$win.find('.cancel-disable-2fa').on('click', () => {
password_confirm_promise.resolve(false);
$win.close();
});
$password_entry.focus();
const ok = await password_confirm_promise;
if ( ! ok ) return;
$el_window.find('.enable-2fa').show();
$el_window.find('.disable-2fa').hide();
$el_window.find('.user-otp-state').text(i18n('two_factor_disabled'));
$el_window.find('.settings-card-security').removeClass('settings-card-success');
$el_window.find('.settings-card-security').addClass('settings-card-warning');
});
}
}

View File

@@ -7,12 +7,12 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@@ -24,41 +24,45 @@ export default {
icon: 'speedometer-outline.svg',
html: () => {
return `
<h1>${i18n('usage')}<button class="update-usage-details" style="float:right;"><svg class="update-usage-details-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/> <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/> </svg></button></h1>
<h1 class="settings-section-header">${i18n('usage')}<button class="update-usage-details"><svg class="update-usage-details-icon" xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16"> <path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/> <path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/> </svg></button></h1>
<div class="driver-usage">
<div class="driver-usage-header">
<h3 style="margin:0; font-size: 14px; flex-grow: 1; font-weight: 500;">${i18n('Storage')}</h3>
<div style="font-size: 13px; margin-bottom: 3px; opacity:0.85;">
<span id="storage-used"></span>
<span> used of </span>
<span id="storage-capacity"></span>
<span id="storage-puter-used-w" style="display:none;">&nbsp;(<span id="storage-puter-used"></span> ${i18n('storage_puter_used')})</span>
<div class="storage-section">
<div class="storage-header">
<h3 class="storage-title">${i18n('Storage')}</h3>
<div class="storage-stats">
<span id="storage-used" class="storage-amount"></span>
<span class="storage-separator">/</span>
<span id="storage-capacity" class="storage-amount"></span>
</div>
</div>
<div class="usage-progbar-wrapper">
<div class="usage-progbar" id="storage-bar">
<span class="usage-progbar-percent" id="storage-used-percent"></span>
</div>
</div>
<div id="storage-puter-used-w" class="storage-puter-info">
<span id="storage-puter-used"></span>
</div>
</div>
<div id="storage-bar-wrapper">
<span id="storage-used-percent"></span>
<div id="storage-bar"></div>
<div id="storage-bar-host"></div>
</div>
<div class="driver-usage-container" style="margin-top: 30px;">
<div class="driver-usage-container">
<div class="driver-usage-header">
<h3 style="margin:0; font-size: 14px; flex-grow: 1; font-weight: 500;">${i18n('Resources')}</h3>
<div style="font-size: 13px; margin-bottom: 3px; opacity:0.85;">
<h3 class="driver-usage-title">${i18n('Resources')}</h3>
<div class="driver-usage-stats">
<span id="total-usage"></span>
<span> used of </span>
<span id="total-capacity"></span>
</div>
</div>
<div class="usage-progbar-wrapper">
<div class="usage-progbar" style="width: 0;">
<span class="usage-progbar-percent"></span>
<div class="usage-progbar" id="resources-bar">
<span class="usage-progbar-percent" id="resources-used-percent"></span>
</div>
</div>
<div class="driver-usage-details" style="margin-top: 5px; font-size: 13px; cursor: pointer;">
<div class="driver-usage-details driver-usage-details-section">
<div class="caret"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-right-fill" viewBox="0 0 16 16"><path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/></svg></div>
<span class="driver-usage-details-text disable-user-select">View usage details</span>
<span class="driver-usage-details-text disable-user-select">${i18n('view_usage_details')}</span>
</div>
<div class="driver-usage-details-content hide-scrollbar" style="display: none;">
<div class="driver-usage-details-content hide-scrollbar">
</div>
</div>
</div>`;
@@ -76,10 +80,10 @@ $(document).on('click', '.driver-usage-details', function() {
$('.driver-usage-details').toggleClass('active');
// change the text of the driver-usage-details-text depending on the class
if($('.driver-usage-details').hasClass('active')){
$('.driver-usage-details-text').text('Hide usage details');
}else{
$('.driver-usage-details-text').text('View usage details');
if ( $('.driver-usage-details').hasClass('active') ){
$('.driver-usage-details-text').text(i18n('hide_usage_details'));
} else {
$('.driver-usage-details-text').text(i18n('view_usage_details'));
}
});
@@ -87,7 +91,7 @@ async function update_usage_details($el_window){
// Add spinning animation and record start time
const startTime = Date.now();
$($el_window).find('.update-usage-details-icon').css('animation', 'spin 1s linear infinite');
const monthlyUsagePromise = puter.auth.getMonthlyUsage().then(res => {
let monthlyAllowance = res.allowanceInfo?.monthUsageAllowance;
let remaining = res.allowanceInfo?.remaining;
@@ -96,76 +100,66 @@ async function update_usage_details($el_window){
$('#total-usage').html(window.number_format(totalUsage / 100_000_000, { decimals: 2, prefix: '$' }));
$('#total-capacity').html(window.number_format(monthlyAllowance / 100_000_000, { decimals: 2, prefix: '$' }));
$('.usage-progbar-percent').html(totalUsagePercentage + '%');
$('.usage-progbar').css('width', totalUsagePercentage + '%');
// build the table for the usage details
let h = '<table class="driver-usage-details-content-table">';
$('#resources-used-percent').html(`${totalUsagePercentage}%`);
$('#resources-bar').css('width', `${totalUsagePercentage}%`);
h += `<thead>
<tr>
<th>Resource</th>
<th>Units</th>
<th>Cost</th>
</tr>
</thead>`;
const tableRows = Object.keys(res.usage)
.filter(key => typeof res.usage[key] === 'object')
.map(key => {
let units = res.usage[key].units;
h += `<tbody>`;
for(let key in res.usage){
// value must be object
if(typeof res.usage[key] !== 'object')
continue;
// Bytes should be formatted as human readable
if (key.startsWith('filesystem:') && key.endsWith(':bytes')) {
units = window.byte_format(units);
}
// Everything else should be formatted as a number
else {
units = window.number_format(units, { decimals: 0, thousandSeparator: ',' });
}
// get the units
let units = res.usage[key].units;
return `
<tr>
<td title="${key}">${key}</td>
<td>${units}</td>
<td>${window.number_format(res.usage[key].cost / 100_000_000, { decimals: 2, prefix: '$' })}</td>
</tr>
`;
}).join('');
// Bytes should be formatted as human readable
if(key.startsWith('filesystem:') && key.endsWith(':bytes')){
units = window.byte_format(units);
}
// Everything else should be formatted as a number
else{
units = window.number_format(units, {decimals: 0, thousandSeparator: ','});
}
h += `
<tr>
<td>${key}</td>
<td>${units}</td>
<td>${window.number_format(res.usage[key].cost / 100_000_000, { decimals: 2, prefix: '$' })}</td>
</tr>`;
}
h += `</tbody>`;
h += '</table>';
const h = `
<table class="driver-usage-details-content-table">
<thead>
<tr>
<th>${i18n('resource')}</th>
<th>${i18n('resource_units')}</th>
<th>${i18n('resource_cost')}</th>
</tr>
</thead>
<tbody>
${tableRows}
</tbody>
</table>
`;
$('.driver-usage-details-content').html(h);
});
const spacePromise = puter.fs.space().then(res => {
let usage_percentage = (res.used / res.capacity * 100).toFixed(0);
const used = res.host_used || res.used;
let usage_percentage = (used / res.capacity * 100).toFixed(0);
usage_percentage = usage_percentage > 100 ? 100 : usage_percentage;
let general_used = res.used;
let host_usage_percentage = 0;
if ( res.host_used ) {
$('#storage-puter-used').html(window.byte_format(res.used));
$('#storage-puter-used-w').show();
general_used = res.host_used;
host_usage_percentage = ((res.host_used - res.used) / res.capacity * 100).toFixed(0);
$('#storage-puter-used').html(`Puter is using ${window.byte_format(res.used)}`);
$('#storage-puter-used-w').addClass('visible');
}
$('#storage-used').html(window.byte_format(general_used));
$('#storage-used').html(window.byte_format(used));
$('#storage-capacity').html(window.byte_format(res.capacity));
$('#storage-used-percent').html(
usage_percentage + '%' +
(host_usage_percentage > 0
? ' / ' + host_usage_percentage + '%' : '')
);
$('#storage-bar').css('width', usage_percentage + '%');
$('#storage-bar-host').css('width', host_usage_percentage + '%');
if (usage_percentage >= 100) {
$('#storage-used-percent').html(`${usage_percentage}%`);
$('#storage-bar').css('width', `${usage_percentage}%`);
if ( usage_percentage >= 100 ) {
$('#storage-bar').css({
'border-top-right-radius': '3px',
'border-bottom-right-radius': '3px',
@@ -175,14 +169,14 @@ async function update_usage_details($el_window){
// Wait for both promises to complete
await Promise.all([monthlyUsagePromise, spacePromise]);
// Ensure spinning continues for at least 1 second
const elapsed = Date.now() - startTime;
const minDuration = 1000; // 1 second
if (elapsed < minDuration) {
if ( elapsed < minDuration ) {
await new Promise(resolve => setTimeout(resolve, minDuration - elapsed));
}
// Remove spinning animation
$($el_window).find('.update-usage-details-icon').css('animation', '');
}

View File

@@ -7,19 +7,19 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Placeholder from '../../util/Placeholder.js';
import PasswordEntry from '../Components/PasswordEntry.js';
import UIWindow from '../UIWindow.js'
import UIWindow from '../UIWindow.js';
// TODO: DRY: We could specify a validator and endpoint instead of writing
// a DOM tree and event handlers for each of these. (low priority)
@@ -30,26 +30,21 @@ async function UIWindowChangeEmail(options){
const place_password_entry = Placeholder();
const internal_id = window.uuidv4();
let h = '';
h += `<div class="change-email" style="padding: 20px; border-bottom: 1px solid #ced7e1;">`;
// error msg
h += `<div class="form-error-msg"></div>`;
// success msg
h += `<div class="form-success-msg"></div>`;
// new email
h += `<div style="overflow: hidden; margin-top: 10px; margin-bottom: 30px;">`;
h += `<label for="confirm-new-email-${internal_id}">${i18n('new_email')}</label>`;
h += `<input id="confirm-new-email-${internal_id}" type="text" name="new-email" class="new-email" autocomplete="off" />`;
h += `</div>`;
// password confirmation
h += `<div style="overflow: hidden; margin-top: 10px; margin-bottom: 30px;">`;
h += `<label>${i18n('account_password')}</label>`;
h += `${place_password_entry.html}`;
h += `</div>`;
// Change Email
h += `<button class="change-email-btn button button-primary button-block button-normal">${i18n('change_email')}</button>`;
h += `</div>`;
const h = `
<div class="change-email settings-form-container">
<div class="form-error-msg" role="alert" aria-live="polite"></div>
<div class="form-success-msg" role="alert" aria-live="polite"></div>
<div class="form-field">
<label class="form-label" for="confirm-new-email-${internal_id}">${i18n('new_email')}</label>
<input id="confirm-new-email-${internal_id}" type="text" name="new-email" class="new-email form-input" autocomplete="off" aria-required="true" />
</div>
<div class="form-field">
<label class="form-label">${i18n('account_password')}</label>
${place_password_entry.html}
</div>
<button class="change-email-btn button button-primary button-block button-normal" aria-label="${i18n('change_email')}">${i18n('change_email')}</button>
</div>
`;
const el_window = await UIWindow({
title: i18n('change_email'),
@@ -68,78 +63,64 @@ async function UIWindowChangeEmail(options){
init_center: true,
allow_native_ctxmenu: false,
allow_user_select: false,
width: 350,
width: 380,
height: 'auto',
dominant: true,
show_in_taskbar: false,
onAppend: function(this_window){
$(this_window).find(`.new-email`).get(0)?.focus({preventScroll:true});
$(this_window).find('.new-email').get(0)?.focus({ preventScroll: true });
},
window_class: 'window-publishWebsite',
window_class: 'window-change-email',
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
},
...options.window_options
})
},
...options.window_options,
});
password_entry.attach(place_password_entry);
$(el_window).find('.change-email-btn').on('click', function(e){
// hide previous error/success msg
$(el_window).find('.form-success-msg, .form-success-msg').hide();
$(el_window).find('.form-error-msg, .form-success-msg').removeClass('visible');
const new_email = $(el_window).find('.new-email').val();
const password = $(el_window).find('.password').val();
if(!new_email){
$(el_window).find('.form-error-msg').html(i18n('all_fields_required'));
$(el_window).find('.form-error-msg').fadeIn();
if ( !new_email ){
$(el_window).find('.form-error-msg').html(i18n('all_fields_required')).addClass('visible');
return;
}
$(el_window).find('.form-error-msg').hide();
// disable button
$(el_window).find('.change-email-btn').addClass('disabled');
// disable input
$(el_window).find('.change-email-btn').addClass('loading');
$(el_window).find('.new-email').attr('disabled', true);
$.ajax({
url: window.api_origin + "/user-protected/change-email",
url: `${window.api_origin}/user-protected/change-email`,
type: 'POST',
async: true,
headers: {
"Authorization": "Bearer "+window.auth_token
'Authorization': `Bearer ${window.auth_token}`,
},
contentType: "application/json",
data: JSON.stringify({
new_email: new_email,
contentType: 'application/json',
data: JSON.stringify({
new_email: new_email,
password: password_entry.get('value'),
}),
success: function (data){
$(el_window).find('.form-success-msg').html(i18n('email_change_confirmation_sent'));
$(el_window).find('.form-success-msg').fadeIn();
}),
success: function(data){
$(el_window).find('.change-email-btn').removeClass('loading');
$(el_window).find('.form-success-msg').html(i18n('email_change_confirmation_sent')).addClass('visible');
$(el_window).find('input').val('');
// update email
window.user.email = new_email;
// enable button
$(el_window).find('.change-email-btn').removeClass('disabled');
// enable input
$(el_window).find('.new-email').attr('disabled', false);
},
error: function (err){
$(el_window).find('.form-error-msg').html(html_encode(err.responseJSON?.message));
$(el_window).find('.form-error-msg').fadeIn();
// enable button
$(el_window).find('.change-email-btn').removeClass('disabled');
// enable input
error: function(err){
$(el_window).find('.change-email-btn').removeClass('loading');
$(el_window).find('.form-error-msg').html(html_encode(err.responseJSON?.message)).addClass('visible');
$(el_window).find('.new-email').attr('disabled', false);
}
});
})
},
});
});
}
export default UIWindowChangeEmail
export default UIWindowChangeEmail;

View File

@@ -7,31 +7,32 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import UIWindow from '../UIWindow.js'
import UIWindowFinalizeUserDeletion from './UIWindowFinalizeUserDeletion.js'
import UIWindow from '../UIWindow.js';
import UIWindowFinalizeUserDeletion from './UIWindowFinalizeUserDeletion.js';
async function UIWindowConfirmUserDeletion(options){
return new Promise(async (resolve) => {
options = options ?? {};
let h = '';
h += `<div style="padding: 20px;">`;
h += `<div class="generic-close-window-button disable-user-select"> &times; </div>`;
h += `<img src="${window.icons['danger.svg']}" class="account-deletion-confirmation-icon">`;
h += `<p class="account-deletion-confirmation-prompt">${i18n('confirm_delete_user')}</p>`;
h += `<button class="button button-block button-danger proceed-with-user-deletion">${i18n('proceed_with_account_deletion')}</button>`;
h += `<button class="button button-block button-secondary cancel-user-deletion">${i18n('cancel')}</button>`;
h += `</div>`;
const h = `
<div class="deletion-dialog-content">
<div class="generic-close-window-button disable-user-select" role="button" aria-label="${i18n('close')}"> &times; </div>
<img src="${window.icons['danger.svg']}" class="account-deletion-confirmation-icon" alt="${i18n('warning')}" role="img">
<p class="account-deletion-confirmation-prompt">${i18n('confirm_delete_user')}</p>
<button class="button button-block button-danger proceed-with-user-deletion" aria-label="${i18n('proceed_with_account_deletion')}">${i18n('proceed_with_account_deletion')}</button>
<button class="button button-block button-secondary cancel-user-deletion" aria-label="${i18n('cancel')}">${i18n('cancel')}</button>
</div>
`;
const el_window = await UIWindow({
title: i18n('confirm_delete_user_title'),
@@ -78,7 +79,7 @@ async function UIWindowConfirmUserDeletion(options){
UIWindowFinalizeUserDeletion();
$(el_window).close();
});
})
});
}
export default UIWindowConfirmUserDeletion;

View File

@@ -23,36 +23,27 @@ async function UIWindowFinalizeUserDeletion(options){
return new Promise(async (resolve) => {
options = options ?? {};
let h = '';
// if user is temporary, ask them to type in 'confirm' to delete their account
if(window.user.is_temp){
h += `<div style="padding: 20px;">`;
h += `<div class="generic-close-window-button disable-user-select"> &times; </div>`;
h += `<img src="${window.icons['danger.svg']}" class="account-deletion-confirmation-icon">`;
h += `<p class="account-deletion-confirmation-prompt">${i18n('type_confirm_to_delete_account')}</p>`;
// error message
h += `<div class="error-message"></div>`;
// input field
h += `<input type="text" class="confirm-temporary-user-deletion" placeholder="${i18n('type_confirm_to_delete_account')}">`;
h += `<button class="button button-block button-danger proceed-with-user-deletion">${i18n('delete_account')}</button>`;
h += `<button class="button button-block button-secondary cancel-user-deletion">${i18n('cancel')}</button>`;
h += `</div>`;
}
// otherwise ask for password
else{
h += `<div style="padding: 20px;">`;
h += `<div class="generic-close-window-button disable-user-select"> &times; </div>`;
h += `<img src="${window.icons['danger.svg']}" class="account-deletion-confirmation-icon">`;
h += `<p class="account-deletion-confirmation-prompt">${i18n('enter_password_to_confirm_delete_user')}</p>`;
// error message
h += `<div class="error-message"></div>`;
// input field
h += `<input type="password" class="confirm-user-deletion-password" placeholder="${i18n('current_password')}">`;
h += `<button class="button button-block button-danger proceed-with-user-deletion">${i18n('delete_account')}</button>`;
h += `<button class="button button-block button-secondary cancel-user-deletion">${i18n('cancel')}</button>`;
h += `</div>`;
}
const h = window.user.is_temp ? `
<div class="deletion-dialog-content">
<div class="generic-close-window-button disable-user-select" role="button" aria-label="${i18n('close')}"> &times; </div>
<img src="${window.icons['danger.svg']}" class="account-deletion-confirmation-icon" alt="${i18n('warning')}" role="img">
<p class="account-deletion-confirmation-prompt">${i18n('type_confirm_to_delete_account')}</p>
<div class="form-error-msg" role="alert" aria-live="polite"></div>
<input type="text" class="confirm-temporary-user-deletion form-input" placeholder="${i18n('type_confirm_to_delete_account')}" aria-label="${i18n('type_confirm_to_delete_account')}" aria-required="true">
<button class="button button-block button-danger proceed-with-user-deletion" aria-label="${i18n('delete_account')}">${i18n('delete_account')}</button>
<button class="button button-block button-secondary cancel-user-deletion" aria-label="${i18n('cancel')}">${i18n('cancel')}</button>
</div>
` : `
<div class="deletion-dialog-content">
<div class="generic-close-window-button disable-user-select" role="button" aria-label="${i18n('close')}"> &times; </div>
<img src="${window.icons['danger.svg']}" class="account-deletion-confirmation-icon" alt="${i18n('warning')}" role="img">
<p class="account-deletion-confirmation-prompt">${i18n('enter_password_to_confirm_delete_user')}</p>
<div class="form-error-msg" role="alert" aria-live="polite"></div>
<input type="password" class="confirm-user-deletion-password form-input" placeholder="${i18n('current_password')}" aria-label="${i18n('current_password')}" aria-required="true">
<button class="button button-block button-danger proceed-with-user-deletion" aria-label="${i18n('delete_account')}">${i18n('delete_account')}</button>
<button class="button button-block button-secondary cancel-user-deletion" aria-label="${i18n('cancel')}">${i18n('cancel')}</button>
</div>
`;
const el_window = await UIWindow({
title: i18n('confirm_delete_user_title'),
@@ -92,28 +83,25 @@ async function UIWindowFinalizeUserDeletion(options){
});
$(el_window).find('.proceed-with-user-deletion').on('click', function(){
$(el_window).find('.error-message').hide();
// if user is temporary, check if they typed 'confirm'
$(el_window).find('.form-error-msg').removeClass('visible');
if(window.user.is_temp){
const confirm = $(el_window).find('.confirm-temporary-user-deletion').val().toLowerCase();
// user must type 'confirm' or the translation of 'confirm' to delete their account
if(confirm !== 'confirm' && confirm !== i18n('confirm').toLowerCase()){
$(el_window).find('.error-message').html(i18n('type_confirm_to_delete_account'), false);
$(el_window).find('.error-message').show();
$(el_window).find('.form-error-msg').html(i18n('type_confirm_to_delete_account')).addClass('visible');
return;
}
}
// otherwise, check if password is correct
else{
if($(el_window).find('.confirm-user-deletion-password').val() === ''){
$(el_window).find('.error-message').html(i18n('all_fields_required'), false);
$(el_window).find('.error-message').show();
$(el_window).find('.form-error-msg').html(i18n('all_fields_required')).addClass('visible');
return;
}
}
// delete user
$(el_window).find('.proceed-with-user-deletion').addClass('loading');
$.ajax({
url: window.api_origin + "/delete-own-user",
type: 'POST',
@@ -130,21 +118,18 @@ async function UIWindowFinalizeUserDeletion(options){
window.logout();
},
400: function(){
$(el_window).find('.error-message').html(i18n('incorrect_password'));
$(el_window).find('.error-message').show();
$(el_window).find('.proceed-with-user-deletion').removeClass('loading');
$(el_window).find('.form-error-msg').html(i18n('incorrect_password')).addClass('visible');
}
},
success: function(data){
if(data.success){
// mark user as deleted
window.user.deleted = true;
// log user out
window.logout();
}
else{
$(el_window).find('.error-message').html(html_encode(data.error));
$(el_window).find('.error-message').show();
$(el_window).find('.proceed-with-user-deletion').removeClass('loading');
$(el_window).find('.form-error-msg').html(html_encode(data.error)).addClass('visible');
}
}
});

View File

@@ -16,8 +16,8 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import UIAlert from "./UIAlert.js";
import UIWindow from "./UIWindow.js";
import UIAlert from "../UIAlert.js";
import UIWindow from "../UIWindow.js";
const UIWindowManageSessions = async function UIWindowManageSessions (options) {
options = options ?? {};
@@ -30,8 +30,6 @@ const UIWindowManageSessions = async function UIWindowManageSessions (options) {
uid: null,
is_dir: false,
message: 'message',
// body_icon: options.body_icon,
// backdrop: options.backdrop ?? false,
is_droppable: false,
has_head: true,
selectable_body: false,
@@ -40,7 +38,15 @@ const UIWindowManageSessions = async function UIWindowManageSessions (options) {
window_class: 'window-session-manager',
dominant: true,
body_content: '',
// width: 600,
width: 500,
height: "auto",
body_css: {
padding: '20px',
'background-color': 'rgb(245 247 249)',
},
window_css: {
'background-color': 'rgb(245 247 249)',
},
...options.window_options,
});
@@ -51,42 +57,64 @@ const UIWindowManageSessions = async function UIWindowManageSessions (options) {
el.classList.add('current-session');
}
el.dataset.uuid = session.uuid;
// '<pre>' +
// JSON.stringify(session, null, 2) +
// '</pre>';
const el_uuid = document.createElement('div');
el_uuid.textContent = session.uuid;
el.appendChild(el_uuid);
el_uuid.classList.add('session-widget-uuid');
// Helper function to format relative time
const getRelativeTime = (timestamp) => {
if (!timestamp) return i18n('unknown');
const now = Date.now();
const time = new Date(timestamp).getTime();
const diff = now - time;
const el_meta = document.createElement('div');
el_meta.classList.add('session-widget-meta');
for ( const key in session.meta ) {
const el_entry = document.createElement('div');
el_entry.classList.add('session-widget-meta-entry');
const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
const el_key = document.createElement('div');
el_key.textContent = key;
el_key.classList.add('session-widget-meta-key');
el_entry.appendChild(el_key);
if (days > 0) return `${days} ${i18n(days > 1 ? 'days' : 'day')} ${i18n('ago')}`;
if (hours > 0) return `${hours} ${i18n(hours > 1 ? 'hours' : 'hour')} ${i18n('ago')}`;
if (minutes > 0) return `${minutes} ${i18n(minutes > 1 ? 'minutes' : 'minute')} ${i18n('ago')}`;
return i18n('just_now');
};
const el_value = document.createElement('div');
el_value.textContent = session.meta[key];
el_value.classList.add('session-widget-meta-value');
el_entry.appendChild(el_value);
// Helper function to detect device type from user agent
const getDeviceInfo = (userAgent) => {
if (!userAgent) return { type: 'desktop', name: i18n('device_desktop') };
el_meta.appendChild(el_entry);
}
el.appendChild(el_meta);
const ua = userAgent.toLowerCase();
if (ua.includes('mobile') || ua.includes('android') || ua.includes('iphone')) {
return { type: 'mobile', name: i18n('device_mobile') };
}
if (ua.includes('tablet') || ua.includes('ipad')) {
return { type: 'tablet', name: i18n('device_tablet') };
}
return { type: 'desktop', name: i18n('device_desktop') };
};
const el_actions = document.createElement('div');
el_actions.classList.add('session-widget-actions');
const deviceInfo = getDeviceInfo(session.meta?.['user-agent']);
const el_btn_revoke = document.createElement('button');
el_btn_revoke.textContent = i18n('ui_revoke');
el_btn_revoke.classList.add('button', 'button-danger');
el_btn_revoke.addEventListener('click', async () => {
const el_content = document.createElement('div');
el_content.classList.add('session-widget-content');
const el_main = document.createElement('div');
el_main.classList.add('session-widget-main');
el_main.innerHTML = `
<div class="session-widget-title">
${deviceInfo.name}
${session.current ? `<span class="current-badge">${i18n('current')}</span>` : ''}
</div>
<div class="session-widget-details">
<span class="session-widget-time">${getRelativeTime(session.meta?.timestamp || session.created_at)}</span>
${session.meta?.location || session.meta?.ip ? `<span class="session-widget-separator">•</span><span class="session-widget-location">${session.meta?.location || session.meta?.ip}</span>` : ''}
</div>
`;
el_content.appendChild(el_main);
if (!session.current) {
const el_btn_revoke = document.createElement('button');
el_btn_revoke.textContent = i18n('ui_revoke');
el_btn_revoke.classList.add('button', 'button-small', 'button-danger');
el_btn_revoke.addEventListener('click', async () => {
try{
const alert_resp = await UIAlert({
message: i18n('confirm_session_revoke'),
@@ -124,13 +152,15 @@ const UIWindowManageSessions = async function UIWindowManageSessions (options) {
el.remove();
return;
}
UIAlert({ message: await resp.text() }).appendTo(w_body);
UIAlert({ message: await resp.text() });
} catch ( e ) {
UIAlert({ message: e.toString() }).appendTo(w_body);
UIAlert({ message: e.toString() });
}
});
el_actions.appendChild(el_btn_revoke);
el.appendChild(el_actions);
el_content.appendChild(el_btn_revoke);
}
el.appendChild(el_content);
return {
appendTo (parent) {
@@ -168,6 +198,14 @@ const UIWindowManageSessions = async function UIWindowManageSessions (options) {
w_body.classList.add('session-manager-list');
// Add header
const header = document.createElement('div');
header.classList.add('session-manager-header');
header.innerHTML = `
<p class="session-manager-description">${i18n('session_manager_description')}</p>
`;
w_body.appendChild(header);
reload_sessions();
const interval = setInterval(reload_sessions, 8000);
w.on_close = () => {

View File

@@ -7,18 +7,18 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Placeholder from '../../util/Placeholder.js';
import UIWindow from '../UIWindow.js'
import UIWindow from '../UIWindow.js';
def(Symbol('TSettingsTab'), 'ui.traits.TSettingsTab');
@@ -31,43 +31,58 @@ async function UIWindowSettings(options){
const tabs = svc_settings.get_tabs();
const tab_placeholders = [];
let h = '';
const savedSize = await puter.kv.get('settings_window_size').catch(() => null);
h += `<div class="settings-container">`;
h += `<div class="settings">`;
// sidebar toggle
h += `<button class="sidebar-toggle hidden-lg hidden-xl hidden-md"><div class="sidebar-toggle-button"><span></span><span></span><span></span></div></button>`;
// sidebar
h += `<div class="settings-sidebar disable-user-select disable-context-menu">`;
// if data-is_fullpage="1" show title saying "Settings"
if (options.window_options?.is_fullpage) {
h += `<div class="settings-sidebar-title">${i18n('settings')}</div>`;
}
const sidebarTitle = options.window_options?.is_fullpage
? `<div class="settings-sidebar-title">${i18n('settings')}</div>`
: '';
// sidebar items
h += `<div class="settings-sidebar-burger disable-context-menu disable-user-select" style="background-image: url(${window.icons['menu']});"></div>`;
tabs.forEach((tab, i) => {
h += `<div class="settings-sidebar-item disable-context-menu disable-user-select ${i === 0 ? 'active' : ''}" data-settings="${tab.id}" style="background-image: url(${window.icons[tab.icon]});">${i18n(tab.title_i18n_key)}</div>`;
});
h += `</div>`;
const sidebarItems = tabs.map((tab, i) => `
<div class="settings-sidebar-item disable-context-menu disable-user-select ${i === 0 ? 'active' : ''}"
data-settings="${tab.id}"
style="background-image: url(${window.icons[tab.icon]}); --icon-url: url(${window.icons[tab.icon]});">
${i18n(tab.title_i18n_key)}
</div>
`).join('');
// content
h += `<div class="settings-content-container">`;
const contentTabs = tabs.map((tab, i) => {
let content;
if (tab.factory || tab.dom) {
tab_placeholders[i] = Placeholder();
content = tab_placeholders[i].html;
} else {
content = tab.html();
}
return `
<div class="settings-content ${i === 0 ? 'active' : ''}" data-settings="${tab.id}">
${content}
</div>
`;
}).join('');
tabs.forEach((tab, i) => {
h += `<div class="settings-content ${i === 0 ? 'active' : ''}" data-settings="${tab.id}">`;
if ( tab.factory || tab.dom ) {
tab_placeholders[i] = Placeholder();
h += tab_placeholders[i].html;
} else {
h += tab.html();
}
h += `</div>`;
});
h += `</div>`;
h += `</div>`;
h += `</div>`;
const h = `
<div class="settings-container">
<div class="settings">
<div class="settings-backdrop hidden-lg hidden-xl hidden-md"></div>
<button class="sidebar-toggle hidden-lg hidden-xl hidden-md">
<div class="sidebar-toggle-button">
<span></span>
<span></span>
<span></span>
</div>
</button>
<div class="settings-sidebar disable-user-select disable-context-menu">
${sidebarTitle}
<div class="settings-sidebar-burger disable-context-menu disable-user-select"
style="background-image: url(${window.icons['menu']});"></div>
${sidebarItems}
</div>
<div class="settings-content-container">
${contentTabs}
</div>
</div>
</div>
`;
const el_window = await UIWindow({
title: 'Settings',
@@ -86,8 +101,10 @@ async function UIWindowSettings(options){
allow_native_ctxmenu: true,
allow_user_select: true,
backdrop: false,
width: 800,
height: 'auto',
width: savedSize?.width || 800,
height: savedSize?.height || 'auto',
minWidth: 480,
minHeight: 500,
dominant: true,
show_in_taskbar: false,
draggable_body: false,
@@ -99,9 +116,9 @@ async function UIWindowSettings(options){
body_css: {
width: 'initial',
height: '100%',
overflow: 'auto'
overflow: 'auto',
},
...options?.window_options??{}
...options?.window_options ?? {},
});
const $el_window = $(el_window);
tabs.forEach((tab, i) => {
@@ -119,9 +136,9 @@ async function UIWindowSettings(options){
});
// If options.tab is provided, open that tab
if (options.tab) {
if ( options.tab ) {
const $tabToOpen = $el_window.find(`.settings-sidebar-item[data-settings="${options.tab}"]`);
if ($tabToOpen.length > 0) {
if ( $tabToOpen.length > 0 ) {
setTimeout(() => {
$tabToOpen.trigger('click');
}, 50);
@@ -142,54 +159,88 @@ async function UIWindowSettings(options){
// Run on_show handlers
const tab = tabs.find((tab) => tab.id === settings);
if (tab?.on_show) {
if ( tab?.on_show ) {
tab.on_show($content);
}
})
// hide sidebar on mobile
const $settings = $this.closest('.settings');
$settings.find('.settings-sidebar').removeClass('active');
$settings.find('.sidebar-toggle').removeClass('active');
$settings.find('.settings-backdrop').removeClass('active');
});
$(el_window).on('click', '.sidebar-toggle', function(e) {
e.preventDefault();
e.stopPropagation();
const $settings = $(this).closest('.settings');
const isActive = $settings.find('.settings-sidebar').hasClass('active');
$settings.find('.settings-sidebar').toggleClass('active');
$settings.find('.sidebar-toggle').toggleClass('active');
$settings.find('.settings-backdrop').toggleClass('active');
// Prevent body scroll when sidebar is open
if ( !isActive ) {
$('body').css('overflow', 'hidden');
} else {
$('body').css('overflow', '');
}
});
$(el_window).on('click', '.settings-backdrop', function() {
const $settings = $(this).closest('.settings');
$settings.find('.settings-sidebar').removeClass('active');
$settings.find('.sidebar-toggle').removeClass('active');
$settings.find('.settings-backdrop').removeClass('active');
$('body').css('overflow', '');
});
$(el_window).on('click', function(e) {
const $target = $(e.target);
if ( !$target.closest('.settings-sidebar').length &&
!$target.closest('.sidebar-toggle').length &&
!$target.closest('.settings-backdrop').length ) {
const $settings = $(el_window).find('.settings');
if ( $settings.find('.settings-sidebar').hasClass('active') ) {
$settings.find('.settings-sidebar').removeClass('active');
$settings.find('.sidebar-toggle').removeClass('active');
$settings.find('.settings-backdrop').removeClass('active');
$('body').css('overflow', '');
}
}
});
$(el_window).on('resizestop', function() {
const width = $(el_window).width();
const height = $(el_window).height();
puter.kv.set('settings_window_size', { width, height });
});
const updateWindowSizeClasses = () => {
const $settings = $el_window.find('.settings');
const width = $el_window.width();
$settings.removeClass('window-xs window-sm window-md');
if (width < 576) {
$settings.addClass('window-xs');
} else if (width < 768) {
$settings.addClass('window-sm');
} else if (width < 992) {
$settings.addClass('window-md');
}
};
const resizeObserver = new ResizeObserver(() => {
updateWindowSizeClasses();
});
resizeObserver.observe(el_window);
updateWindowSizeClasses();
resolve(el_window);
});
}
$(document).on('mousedown', '.sidebar-toggle', function(e) {
e.preventDefault();
$('.settings-sidebar').toggleClass('active');
$('.sidebar-toggle-button').toggleClass('active');
// move sidebar toggle button
setTimeout(() => {
$('.sidebar-toggle').css({
left: $('.settings-sidebar').hasClass('active') ? 243 : 2
});
}, 10);
})
$(document).on('click', '.settings-sidebar-item', function(e) {
// hide sidebar
$('.settings-sidebar').removeClass('active');
// move sidebar toggle button ro the right
setTimeout(() => {
$('.sidebar-toggle').css({
left: 2
});
}, 10);
})
// clicking anywhere on the page will close the sidebar
$(document).on('click', function(e) {
// print event target class
if (!$(e.target).closest('.settings-sidebar').length && !$(e.target).closest('.sidebar-toggle-button').length && !$(e.target).hasClass('sidebar-toggle-button') && !$(e.target).hasClass('sidebar-toggle')) {
$('.settings-sidebar').removeClass('active');
$('.sidebar-toggle-button').removeClass('active');
// move sidebar toggle button ro the right
setTimeout(() => {
$('.sidebar-toggle').css({
left: 2
});
}, 10);
}
})
export default UIWindowSettings
export default UIWindowSettings;

View File

@@ -0,0 +1,56 @@
/**
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* Builds a consistent settings card HTML structure
* @param {Object} config - Card configuration
* @param {string} config.label - Main label text for the card
* @param {string} [config.description] - Optional description text shown below label
* @param {string} [config.control] - HTML for the control element (button, select, etc.)
* @param {string} [config.variant] - Card variant: 'danger', 'success', 'warning', or undefined for default
* @param {string} [config.className] - Additional CSS classes to add
* @returns {string} HTML string for the settings card
*/
export default function build_settings_card(config) {
const {
label,
description,
control = '',
variant,
className = '',
} = config;
const variantClass = variant ? `settings-card-${variant}` : '';
const classes = `settings-card ${variantClass} ${className}`.trim();
let labelContent = `<strong class="settings-card-label">${label}</strong>`;
if ( description ) {
labelContent = `<div class="settings-card-label">
<strong>${label}</strong>
<span class="settings-card-description">${description}</span>
</div>`;
}
return `<div class="${classes}">
<div class="settings-card-row">
${labelContent}
${control ? `<div class="settings-card-control">${control}</div>` : ''}
</div>
</div>`;
}

View File

@@ -87,6 +87,8 @@ async function UIWindow(options) {
options.is_maximized = options.is_maximized ?? false;
options.is_openFileDialog = options.is_openFileDialog ?? false;
options.is_resizable = options.is_resizable ?? true;
options.minWidth = options.minWidth ?? 200;
options.minHeight = options.minHeight ?? 200;
// if this is a fullpage window, it won't be resizable
if(options.is_fullpage){
@@ -2125,8 +2127,8 @@ async function UIWindow(options) {
$(el_window).resizable({
handles: "n, ne, nw, e, s, se, sw, w",
minWidth: 200,
minHeight: 200,
minWidth: options.minWidth,
minHeight: options.minHeight,
start: function(){
window.a_window_is_resizing = true;
$(el_window_app_iframe).css('pointer-events', 'none');

View File

@@ -16,224 +16,378 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
Plan:
Components: OneAtATimeView < ... >
Screen 1: QR code and entry box for testing
Components: Flexer < QRCodeView, CodeEntryView, ActionsView >
Logic:
- when CodeEntryView has a value, check it against the QR code value...
... then go to the next screen
- CodeEntryView will have callbacks: `verify`, `on_verified`
- cancel action
import UIWindow from './UIWindow.js';
import Placeholder from '../util/Placeholder.js';
import CodeEntryView from './Components/CodeEntryView.js';
import QRCodeView from './Components/QRCode.js';
import RecoveryCodesView from './Components/RecoveryCodesView.js';
Screen 2: Recovery codes
Components: Flexer < RecoveryCodesView, ConfirmationsView, ActionsView >
Logic:
- done action
- cancel action
- when done action is clicked, call /auth/configure-2fa/enable
const UIWindow2FASetup = async function UIWindow2FASetup() {
return new Promise(async (resolve) => {
let current_step = 1;
let qr_data = null;
*/
const place_qr = Placeholder();
const place_code_entry = Placeholder();
const place_recovery_codes = Placeholder();
import TeePromise from "../util/TeePromise.js";
import ValueHolder from "../util/ValueHolder.js";
import Button from "./Components/Button.js";
import CodeEntryView from "./Components/CodeEntryView.js";
import ConfirmationsView from "./Components/ConfirmationsView.js";
import Flexer from "./Components/Flexer.js";
import QRCodeView from "./Components/QRCode.js";
import RecoveryCodesView from "./Components/RecoveryCodesView.js";
import StepHeading from "./Components/StepHeading.js";
import StepView from "./Components/StepView.js";
import JustHTML from "./Components/JustHTML.js";
import UIComponentWindow from "./UIComponentWindow.js";
const UIWindow2FASetup = async function UIWindow2FASetup () {
// FIRST REQUEST :: Generate the QR code and recovery codes
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/setup`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const data = await resp.json();
// SECOND REQUEST :: Verify the code [first wizard screen]
const check_code_ = async function check_code_ (value) {
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/test`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: value,
}),
const qr_component = new QRCodeView({
value: '',
size: 200,
enlarge_option: true,
});
const data = await resp.json();
return data.ok;
};
// FINAL REQUEST :: Enable 2FA [second wizard screen]
const enable_2fa_ = async function check_code_ (value) {
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/enable`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const data = await resp.json();
return data.ok;
};
let stepper;
let code_entry;
let win;
let done_enabled = new ValueHolder(false);
const promise = new TeePromise();
const component =
new StepView({
_ref: me => stepper = me,
children: [
new Flexer({
children: [
new StepHeading({
symbol: '1',
text: i18n('setup2fa_1_step_heading'),
}),
new JustHTML({
html: `<div style="color: #3b4863">${i18n('setup2fa_1_instructions', [], false)}</div>`
}),
new StepHeading({
symbol: '2',
text: i18n('setup2fa_2_step_heading')
}),
new QRCodeView({
value: data.url,
}),
new StepHeading({
symbol: '3',
text: i18n('setup2fa_3_step_heading')
}),
new CodeEntryView({
_ref: me => code_entry = me,
async [`property.value`] (value, { component }) {
if ( ! await check_code_(value) ) {
component.set('error', 'Invalid code');
component.set('is_checking_code', false);
return;
}
component.set('is_checking_code', false);
stepper.next();
}
}),
],
['event.focus'] () {
code_entry.focus();
}
}),
new Flexer({
children: [
new StepHeading({
symbol: '4',
text: i18n('setup2fa_4_step_heading')
}),
new JustHTML({
html: `<div style="color: #3b4863">${i18n('setup2fa_4_instructions', [], false)}</div>`
}),
new RecoveryCodesView({
values: data.codes,
}),
new StepHeading({
symbol: '5',
text: i18n('setup2fa_5_step_heading')
}),
new ConfirmationsView({
confirmations: [
i18n('setup2fa_5_confirmation_1'),
i18n('setup2fa_5_confirmation_2'),
],
confirmed: done_enabled,
}),
new Button({
enabled: done_enabled,
label: i18n('setup2fa_5_button'),
on_click: async () => {
await enable_2fa_();
stepper.next();
},
}),
]
}),
]
})
;
stepper.values_['done'].sub(value => {
if ( ! value ) return;
$(win).close();
// Write "2FA enabled" in green in the console
console.log('%c2FA enabled', 'color: green');
promise.resolve(true);
})
win = await UIComponentWindow({
component,
on_before_exit: async () => {
if ( ! stepper.get('done') ) {
promise.resolve(false);
const code_entry_component = new CodeEntryView({
async [`property.value`](value, { component }) {
if (!await check_code(value)) {
component.set('error', i18n('code_invalid'));
component.set('is_checking_code', false);
return;
}
component.set('is_checking_code', false);
go_to_step(4);
}
return true
},
});
title: '2FA Setup',
app: 'instant-login',
single_instance: true,
icon: null,
uid: null,
is_dir: false,
// has_head: false,
selectable_body: true,
// selectable_body: false,
allow_context_menu: false,
is_resizable: false,
is_droppable: false,
init_center: true,
allow_native_ctxmenu: false,
allow_user_select: true,
// backdrop: true,
width: 550,
height: 'auto',
dominant: true,
show_in_taskbar: false,
draggable_body: false,
center: true,
onAppend: function(this_window){
},
window_class: 'window-qr',
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
padding: '20px',
},
const recovery_codes_component = new RecoveryCodesView({
values: [],
});
const h = `
<div class="setup-2fa-container">
<!-- Step Indicator -->
<div class="step-indicator">
<div class="step-dot active" data-step="1">
<span class="step-number">1</span>
</div>
<div class="step-line"></div>
<div class="step-dot" data-step="2">
<span class="step-number">2</span>
</div>
<div class="step-line"></div>
<div class="step-dot" data-step="3">
<span class="step-number">3</span>
</div>
<div class="step-line"></div>
<div class="step-dot" data-step="4">
<span class="step-number">4</span>
</div>
</div>
<!-- Step 1: Introduction -->
<div class="step-content" data-step="1">
<h2 class="step-title">${i18n('setup2fa_intro_title')}</h2>
<p class="step-description">${i18n('setup2fa_intro_description')}</p>
<div class="info-box">
<div class="info-box-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM13 17H11V11H13V17ZM13 9H11V7H13V9Z" fill="#088ef0"/>
</svg>
</div>
<div>
<h3 class="info-box-title">${i18n('setup2fa_intro_what_youll_need')}</h3>
<p class="info-box-text">${i18n('setup2fa_intro_requirements')}</p>
</div>
</div>
<div class="steps-overview">
<h3 class="steps-overview-title">${i18n('setup2fa_intro_steps_title')}</h3>
<ol class="steps-list">
<li>${i18n('setup2fa_intro_step_1')}</li>
<li>${i18n('setup2fa_intro_step_2')}</li>
<li>${i18n('setup2fa_intro_step_3')}</li>
</ol>
</div>
</div>
<!-- Step 2: Scan QR Code -->
<div class="step-content" data-step="2" style="display: none;">
<h2 class="step-title">${i18n('setup2fa_scan_title')}</h2>
<p class="step-description">${i18n('setup2fa_scan_description')}</p>
<div class="qr-container">
${place_qr.html}
</div>
<div class="manual-setup">
<p class="manual-setup-label">${i18n('setup2fa_manual_setup')}</p>
<div class="manual-setup-key-wrapper">
<code class="manual-setup-key" id="manual-key"></code>
<button class="button button-small copy-key-btn">${i18n('copy')}</button>
</div>
</div>
</div>
<!-- Step 3: Verify Setup -->
<div class="step-content" data-step="3" style="display: none;">
<h2 class="step-title">${i18n('setup2fa_verify_title')}</h2>
<p class="step-description">${i18n('setup2fa_verify_description')}</p>
<div class="form-field">
<div class="code-entry-container">
${place_code_entry.html}
</div>
</div>
</div>
<!-- Step 4: Save Recovery Codes -->
<div class="step-content" data-step="4" style="display: none;">
<h2 class="step-title">${i18n('setup2fa_recovery_title')}</h2>
<div class="warning-box">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 0C4.48 0 0 4.48 0 10C0 15.52 4.48 20 10 20C15.52 20 20 15.52 20 10C20 4.48 15.52 0 10 0ZM11 15H9V13H11V15ZM11 11H9V5H11V11Z" fill="#f59e0b"/>
</svg>
<p>${i18n('setup2fa_recovery_warning')}</p>
</div>
<div class="recovery-codes-container">
${place_recovery_codes.html}
</div>
<div class="confirmation-checkbox">
<input type="checkbox" id="codes-saved-checkbox" />
<label for="codes-saved-checkbox">${i18n('setup2fa_codes_saved_confirmation')}</label>
</div>
</div>
<!-- Navigation Buttons -->
<div class="step-navigation">
<button class="button button-default btn-back" style="display: none;">
<span>← ${i18n('back')}</span>
</button>
<button class="button button-primary btn-continue">
<span>${i18n('continue')}</span>
</button>
<button class="button button-primary btn-verify" style="display: none;">
<span>${i18n('verify_code')}</span>
</button>
<button class="button button-primary btn-finish" style="display: none;" disabled>
<span>${i18n('finish')}</span>
</button>
</div>
</div>
`;
const el_window = await UIWindow({
title: i18n('setup_2fa'),
app: '2fa-setup',
single_instance: true,
icon: null,
uid: null,
is_dir: false,
body_content: h,
has_head: true,
selectable_body: false,
draggable_body: false,
allow_context_menu: false,
is_resizable: false,
is_droppable: false,
init_center: true,
allow_native_ctxmenu: false,
allow_user_select: true,
width: 550,
height: 'auto',
dominant: true,
show_in_taskbar: false,
window_class: 'window-2fa-setup',
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
},
onAppend: function(this_window) {
initialize_2fa_setup(this_window);
},
});
async function initialize_2fa_setup(window_el) {
qr_component.attach(place_qr);
code_entry_component.attach(place_code_entry);
recovery_codes_component.attach(place_recovery_codes);
// Sync verify button state with code entry submit button
const syncVerifyButton = () => {
const isDisabled = $(window_el).find('.code-confirm-btn').prop('disabled');
$(window_el).find('.btn-verify').prop('disabled', isDisabled);
};
// Watch for changes to the submit button state
const observer = new MutationObserver(syncVerifyButton);
const submitBtn = $(window_el).find('.code-confirm-btn').get(0);
if (submitBtn) {
observer.observe(submitBtn, { attributes: true, attributeFilter: ['disabled'] });
}
// Also sync on input changes
$(window_el).on('input', '.digit-input', syncVerifyButton);
$(window_el).find('.btn-continue').on('click', async function() {
if (current_step === 1) {
if (!qr_data) {
try {
$(this).addClass('loading').prop('disabled', true);
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/setup`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
qr_data = await resp.json();
qr_component.set('value', qr_data.url);
recovery_codes_component.set('values', qr_data.codes);
const secret = new URL(qr_data.url).searchParams.get('secret');
$(window_el).find('#manual-key').text(secret);
$(this).removeClass('loading').prop('disabled', false);
} catch (error) {
console.error('Error setting up 2FA:', error);
$(this).removeClass('loading').prop('disabled', false);
return;
}
}
go_to_step(2);
} else if (current_step === 2) {
go_to_step(3);
setTimeout(() => {
code_entry_component.focus();
}, 100);
}
});
$(window_el).find('.copy-key-btn').on('click', function() {
const secret = $(window_el).find('#manual-key').text();
navigator.clipboard.writeText(secret);
$(this).text(i18n('copied'));
setTimeout(() => {
$(this).text(i18n('copy'));
}, 2000);
});
$(window_el).find('.btn-back').on('click', function() {
if (current_step === 2) {
go_to_step(1);
} else if (current_step === 3) {
go_to_step(2);
} else if (current_step === 4) {
go_to_step(3);
}
});
$(window_el).find('#codes-saved-checkbox').on('change', function() {
$(window_el).find('.btn-finish').prop('disabled', !this.checked);
});
$(window_el).find('.btn-verify').on('click', function() {
const $verifyBtn = $(this);
// Trigger the code entry component's submit button
$(window_el).find('.code-confirm-btn').click();
// Update verify button to show loading state
$verifyBtn.addClass('loading').prop('disabled', true);
// Watch for code entry component to finish checking
const checkInterval = setInterval(() => {
if (!code_entry_component.get('is_checking_code')) {
$verifyBtn.removeClass('loading');
clearInterval(checkInterval);
}
}, 100);
});
$(window_el).find('.btn-finish').on('click', async function() {
$(this).addClass('loading').prop('disabled', true);
const success = await enable_2fa();
if (success) {
$(el_window).close();
resolve(true);
} else {
$(this).removeClass('loading').prop('disabled', false);
}
});
}
function go_to_step(step) {
const $container = $(el_window).find('.setup-2fa-container');
$container.find('.step-content').hide();
$container.find(`.step-content[data-step="${step}"]`).show();
$container.find('.step-dot').removeClass('active');
for (let i = 1; i <= step; i++) {
$container.find(`.step-dot[data-step="${i}"]`).addClass('active');
}
if (step === 1) {
$container.find('.btn-back').hide();
$container.find('.btn-continue').show().find('span').text(i18n('continue'));
$container.find('.btn-verify').hide();
$container.find('.btn-finish').hide();
} else if (step === 2) {
$container.find('.btn-back').show();
$container.find('.btn-continue').show().find('span').text(i18n('continue'));
$container.find('.btn-verify').hide();
$container.find('.btn-finish').hide();
} else if (step === 3) {
$container.find('.btn-back').show();
$container.find('.btn-continue').hide();
$container.find('.btn-verify').show().prop('disabled', true);
$container.find('.btn-finish').hide();
} else if (step === 4) {
$container.find('.btn-back').show();
$container.find('.btn-continue').hide();
$container.find('.btn-verify').hide();
$container.find('.btn-finish').show();
}
current_step = step;
}
async function check_code(code) {
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/test`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ code }),
});
const data = await resp.json();
return data.ok;
}
async function enable_2fa() {
const resp = await fetch(`${window.api_origin}/auth/configure-2fa/enable`, {
method: 'POST',
headers: {
Authorization: `Bearer ${puter.authToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
const data = await resp.json();
return data.ok;
}
$(el_window).on('before_close', function() {
if (current_step < 4) {
resolve(false);
}
});
});
return { promise };
}
};
export default UIWindow2FASetup;

View File

@@ -24,31 +24,25 @@ async function UIWindowChangePassword(options){
options = options ?? {};
const internal_id = window.uuidv4();
let h = '';
h += `<div class="change-password" style="padding: 20px; border-bottom: 1px solid #ced7e1;">`;
// error msg
h += `<div class="form-error-msg"></div>`;
// success msg
h += `<div class="form-success-msg"></div>`;
// current password
h += `<div style="overflow: hidden; margin-bottom: 20px;">`;
h += `<label for="current-password-${internal_id}">${i18n('current_password')}</label>`;
h += `<input id="current-password-${internal_id}" class="current-password" type="password" name="current-password" autocomplete="current-password" />`;
h += `</div>`;
// new password
h += `<div style="overflow: hidden; margin-top: 20px; margin-bottom: 20px;">`;
h += `<label for="new-password-${internal_id}">${i18n('new_password')}</label>`;
h += `<input id="new-password-${internal_id}" type="password" class="new-password" name="new-password" autocomplete="off" />`;
h += `</div>`;
// confirm new password
h += `<div style="overflow: hidden; margin-top: 20px; margin-bottom: 20px;">`;
h += `<label for="confirm-new-password-${internal_id}">${i18n('confirm_new_password')}</label>`;
h += `<input id="confirm-new-password-${internal_id}" type="password" name="confirm-new-password" class="confirm-new-password" autocomplete="off" />`;
h += `</div>`;
// Change Password
h += `<button class="change-password-btn button button-primary button-block button-normal">${i18n('change_password')}</button>`;
h += `</div>`;
const h = `
<div class="change-password settings-form-container">
<div class="form-error-msg" role="alert" aria-live="polite"></div>
<div class="form-success-msg" role="alert" aria-live="polite"></div>
<div class="form-field">
<label class="form-label" for="current-password-${internal_id}">${i18n('current_password')}</label>
<input id="current-password-${internal_id}" class="current-password form-input" type="password" name="current-password" autocomplete="current-password" aria-required="true" />
</div>
<div class="form-field">
<label class="form-label" for="new-password-${internal_id}">${i18n('new_password')}</label>
<input id="new-password-${internal_id}" type="password" class="new-password form-input" name="new-password" autocomplete="new-password" aria-required="true" />
</div>
<div class="form-field">
<label class="form-label" for="confirm-new-password-${internal_id}">${i18n('confirm_new_password')}</label>
<input id="confirm-new-password-${internal_id}" type="password" name="confirm-new-password" class="confirm-new-password form-input" autocomplete="new-password" aria-required="true" />
</div>
<button class="change-password-btn button button-primary button-block button-normal" aria-label="${i18n('change_password')}">${i18n('change_password')}</button>
</div>
`;
const el_window = await UIWindow({
title: i18n('window_title_change_password'),
@@ -67,14 +61,14 @@ async function UIWindowChangePassword(options){
init_center: true,
allow_native_ctxmenu: false,
allow_user_select: false,
width: 350,
width: 380,
height: 'auto',
dominant: true,
show_in_taskbar: false,
onAppend: function(this_window){
$(this_window).find(`.current-password`).get(0).focus({preventScroll:true});
},
window_class: 'window-publishWebsite',
window_class: 'window-change-password',
body_css: {
width: 'initial',
height: '100%',
@@ -89,31 +83,26 @@ async function UIWindowChangePassword(options){
const new_password = $(el_window).find('.new-password').val();
const confirm_new_password = $(el_window).find('.confirm-new-password').val();
// hide success message
$(el_window).find('.form-success-msg').hide();
// hide messages
$(el_window).find('.form-error-msg, .form-success-msg').removeClass('visible');
// check if all fields are filled
if(!current_password || !new_password || !confirm_new_password){
$(el_window).find('.form-error-msg').html('All fields are required.');
$(el_window).find('.form-error-msg').fadeIn();
$(el_window).find('.form-error-msg').html(i18n('all_fields_required')).addClass('visible');
return;
}
// check if new password and confirm new password are the same
else if(new_password !== confirm_new_password){
$(el_window).find('.form-error-msg').html(i18n('passwords_do_not_match'));
$(el_window).find('.form-error-msg').fadeIn();
$(el_window).find('.form-error-msg').html(i18n('passwords_do_not_match')).addClass('visible');
return;
}
// check password strength
const pass_strength = check_password_strength(new_password);
if(!pass_strength.overallPass){
$(el_window).find('.form-error-msg').html(i18n('password_strength_error'));
$(el_window).find('.form-error-msg').fadeIn();
$(el_window).find('.form-error-msg').html(i18n('password_strength_error')).addClass('visible');
return;
}
$(el_window).find('.form-error-msg').hide();
$.ajax({
url: window.api_origin + "/user-protected/change-password",
type: 'POST',
@@ -127,13 +116,11 @@ async function UIWindowChangePassword(options){
new_pass: new_password,
}),
success: function (data){
$(el_window).find('.form-success-msg').html(i18n('password_changed'));
$(el_window).find('.form-success-msg').fadeIn();
$(el_window).find('.form-success-msg').html(i18n('password_changed')).addClass('visible');
$(el_window).find('input').val('');
},
error: function (err){
$(el_window).find('.form-error-msg').html(html_encode(err.responseText));
$(el_window).find('.form-error-msg').fadeIn();
$(el_window).find('.form-error-msg').html(html_encode(err.responseText)).addClass('visible');
}
});
})

View File

@@ -24,21 +24,17 @@ async function UIWindowChangeUsername(options){
options = options ?? {};
const internal_id = window.uuidv4();
let h = '';
h += `<div class="change-username" style="padding: 20px; border-bottom: 1px solid #ced7e1;">`;
// error msg
h += `<div class="form-error-msg"></div>`;
// success msg
h += `<div class="form-success-msg"></div>`;
// new username
h += `<div style="overflow: hidden; margin-top: 10px; margin-bottom: 30px;">`;
h += `<label for="confirm-new-username-${internal_id}">${i18n('new_username')}</label>`;
h += `<input id="confirm-new-username-${internal_id}" type="text" name="new-username" class="new-username" autocomplete="off" />`;
h += `</div>`;
// Change Username
h += `<button class="change-username-btn button button-primary button-block button-normal">${i18n('change_username')}</button>`;
h += `</div>`;
const h = `
<div class="change-username settings-form-container">
<div class="form-error-msg" role="alert" aria-live="polite"></div>
<div class="form-success-msg" role="alert" aria-live="polite"></div>
<div class="form-field">
<label class="form-label" for="confirm-new-username-${internal_id}">${i18n('new_username')}</label>
<input id="confirm-new-username-${internal_id}" type="text" name="new-username" class="new-username form-input" autocomplete="off" aria-required="true" />
</div>
<button class="change-username-btn button button-primary button-block button-normal" aria-label="${i18n('change_username')}">${i18n('change_username')}</button>
</div>
`;
const el_window = await UIWindow({
title: i18n('change_username'),
@@ -57,14 +53,14 @@ async function UIWindowChangeUsername(options){
init_center: true,
allow_native_ctxmenu: false,
allow_user_select: false,
width: 350,
width: 380,
height: 'auto',
dominant: true,
show_in_taskbar: false,
onAppend: function(this_window){
$(this_window).find(`.new-username`).get(0)?.focus({preventScroll:true});
},
window_class: 'window-publishWebsite',
window_class: 'window-change-username',
body_css: {
width: 'initial',
height: '100%',
@@ -75,24 +71,18 @@ async function UIWindowChangeUsername(options){
})
$(el_window).find('.change-username-btn').on('click', function(e){
// hide previous error/success msg
$(el_window).find('.form-success-msg, .form-success-msg').hide();
$(el_window).find('.form-error-msg, .form-success-msg').removeClass('visible');
const new_username = $(el_window).find('.new-username').val();
if(!new_username){
$(el_window).find('.form-error-msg').html(i18n('all_fields_required'));
$(el_window).find('.form-error-msg').fadeIn();
$(el_window).find('.form-error-msg').html(i18n('all_fields_required')).addClass('visible');
return;
}
$(el_window).find('.form-error-msg').hide();
// disable button
$(el_window).find('.change-username-btn').addClass('disabled');
// disable input
$(el_window).find('.change-username-btn').addClass('loading');
$(el_window).find('.new-username').attr('disabled', true);
$.ajax({
url: window.api_origin + "/change_username",
type: 'POST',
@@ -101,31 +91,23 @@ async function UIWindowChangeUsername(options){
"Authorization": "Bearer "+window.auth_token
},
contentType: "application/json",
data: JSON.stringify({
new_username: new_username,
}),
data: JSON.stringify({
new_username: new_username,
}),
success: function (data){
$(el_window).find('.form-success-msg').html(i18n('username_changed'));
$(el_window).find('.form-success-msg').fadeIn();
$(el_window).find('.change-username-btn').removeClass('loading');
$(el_window).find('.form-success-msg').html(i18n('username_changed')).addClass('visible');
$(el_window).find('input').val('');
// update auth data
update_username_in_gui(new_username);
// update username
window.user.username = new_username;
// enable button
$(el_window).find('.change-username-btn').removeClass('disabled');
// enable input
$(el_window).find('.new-username').attr('disabled', false);
},
error: function (err){
$(el_window).find('.form-error-msg').html(html_encode(err.responseJSON?.message));
$(el_window).find('.form-error-msg').fadeIn();
// enable button
$(el_window).find('.change-username-btn').removeClass('disabled');
// enable input
$(el_window).find('.change-username-btn').removeClass('loading');
$(el_window).find('.form-error-msg').html(html_encode(err.responseJSON?.message)).addClass('visible');
$(el_window).find('.new-username').attr('disabled', false);
}
});
});
})
}

View File

@@ -23,60 +23,63 @@ async function UIWindowDesktopBGSettings(options){
options = options ?? {};
return new Promise(async (resolve) => {
let h = '';
const original_background_css = $('body').attr('style');
let bg_url = window.desktop_bg_url,
bg_color = window.desktop_bg_color,
const original_bg_url = window.desktop_bg_url;
const original_bg_color = window.desktop_bg_color;
const original_bg_fit = window.desktop_bg_fit;
let bg_url = window.desktop_bg_url,
bg_color = window.desktop_bg_color,
bg_fit = window.desktop_bg_fit;
h += `<div style="padding: 10px; border-bottom: 1px solid #ced7e1;">`;
// type
h += `<label>${i18n('background')}:</label>`;
h += `<select class="desktop-bg-type" style="width: 150px; margin-bottom: 20px;">`
h += `<option value="default">${i18n('default')}</option>`;
h += `<option value="picture">${i18n('picture')}</option>`;
h += `<option value="color">${i18n('color')}</option>`;
h += `</select>`;
// Picture
h += `<div class="desktop-bg-settings-wrapper desktop-bg-settings-picture">`;
h += `<label>${i18n('image')}:</label>`;
h += `<button class="button button-default button-small browse">${i18n('browse')}</button>`;
h += `<label style="margin-top: 20px;">${i18n('fit')}:</label>`;
h += `<select class="desktop-bg-fit" style="width: 150px;">`
h += `<option value="cover">${i18n('cover')}</option>`;
h += `<option value="center">${i18n('center')}</option>`;
h += `<option value="contain">${i18n('contain')}</option>`;
h += `<option value="repeat">${i18n('repeat')}</option>`;
h += `</select>`;
h += `</div>`
// Color
h += `<div class="desktop-bg-settings-wrapper desktop-bg-settings-color">`;
h += `<label>${i18n('color')}:</label>`;
h += `<div class="desktop-bg-color-blocks">`;
h += `<div class="desktop-bg-color-block" data-color="#4F7BB5" style="background-color: #4F7BB5"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#545554" style="background-color: #545554"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#F5D3CE" style="background-color: #F5D3CE"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#52A758" style="background-color: #52A758"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#ad3983" style="background-color: #ad3983"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#ffffff" style="background-color: #ffffff"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#000000" style="background-color: #000000"></div>`;
h += `<div class="desktop-bg-color-block" data-color="#454545" style="background-color: #454545"></div>`;
h += `<div class="desktop-bg-color-block desktop-bg-color-block-palette" data-color="" style="background-image: url(${window.icons['palette.svg']});
background-repeat: no-repeat;
background-size: contain;
background-position: center;"><input type="color" style="width:25px; height: 25px; opacity:0;"></div>`;
h += `</div>`;
h += `</div>`;
h += `<div style="padding-top: 5px; overflow:hidden; margin-top: 25px; border-top: 1px solid #CCC;">`
h += `<button class="button button-primary apply" style="float:right;">${i18n('apply')}</button>`;
h += `<button class="button button-default cancel" style="float:right; margin-right: 10px;">${i18n('cancel')}</button>`;
h += `</div>`;
h += `</div>`;
const h = `
<div class="settings-form-container">
<div class="form-field">
<label class="form-label">${i18n('background')}</label>
<select class="desktop-bg-type desktop-bg-select">
<option value="default">${i18n('default')}</option>
<option value="picture">${i18n('picture')}</option>
<option value="color">${i18n('color')}</option>
</select>
</div>
<div class="desktop-bg-settings-wrapper desktop-bg-settings-picture">
<div class="form-field">
<label class="form-label">${i18n('image')}</label>
<button class="button button-default button-small browse">${i18n('browse')}</button>
</div>
<div class="form-field">
<label class="form-label">${i18n('fit')}</label>
<select class="desktop-bg-fit desktop-bg-select">
<option value="cover">${i18n('cover')}</option>
<option value="center">${i18n('center')}</option>
<option value="contain">${i18n('contain')}</option>
<option value="repeat">${i18n('repeat')}</option>
</select>
</div>
</div>
<div class="desktop-bg-settings-wrapper desktop-bg-settings-color">
<div class="form-field">
<label class="form-label">${i18n('color')}</label>
<div class="desktop-bg-color-blocks">
<div class="desktop-bg-color-block" data-color="#4F7BB5" style="background-color: #4F7BB5" tabindex="0" role="button" aria-label="${i18n('color')}: Blue"></div>
<div class="desktop-bg-color-block" data-color="#545554" style="background-color: #545554" tabindex="0" role="button" aria-label="${i18n('color')}: Gray"></div>
<div class="desktop-bg-color-block" data-color="#F5D3CE" style="background-color: #F5D3CE" tabindex="0" role="button" aria-label="${i18n('color')}: Pink"></div>
<div class="desktop-bg-color-block" data-color="#52A758" style="background-color: #52A758" tabindex="0" role="button" aria-label="${i18n('color')}: Green"></div>
<div class="desktop-bg-color-block" data-color="#ad3983" style="background-color: #ad3983" tabindex="0" role="button" aria-label="${i18n('color')}: Purple"></div>
<div class="desktop-bg-color-block" data-color="#ffffff" style="background-color: #ffffff" tabindex="0" role="button" aria-label="${i18n('color')}: White"></div>
<div class="desktop-bg-color-block" data-color="#000000" style="background-color: #000000" tabindex="0" role="button" aria-label="${i18n('color')}: Black"></div>
<div class="desktop-bg-color-block" data-color="#454545" style="background-color: #454545" tabindex="0" role="button" aria-label="${i18n('color')}: Dark Gray"></div>
<div class="desktop-bg-color-block desktop-bg-color-block-palette" data-color="" tabindex="0" role="button" aria-label="${i18n('color')}: Custom" style="background-image: url(${window.icons['palette.svg']}); background-repeat: no-repeat; background-size: 20px 20px; background-position: center;">
<input type="color" class="desktop-bg-color-picker" aria-label="Custom color picker">
</div>
</div>
</div>
</div>
<div class="desktop-bg-button-container">
<button class="button button-default cancel">${i18n('cancel')}</button>
<button class="button button-primary apply">${i18n('apply')}</button>
</div>
</div>
`;
const el_window = await UIWindow({
title: i18n('change_desktop_background'),
@@ -94,10 +97,9 @@ async function UIWindowDesktopBGSettings(options){
allow_native_ctxmenu: true,
allow_user_select: true,
onAppend: function(this_window){
$(this_window).find(`.access-recipient`).focus();
},
window_class: 'window-give-access',
width: 350,
window_class: 'window-desktop-bg-settings',
width: 400,
window_css: {
height: 'initial',
},
@@ -126,10 +128,16 @@ async function UIWindowDesktopBGSettings(options){
$(el_window).find('.desktop-bg-type').val('default');
}
$(el_window).find('.desktop-bg-color-block:not(.desktop-bg-color-block-palette').on('click', async function(e){
$(el_window).find('.desktop-bg-color-block:not(.desktop-bg-color-block-palette)').on('click', async function(e){
window.set_desktop_background({color: $(this).attr('data-color')})
})
$(el_window).find('.desktop-bg-color-block-palette input').on('change', async function(e){
$(el_window).find('.desktop-bg-color-block:not(.desktop-bg-color-block-palette)').on('keydown', async function(e){
if(e.key === 'Enter' || e.key === ' '){
e.preventDefault();
window.set_desktop_background({color: $(this).attr('data-color')})
}
})
$(el_window).find('.desktop-bg-color-picker').on('change', async function(e){
window.set_desktop_background({color: $(this).val()})
})
$(el_window).on('file_opened', function(e){
@@ -208,6 +216,9 @@ async function UIWindowDesktopBGSettings(options){
$(el_window).find('.cancel').on('click', function(){
$('body').attr('style', original_background_css);
window.desktop_bg_url = original_bg_url;
window.desktop_bg_color = original_bg_color;
window.desktop_bg_fit = original_bg_fit;
$(el_window).close();
resolve(true);
})

View File

@@ -7,18 +7,18 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import UIAlert from './UIAlert.js';
import UIWindow from './UIWindow.js'
import UIWindow from './UIWindow.js';
async function UIWindowQR(options){
return new Promise(async (resolve) => {
@@ -31,20 +31,19 @@ async function UIWindowQR(options){
return resolve();
}
let h = '';
h += `<div style="padding: 20px; margin-top: 0;">`;
// success
h += `<div class="feedback-sent-success">`;
h += `<img src="${html_encode(window.icons['c-check.svg'])}" style="width:50px; height:50px; display: block; margin:10px auto;">`;
h += `<p style="text-align:center; margin-bottom:10px; color: #005300; padding: 10px;">${i18n('feedback_sent_confirmation')}</p>`;
h+= `</div>`;
// form
h += `<div class="feedback-form">`;
h += `<p style="margin-top:0; font-size: 15px; -webkit-font-smoothing: antialiased;">${i18n('feedback_c2a')}</p>`;
h += `<textarea class="feedback-message" style="width:100%; height: 200px; padding: 10px; box-sizing: border-box;"></textarea>`;
h += `<button class="button button-primary send-feedback-btn" style="float: right; margin-bottom: 15px; margin-top: 10px;">${i18n('send')}</button>`;
h += `</div>`;
h += `</div>`;
const h = `
<div class="feedback-container">
<div class="feedback-sent-success">
<img class="feedback-success-icon" src="${html_encode(window.icons['c-check.svg'])}">
<p class="feedback-success-message">${i18n('feedback_sent_confirmation')}</p>
</div>
<div class="feedback-form">
<p class="feedback-c2a">${i18n('feedback_c2a')}</p>
<textarea class="feedback-message"></textarea>
<button class="button button-primary send-feedback-btn">${i18n('send')}</button>
</div>
</div>
`;
const el_window = await UIWindow({
title: i18n('contact_us'),
@@ -68,7 +67,7 @@ async function UIWindowQR(options){
dominant: true,
show_in_taskbar: false,
onAppend: function(this_window){
$(this_window).find('.feedback-message').get(0).focus({preventScroll:true});
$(this_window).find('.feedback-message').get(0).focus({ preventScroll: true });
},
window_class: 'window-feedback',
body_css: {
@@ -76,31 +75,33 @@ async function UIWindowQR(options){
height: '100%',
'background-color': 'rgb(245 247 249)',
'backdrop-filter': 'blur(3px)',
}
})
},
});
$(el_window).find('.send-feedback-btn').on('click', function(e){
const message = $(el_window).find('.feedback-message').val();
if(message)
if ( message )
{
$(this).prop('disabled', true);
}
$.ajax({
url: window.api_origin + "/contactUs",
url: `${window.api_origin}/contactUs`,
type: 'POST',
async: true,
contentType: "application/json",
contentType: 'application/json',
headers: {
"Authorization": "Bearer "+window.auth_token
},
data: JSON.stringify({
'Authorization': `Bearer ${window.auth_token}`,
},
data: JSON.stringify({
message: message,
}),
success: async function (data){
success: async function(data){
$(el_window).find('.feedback-form').hide();
$(el_window).find('.feedback-sent-success').show(100);
}
})
})
})
},
});
});
});
}
export default UIWindowQR
export default UIWindowQR;

View File

@@ -37,56 +37,40 @@ async function UIWindowLogin(options){
return new Promise(async (resolve) => {
const internal_id = window.uuidv4();
let h = ``;
h += `<div style="max-width:100%; width:100%; height:100%; min-height:0; box-sizing:border-box; display:flex; flex-direction:column; justify-content:flex-start; align-items:stretch; padding:0; overflow:auto; color:var(--color-text);">`;
// logo
h += `<div class="logo-wrapper" style="display:flex; justify-content:center; padding:20px 20px 0 20px; margin-bottom: 0;">`;
h += `<img src="${window.icons['logo-white.svg']}" style="width: 40px; height: 40px; margin: 0 auto; display: block; padding: 15px; background-color: blue; border-radius: 5px;">`;
h += `</div>`;
// title
h += `<div style="padding:10px 20px; text-align:center; margin-bottom:0;">`;
h += `<h1 style="font-size:18px; margin-bottom:0;">${i18n('log_in')}</h1>`;
h += `</div>`;
// form
h += `<div style="padding:20px; overflow-y:auto; overflow-x:hidden;">`;
h += `<form class="login-form" style="width:100%;">`;
// server messages
h += `<div class="login-error-msg" style="color:#e74c3c; display:none; margin-bottom:10px; line-height:15px; font-size:13px;"></div>`;
// email or username
h += `<div style="position: relative; margin-bottom: 20px;">`;
h += `<label style="display:block; margin-bottom:5px;">${i18n('email_or_username')}</label>`;
if(options.email_or_username){
h += `<input type="text" class="email_or_username" value="${options.email_or_username}" autocomplete="username"/>`;
}else{
h += `<input type="text" class="email_or_username" autocomplete="username"/>`;
}
h += `</div>`;
// password
h += `<div style="position: relative; margin-bottom: 20px;">`;
h += `<label style="display:block; margin-bottom:5px;">${i18n('password')}</label>`;
h += `<input id="password-${internal_id}" class="password" type="${options.show_password ? "text" : "password"}" name="password" autocomplete="current-password"/>`;
// show/hide icon
h += `<span style="position: absolute; right: 5%; top: 50%; cursor: pointer;" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"]}" width="20" height="20">
</span>`;
h += `</div>`;
// login
h += `<button type="submit" class="login-btn button button-primary button-block button-normal">${i18n('log_in')}</button>`;
// password recovery
h += `<p style="text-align:center; margin-bottom: 0;"><span class="forgot-password-link">${i18n('forgot_pass_c2a')}</span></p>`;
h += `</form>`;
h += `</div>`;
// create account link
// If show_signup_button is undefined, the default behavior is to show it.
// If show_signup_button is set to false, the button will not be shown.
if(options.show_signup_button === undefined || options.show_signup_button){
h += `<div class="c2a-wrapper" style="padding:20px;">`;
h += `<button class="signup-c2a-clickable">${i18n('create_free_account')}</button>`;
h += `</div>`;
}
h += `</div>`;
const h = `
<div class="auth-container">
<div class="auth-logo-wrapper">
<img class="auth-logo" src="${window.icons['logo-white.svg']}">
</div>
<div class="auth-title">
<h1>${i18n('log_in')}</h1>
</div>
<div class="auth-form-wrapper">
<form class="auth-form login-form">
<div class="auth-error-msg login-error-msg"></div>
<div class="auth-form-group">
<label class="auth-label">${i18n('email_or_username')}</label>
<input type="text" class="auth-input email_or_username" ${options.email_or_username ? `value="${options.email_or_username}"` : ''} autocomplete="username"/>
</div>
<div class="auth-form-group">
<label class="auth-label">${i18n('password')}</label>
<input id="password-${internal_id}" class="auth-input password" type="${options.show_password ? 'text' : 'password'}" name="password" autocomplete="current-password"/>
<span class="auth-password-toggle" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons['eye-closed.svg'] : window.icons['eye-open.svg']}">
</span>
</div>
<button type="submit" class="login-btn button button-primary button-block button-normal">${i18n('log_in')}</button>
<p class="auth-forgot-password"><span class="forgot-password-link">${i18n('forgot_pass_c2a')}</span></p>
</form>
</div>
${(options.show_signup_button === undefined || options.show_signup_button) ? `
<div class="c2a-wrapper">
<button class="signup-c2a-clickable">${i18n('create_free_account')}</button>
</div>
` : ''}
</div>
`;
const el_window = await UIWindow({
title: null,
@@ -107,7 +91,7 @@ async function UIWindowLogin(options){
allow_native_ctxmenu: true,
allow_user_select: true,
...options.window_options,
width: 350,
width: 400,
dominant: true,
on_close: ()=>{
resolve(false)

View File

@@ -34,17 +34,13 @@ async function UIWindowLoginInProgress(options){
profile_pic = window.icons['profile.svg']
}
let h = '';
h += `<div class="login-progress">`;
h += `<div class="profile-pic" style="background-color: #cecece; background-image: url('${profile_pic}'); width: 70px; height: 70px; background-position: center; background-size: cover; border-radius: 50px; margin-bottom: 15px; margin-top: 40px;"></div>`;
h += `<h1 style="text-align: center;
font-size: 17px;
padding: 10px;
font-weight: 300; margin: -10px 10px 4px 10px;">Logging in as <strong>${options.user_info.email === null ? options.user_info.username : options.user_info.email}</strong></h1>`;
// spinner
h +=`<svg style="float:left; margin-right: 7px; margin-bottom: 30px;" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>`;
h += `</div>`;
const h = `
<div class="login-progress">
<div class="login-progress-profile-pic" style="background-image: url('${profile_pic}');"></div>
<h1 class="login-progress-title">Logging in as <strong>${options.user_info.email === null ? options.user_info.username : options.user_info.email}</strong></h1>
<svg class="login-progress-spinner" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><title>circle anim</title><g fill="#212121" class="nc-icon-wrapper"><g class="nc-loop-circle-24-icon-f"><path d="M12 24a12 12 0 1 1 12-12 12.013 12.013 0 0 1-12 12zm0-22a10 10 0 1 0 10 10A10.011 10.011 0 0 0 12 2z" fill="#212121" opacity=".4"></path><path d="M24 12h-2A10.011 10.011 0 0 0 12 2V0a12.013 12.013 0 0 1 12 12z" data-color="color-2"></path></g><style>.nc-loop-circle-24-icon-f{--animation-duration:0.5s;transform-origin:12px 12px;animation:nc-loop-circle-anim var(--animation-duration) infinite linear}@keyframes nc-loop-circle-anim{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}</style></g></svg>
</div>
`;
const el_window = await UIWindow({
title: i18n('window_title_authenticating'),
@@ -73,7 +69,7 @@ async function UIWindowLoginInProgress(options){
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'background-color': '#F5F5F7',
'backdrop-filter': 'blur(3px)',
}
})

View File

@@ -22,9 +22,7 @@ import UIContextMenu from './UIContextMenu.js'
import UIAlert from './UIAlert.js'
async function UIWindowMyWebsites(options){
let h = '';
h += `<div>`;
h += `</div>`;
const h = `<div></div>`;
const el_window = await UIWindow({
title: `My Websites`,
@@ -46,9 +44,9 @@ async function UIWindowMyWebsites(options){
width: 400,
dominant: false,
body_css: {
padding: '10px',
padding: '20px',
width: 'initial',
'background-color': 'rgba(231, 238, 245)',
'background-color': '#F5F5F7',
'backdrop-filter': 'blur(3px)',
'padding-bottom': 0,
'height': '351px',
@@ -59,12 +57,7 @@ async function UIWindowMyWebsites(options){
// /sites
let init_ts = Date.now();
let loading = setTimeout(function(){
$(el_window).find('.window-body').html(`<p style="text-align: center;
margin-top: 40px;
margin-bottom: 50px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #596c7c;">${i18n('loading')}...</p>`);
$(el_window).find('.window-body').html(`<p class="mywebsites-loading">${i18n('loading')}...</p>`);
}, 1000);
puter.hosting.list().then(function (sites){
@@ -73,35 +66,33 @@ async function UIWindowMyWebsites(options){
clearTimeout(loading);
// user has sites
if(sites.length > 0){
let h ='';
for(let i=0; i< sites.length; i++){
h += `<div class="mywebsites-card" data-uuid="${sites[i].uid}">`;
h += `<a class="mywebsites-address-link" href="https://${sites[i].subdomain}.puter.site" target="_blank">${sites[i].subdomain}.puter.site</a>`;
h += `<img class="mywebsites-site-setting" data-site-uuid="${sites[i].uid}" src="${html_encode(window.icons['cog.svg'])}">`;
// there is a directory associated with this site
if(sites[i].root_dir){
h += `<p class="mywebsites-dir-path" data-path="${html_encode(sites[i].root_dir.path)}" data-name="${html_encode(sites[i].root_dir.name)}" data-uuid="${sites[i].root_dir.id}">`;
h+= `<img src="${html_encode(window.icons['folder.svg'])}">`;
h+= `${html_encode(sites[i].root_dir.path)}`;
h += `</p>`;
h += `<p style="margin-bottom:0; margin-top: 20px; font-size: 13px;">`;
h += `<span class="mywebsites-dis-dir" data-dir-uuid="${html_encode(sites[i].root_dir.id)}" data-site-subdomain="${html_encode(sites[i].subdomain)}" data-site-uuid="${html_encode(sites[i].uid)}">`;
h += `<img style="width: 16px; margin-bottom: -2px; margin-right: 4px;" src="${html_encode(window.icons['plug.svg'])}">${i18n('disassociate_dir')}</span>`;
h += `</p>`;
}
h += `<p class="mywebsites-no-dir-notice" data-site-uuid="${html_encode(sites[i].uid)}" style="${sites[i].root_dir ? `display:none;` : `display:block;`}">${i18n('no_dir_associated_with_site')}</p>`;
h += `</div>`;
}
const h = `
<div class="mywebsites-container">
${sites.map(site => `
<div class="mywebsites-card" data-uuid="${site.uid}" style="position: relative;">
<a class="mywebsites-address-link" href="https://${site.subdomain}.puter.site" target="_blank">${site.subdomain}.puter.site</a>
<img class="mywebsites-site-setting" data-site-uuid="${site.uid}" src="${html_encode(window.icons['cog.svg'])}">
${site.root_dir ? `
<p class="mywebsites-dir-path" data-path="${html_encode(site.root_dir.path)}" data-name="${html_encode(site.root_dir.name)}" data-uuid="${site.root_dir.id}">
<img src="${html_encode(window.icons['folder.svg'])}">
${html_encode(site.root_dir.path)}
</p>
<div style="margin-top: 8px;">
<span class="mywebsites-dis-dir" data-dir-uuid="${html_encode(site.root_dir.id)}" data-site-subdomain="${html_encode(site.subdomain)}" data-site-uuid="${html_encode(site.uid)}">
<img src="${html_encode(window.icons['plug.svg'])}">${i18n('disassociate_dir')}
</span>
</div>
` : ''}
<p class="mywebsites-no-dir-notice" data-site-uuid="${html_encode(site.uid)}" style="${site.root_dir ? 'display:none;' : 'display:block;'}">${i18n('no_dir_associated_with_site')}</p>
</div>
`).join('')}
</div>
`;
$(el_window).find('.window-body').html(h);
}
// has no sites
else{
$(el_window).find('.window-body').html(`<p style="text-align: center;
margin-top: 40px;
margin-bottom: 50px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #596c7c;">${i18n('no_websites_published')}</p>`);
$(el_window).find('.window-body').html(`<p class="mywebsites-empty">${i18n('no_websites_published')}</p>`);
}
}, Date.now() - init_ts < 1000 ? 0 : 2000);
})

View File

@@ -28,16 +28,13 @@ async function UIWindowQR(options){
const placeholder_qr = Placeholder();
let h = '';
// close button containing the multiplication sign
h += `<div class="qr-code-window-close-btn generic-close-window-button"> &times; </div>`;
h += `<div class="otp-qr-code">`;
h += `<h1 style="text-align: center; font-size: 16px; padding: 10px; font-weight: 400; margin: -10px 10px 20px 10px; -webkit-font-smoothing: antialiased; color: #5f626d;">${
i18n(options.message_i18n_key || 'scan_qr_generic')
}</h1>`;
h += `</div>`;
h += placeholder_qr.html;
const h = `
<div class="qr-code-window-close-btn generic-close-window-button">&times;</div>
<div class="qr-code-window-content">
<h1 class="qr-window-title">${i18n(options.message_i18n_key || 'scan_qr_generic')}</h1>
</div>
${placeholder_qr.html}
`;
const el_window = await UIWindow({
title: i18n('window_title_instant_login'),
@@ -65,9 +62,9 @@ async function UIWindowQR(options){
body_css: {
width: 'initial',
height: '100%',
'background-color': 'rgb(245 247 249)',
'background-color': '#F5F5F7',
'backdrop-filter': 'blur(3px)',
padding: '50px 20px',
padding: '40px 20px',
},
})

View File

@@ -7,34 +7,43 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import UIWindow from './UIWindow.js'
import UIAlert from './UIAlert.js'
import UIWindow from './UIWindow.js';
import UIAlert from './UIAlert.js';
function UIWindowRecoverPassword(options){
return new Promise(async (resolve) => {
options = options ?? {};
let h = '';
h += `<div style="-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #3e5362;">`;
h += `<h3 style="text-align:center; font-weight: 400; font-size: 20px;">${i18n('recover_password')}</h3>`;
h += `<form class="pass-recovery-form">`;
h += `<p style="text-align:center; padding: 0 20px;"></p>`;
h += `<div class="error"></div>`;
h += `<label>${i18n('email_or_username')}</label>`;
h += `<input class="pass-recovery-username-or-email" type="text"/>`;
h += `<button type="submit" class="send-recovery-email button button-block button-primary" style="margin-top:10px;">${i18n('send_password_recovery_email')}</button>`;
h += `</form>`;
h += `</div>`;
const h = `
<div class="auth-container">
<div class="auth-logo-wrapper">
<img class="auth-logo" src="${window.icons['logo-white.svg']}">
</div>
<div class="auth-title">
<h1>${i18n('recover_password')}</h1>
</div>
<div class="auth-form-wrapper">
<form class="auth-form pass-recovery-form">
<div class="auth-error-msg error"></div>
<div class="auth-form-group">
<label class="auth-label">${i18n('email_or_username')}</label>
<input class="auth-input pass-recovery-username-or-email" type="text" autocomplete="username"/>
</div>
<button type="submit" class="send-recovery-email button button-primary button-block button-normal">${i18n('send_password_recovery_email')}</button>
</form>
</div>
</div>
`;
const el_window = await UIWindow({
title: null,
@@ -45,7 +54,7 @@ function UIWindowRecoverPassword(options){
body_content: h,
has_head: options.has_head ?? true,
selectable_body: false,
draggable_body: true,
draggable_body: false,
allow_context_menu: false,
is_draggable: options.is_draggable ?? true,
is_droppable: false,
@@ -53,55 +62,58 @@ function UIWindowRecoverPassword(options){
stay_on_top: options.stay_on_top ?? false,
allow_native_ctxmenu: true,
allow_user_select: true,
width: 350,
width: 400,
dominant: true,
...options.window_options,
onAppend: function(el_window){
$(el_window).find('.pass-recovery-username-or-email').first().focus();
},
window_class: 'window-item-properties',
window_css:{
window_class: 'window-recover-password',
window_css: {
height: 'initial',
},
body_css: {
padding: '10px',
width: 'initial',
height: 'initial',
'background-color': 'rgba(231, 238, 245)',
padding: '0',
'background-color': 'rgb(255 255 255)',
'backdrop-filter': 'blur(3px)',
}
})
},
});
$(el_window).find('.pass-recovery-form').on('submit', function(e){
e.preventDefault();
e.stopPropagation();
return false;
})
});
// Send recovery email
$(el_window).find('.send-recovery-email').on('click', function(e){
let email, username;
let input = $(el_window).find('.pass-recovery-username-or-email').val();
if(window.is_email(input))
if ( window.is_email(input) )
{
email = input;
}
else
{
username = input;
}
// todo validation before sending
$.ajax({
url: window.api_origin + "/send-pass-recovery-email",
url: `${window.api_origin}/send-pass-recovery-email`,
type: 'POST',
async: true,
contentType: "application/json",
contentType: 'application/json',
data: JSON.stringify({
email: email,
username: username,
}),
}),
statusCode: {
401: function () {
401: function() {
window.logout();
},
},
success: async function (res){
},
success: async function(res){
$(el_window).close();
await UIAlert({
message: res.message,
@@ -111,18 +123,18 @@ function UIWindowRecoverPassword(options){
window_options: {
backdrop: true,
close_on_backdrop_click: false,
}
})
},
});
},
error: function (err){
error: function(err){
$(el_window).find('.error').html(html_encode(err.responseText));
$(el_window).find('.error').fadeIn();
},
complete: function(){
}
})
})
})
},
});
});
});
}
export default UIWindowRecoverPassword
export default UIWindowRecoverPassword;

View File

@@ -7,30 +7,30 @@
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import UIWindow from './UIWindow.js'
import path from "../lib/path.js"
import UIAlert from './UIAlert.js'
import launch_app from '../helpers/launch_app.js'
import item_icon from '../helpers/item_icon.js'
import UIContextMenu from './UIContextMenu.js'
import UIWindow from './UIWindow.js';
import path from '../lib/path.js';
import UIAlert from './UIAlert.js';
import launch_app from '../helpers/launch_app.js';
import item_icon from '../helpers/item_icon.js';
import UIContextMenu from './UIContextMenu.js';
async function UIWindowSearch(options){
let h = '';
h += `<div class="search-input-wrapper">`;
h += `<input type="text" class="search-input" placeholder="Search" style="background-image:url('${window.icons['magnifier-outline.svg']}');">`;
h += `</div>`;
h += `<div class="search-results" style="overflow-y: auto; max-height: 300px;">`;
const h = `
<div class="search-input-wrapper">
<input type="text" class="search-input" placeholder="Search" style="background-image:url('${window.icons['magnifier-outline.svg']}');">
</div>
<div class="search-results">
`;
const el_window = await UIWindow({
icon: null,
@@ -69,7 +69,7 @@ async function UIWindowSearch(options){
'overflow': 'hidden',
'min-height': '65px',
'padding-bottom': '10px',
}
},
});
$(el_window).find('.search-input').focus();
@@ -77,7 +77,7 @@ async function UIWindowSearch(options){
// Debounce function to limit rate of API calls
function debounce(func, wait) {
let timeout;
return function (...args) {
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
@@ -92,54 +92,57 @@ async function UIWindowSearch(options){
// Debounced search function
const performSearch = debounce(async function(searchInput, resultsContainer) {
// Don't search if input is empty
if (searchInput.val() === '') {
if ( searchInput.val() === '' ) {
resultsContainer.html('');
resultsContainer.hide();
return;
}
// Set loading state
if (!isSearching) {
if ( !isSearching ) {
isSearching = true;
}
try {
// Perform the search
let results = await fetch(window.api_origin + '/search', {
let results = await fetch(`${window.api_origin}/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${puter.authToken}`
'Authorization': `Bearer ${puter.authToken}`,
},
body: JSON.stringify({ text: searchInput.val() })
body: JSON.stringify({ text: searchInput.val() }),
});
results = await results.json();
// Hide results if there are none
if(results.length === 0)
if ( results.length === 0 )
{
resultsContainer.hide();
}
else
{
resultsContainer.show();
}
// Build results HTML
let h = '';
for(let i = 0; i < results.length; i++){
for ( let i = 0; i < results.length; i++ ){
const result = results[i];
h += `<div
h += `<div
class="search-result"
data-path="${html_encode(result.path)}"
data-path="${html_encode(result.path)}"
data-uid="${html_encode(result.uid)}"
data-is_dir="${html_encode(result.is_dir)}"
>`;
// icon
h += `<img src="${(await item_icon(result)).image}" style="width: 20px; height: 20px; margin-right: 6px;">`;
h += `<img class="search-result-icon" src="${(await item_icon(result)).image}">`;
h += html_encode(result.name);
h += `</div>`;
h += '</div>';
}
resultsContainer.html(h);
} catch (error) {
} catch( error ) {
resultsContainer.html('<div class="search-error">Search failed. Please try again.</div>');
console.error('Search error:', error);
} finally {
@@ -161,11 +164,11 @@ $(document).on('click', '.search-result', async function(e){
const is_dir = $(this).attr('data-is_dir') === 'true' || $(this).data('is_dir') === '1';
let open_item_meta;
if(is_dir){
if ( is_dir ){
UIWindow({
path: fspath,
title: path.basename(fspath),
icon: await item_icon({is_dir: true, path: fspath}),
icon: await item_icon({ is_dir: true, path: fspath }),
uid: fsuid,
is_dir: is_dir,
app: 'explorer',
@@ -178,57 +181,56 @@ $(document).on('click', '.search-result', async function(e){
}
// get all info needed to open an item
try{
try {
open_item_meta = await $.ajax({
url: window.api_origin + "/open_item",
url: `${window.api_origin}/open_item`,
type: 'POST',
contentType: "application/json",
contentType: 'application/json',
data: JSON.stringify({
uid: fsuid ?? undefined,
path: fspath ?? undefined,
}),
headers: {
"Authorization": "Bearer "+window.auth_token
'Authorization': `Bearer ${window.auth_token}`,
},
statusCode: {
401: function () {
401: function() {
window.logout();
},
},
});
}catch(err){
} catch( err ){
// Ignored
}
// get a list of suggested apps for this file type.
let suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({uid: fsuid, path: fspath});
let suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({ uid: fsuid, path: fspath });
//---------------------------------------------
// No suitable apps, ask if user would like to
// download
//---------------------------------------------
if(suggested_apps.length === 0){
if ( suggested_apps.length === 0 ){
//---------------------------------------------
// If .zip file, unzip it
//---------------------------------------------
if(path.extname(fspath) === '.zip'){
if ( path.extname(fspath) === '.zip' ){
window.unzipItem(fspath);
return;
}
const alert_resp = await UIAlert(
'Found no suitable apps to open this file with. Would you like to download it instead?',
[
{
label: i18n('download_file'),
value: 'download_file',
type: 'primary',
const alert_resp = await UIAlert('Found no suitable apps to open this file with. Would you like to download it instead?',
[
{
label: i18n('download_file'),
value: 'download_file',
type: 'primary',
},
{
label: i18n('cancel')
}
])
if(alert_resp === 'download_file'){
},
{
label: i18n('cancel'),
},
]);
if ( alert_resp === 'download_file' ){
window.trigger_download([fspath]);
}
return;
@@ -236,9 +238,9 @@ $(document).on('click', '.search-result', async function(e){
//---------------------------------------------
// First suggested app is default app to open this item
//---------------------------------------------
else{
else {
launch_app({
name: suggested_apps[0].name,
name: suggested_apps[0].name,
token: open_item_meta.token,
file_path: fspath,
app_obj: suggested_apps[0],
@@ -249,23 +251,22 @@ $(document).on('click', '.search-result', async function(e){
});
}
// close
$(this).closest('.window').close();
})
});
// Context menu for search results
$(document).on('contextmenu', '.search-result', async function(e){
e.preventDefault();
e.stopPropagation();
const fspath = $(this).data('path');
const fsuid = $(this).data('uid');
const is_dir = $(this).attr('data-is_dir') === 'true' || $(this).data('is_dir') === '1';
// Get the parent directory path
const parent_path = path.dirname(fspath);
// Build context menu items
const menuItems = [
{
@@ -273,12 +274,12 @@ $(document).on('contextmenu', '.search-result', async function(e){
onClick: async function() {
// Trigger the same logic as clicking on the search result
$(e.target).trigger('click');
}
}
},
},
];
// Only add "Open enclosing folder" if we're not already at root
if (parent_path && parent_path !== fspath && parent_path !== '/') {
if ( parent_path && parent_path !== fspath && parent_path !== '/' ) {
menuItems.push('-'); // divider
menuItems.push({
html: i18n('open_containing_folder'),
@@ -291,16 +292,16 @@ $(document).on('contextmenu', '.search-result', async function(e){
is_dir: true,
app: 'explorer',
});
// Close search window
$(e.target).closest('.window').close();
}
},
});
}
UIContextMenu({
items: menuItems
});
})
export default UIWindowSearch
UIContextMenu({
items: menuItems,
});
});
export default UIWindowSearch;

View File

@@ -31,75 +31,58 @@ function UIWindowSignup(options){
return new Promise(async (resolve) => {
const internal_id = window.uuidv4();
let h = '';
h += `<div style="margin: 0 auto; max-width: 500px; min-width: 400px;">`;
// logo
h += `<img src="${window.icons['logo-white.svg']}" style="width: 40px; height: 40px; margin: 0 auto; display: block; padding: 15px; background-color: blue; border-radius: 5px;">`;
// close button
if(!options.has_head && options.show_close_button !== false)
h += `<div class="generic-close-window-button"> &times; </div>`;
// Form
h += `<div style="padding: 20px; border-bottom: 1px solid #ced7e1;">`;
// title
h += `<h1 class="signup-form-title">${i18n('create_free_account')}</h1>`;
// signup form
h += `<form class="signup-form">`;
// error msg
h += `<div class="signup-error-msg"></div>`;
// username
h += `<div style="overflow: hidden;">`;
h += `<label for="username-${internal_id}">${i18n('username')}</label>`;
h += `<input id="username-${internal_id}" value="${html_encode(options.username ?? '')}" class="username" type="text" autocomplete="username" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>`;
h += `</div>`;
// email
h += `<div style="overflow: hidden; margin-top: 20px;">`;
h += `<label for="email-${internal_id}">${i18n('email')}</label>`;
h += `<input id="email-${internal_id}" value="${html_encode(options.email ?? '')}" class="email" type="email" autocomplete="email" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>`;
h += `</div>`;
// password
h += `<div style="overflow: hidden; margin-top: 20px; margin-bottom: 20px; position: relative;">`;
h += `<label for="password-${internal_id}">${i18n('password')}</label>`;
h += `<input id="password-${internal_id}" class="password" type="${options.show_password ? "text" : "password"}" name="password" autocomplete="new-password" />`;
// show/hide icon
h += `<span style="position: absolute; right: 5%; top: 50%; cursor: pointer;" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"]}" width="20" height="20">
</span>`;
h += `</div>`;
// confirm password
h += `<div style="overflow: hidden; margin-top: 20px; margin-bottom: 20px; position: relative">`;
h += `<label for="confirm-password-${internal_id}">${i18n('signup_confirm_password')}</label>`;
h += `<input id="confirm-password-${internal_id}" class="confirm-password" type="${options.show_password ? "text" : "password"}" name="confirm-password" autocomplete="new-password" />`;
// show/hide icon
h += `<span style="position: absolute; right: 5%; top: 50%; cursor: pointer;" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons["eye-closed.svg"] : window.icons["eye-open.svg"]}" width="20" height="20">
</span>`;
h += `</div>`;
// bot trap - if this value is submitted server will ignore the request
h += `<input type="text" name="p102xyzname" class="p102xyzname" value="">`;
// Turnstile widget (only when enabled)
if(window.gui_params?.turnstileSiteKey){
h += `<div style="min-height: 20px; display: flex; justify-content: center;">`;
// appearance: always/execute/interaction-only
// docs: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/?utm_source=chatgpt.com#appearance-modes
h += `<div class="cf-turnstile" data-sitekey="${window.gui_params.turnstileSiteKey}" data-appearance="interaction-only"></div>`;
h += `</div>`;
}
// terms and privacy
h += `<p class="signup-terms">${i18n('tos_fineprint', [], false)}</p>`;
// Create Account
h += `<button class="signup-btn button button-primary button-block button-normal">${i18n('create_free_account')}</button>`
h += `</form>`;
h += `</div>`;
// login link
// create account link
h += `<div class="c2a-wrapper" style="padding:20px;">`;
h += `<button class="login-c2a-clickable">${i18n('log_in')}</button>`;
h += `</div>`;
h += `</div>`;
const h = `
<div class="auth-container">
<div class="auth-logo-wrapper">
<img class="auth-logo" src="${window.icons['logo-white.svg']}">
</div>
${!options.has_head && options.show_close_button !== false ? `
<div class="generic-close-window-button">&times;</div>
` : ''}
<div class="auth-form-wrapper">
<div class="auth-title">
<h1>${i18n('create_free_account')}</h1>
</div>
<form class="auth-form signup-form">
<div class="auth-error-msg signup-error-msg"></div>
<div class="auth-form-group">
<label class="auth-label" for="username-${internal_id}">${i18n('username')}</label>
<input id="username-${internal_id}" value="${html_encode(options.username ?? '')}" class="auth-input username" type="text" autocomplete="username" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>
</div>
<div class="auth-form-group">
<label class="auth-label" for="email-${internal_id}">${i18n('email')}</label>
<input id="email-${internal_id}" value="${html_encode(options.email ?? '')}" class="auth-input email" type="email" autocomplete="email" spellcheck="false" autocorrect="off" autocapitalize="off" data-gramm_editor="false"/>
</div>
<div class="auth-form-group">
<label class="auth-label" for="password-${internal_id}">${i18n('password')}</label>
<input id="password-${internal_id}" class="auth-input password" type="${options.show_password ? 'text' : 'password'}" name="password" autocomplete="new-password" />
<span class="auth-password-toggle" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons['eye-closed.svg'] : window.icons['eye-open.svg']}">
</span>
</div>
<div class="auth-form-group">
<label class="auth-label" for="confirm-password-${internal_id}">${i18n('signup_confirm_password')}</label>
<input id="confirm-password-${internal_id}" class="auth-input confirm-password" type="${options.show_password ? 'text' : 'password'}" name="confirm-password" autocomplete="new-password" />
<span class="auth-password-toggle" id="toggle-show-password-${internal_id}">
<img class="toggle-show-password-icon" src="${options.show_password ? window.icons['eye-closed.svg'] : window.icons['eye-open.svg']}">
</span>
</div>
<input type="text" name="p102xyzname" class="p102xyzname" value="">
${window.gui_params?.turnstileSiteKey ? `
<div class="signup-captcha-wrapper">
<div class="cf-turnstile" data-sitekey="${window.gui_params.turnstileSiteKey}" data-appearance="interaction-only"></div>
</div>
` : ''}
<button class="signup-btn button button-primary button-block button-normal">${i18n('create_free_account')}</button>
<p class="signup-terms">${i18n('tos_fineprint', [], false)}</p>
</form>
</div>
<div class="c2a-wrapper">
<button class="login-c2a-clickable">${i18n('log_in')}</button>
</div>
</div>
`;
const el_window = await UIWindow({
title: null,

View File

@@ -143,21 +143,20 @@ const generate_task_rows = (items, { indent_level, is_last_item_stack }) => {
const UIWindowTaskManager = async function UIWindowTaskManager () {
const svc_process = globalThis.services.get('process');
let h = '';
h += `<div class="task-manager-container">`;
h += `<table>`;
h += `<thead>`;
h += `<tr>`;
h += `<th>${i18n('taskmgr_header_name')}</th>`;
h += `<th>${i18n('taskmgr_header_type')}</th>`;
h += `<th>${i18n('taskmgr_header_status')}</th>`;
h += `</tr>`;
h += `</thead>`;
h += `<tbody class="taskmgr-taskarea">`;
h += `</tbody>`;
h += `</table>`;
h += `</div>`;
const h = `
<div class="task-manager-container">
<table>
<thead>
<tr>
<th>${i18n('taskmgr_header_name')}</th>
<th>${i18n('taskmgr_header_type')}</th>
<th>${i18n('taskmgr_header_status')}</th>
</tr>
</thead>
<tbody class="taskmgr-taskarea"></tbody>
</table>
</div>
`;
const el_window = await UIWindow({
title: i18n('task_manager'),
@@ -182,12 +181,7 @@ const UIWindowTaskManager = async function UIWindowTaskManager () {
},
body_css: {
width: 'initial',
padding: '20px',
'background-color': `hsla(
var(--primary-hue),
var(--primary-saturation),
var(--primary-lightness),
var(--primary-alpha))`,
'background-color': '#F5F5F7',
'backdrop-filter': 'blur(3px)',
'box-sizing': 'border-box',
height: 'calc(100% - 30px)',
@@ -195,7 +189,7 @@ const UIWindowTaskManager = async function UIWindowTaskManager () {
'flex-direction': 'column',
'--scale': '2pt',
'--line-color': '#6e6e6ebd',
padding: '0',
padding: '20px',
},
});

View File

@@ -23,28 +23,83 @@ const UIWindowThemeDialog = async function UIWindowThemeDialog (options) {
const services = globalThis.services;
const svc_theme = services.get('theme');
let state = {};
let state = {
hue: svc_theme.get('hue'),
sat: svc_theme.get('sat'),
lig: svc_theme.get('lig'),
alpha: svc_theme.get('alpha'),
light_text: svc_theme.get('light_text'),
};
let h = '';
h += '<div class="theme-dialog-content" style="display: flex; flex-direction: column; gap: 10pt;">';
h += `<button class="button button-secondary reset-colors-btn">${i18n('reset_colors')}</button>`;
h += `<div class="slider-container" style="display: flex; flex-direction: column; gap: 5px;">`;
h += `<label style="font-weight: 500; color: #5f626d;">${i18n('hue')}</label>`;
h += `<input type="range" class="theme-slider" id="hue-slider" name="hue" min="0" max="360" value="${svc_theme.get('hue')}" style="width: 100%;">`;
h += `</div>`;
h += `<div class="slider-container" style="display: flex; flex-direction: column; gap: 5px;">`;
h += `<label style="font-weight: 500; color: #5f626d;">${i18n('saturation')}</label>`;
h += `<input type="range" class="theme-slider" id="sat-slider" name="sat" min="0" max="100" value="${svc_theme.get('sat')}" style="width: 100%;">`;
h += `</div>`;
h += `<div class="slider-container" style="display: flex; flex-direction: column; gap: 5px;">`;
h += `<label style="font-weight: 500; color: #5f626d;">${i18n('lightness')}</label>`;
h += `<input type="range" class="theme-slider" id="lig-slider" name="lig" min="0" max="100" value="${svc_theme.get('lig')}" style="width: 100%;">`;
h += `</div>`;
h += `<div class="slider-container" style="display: flex; flex-direction: column; gap: 5px;">`;
h += `<label style="font-weight: 500; color: #5f626d;">${i18n('transparency')}</label>`;
h += `<input type="range" class="theme-slider" id="alpha-slider" name="alpha" min="0" max="1" step="0.01" value="${svc_theme.get('alpha')}" style="width: 100%;">`;
h += `</div>`;
h += '</div>';
const h = `
<div class="theme-dialog-content">
<div class="theme-presets">
<button class="theme-preset-btn" data-preset="default">
<div class="preset-color" style="background: hsl(211, 51%, 51%);"></div>
<span>Default</span>
</button>
<button class="theme-preset-btn" data-preset="teal">
<div class="preset-color" style="background: hsl(180, 60%, 50%);"></div>
<span>Teal</span>
</button>
<button class="theme-preset-btn" data-preset="purple">
<div class="preset-color" style="background: hsl(270, 60%, 55%);"></div>
<span>Purple</span>
</button>
<button class="theme-preset-btn" data-preset="forest">
<div class="preset-color" style="background: hsl(140, 60%, 50%);"></div>
<span>Forest</span>
</button>
<button class="theme-preset-btn" data-preset="sunset">
<div class="preset-color" style="background: hsl(25, 85%, 60%);"></div>
<span>Sunset</span>
</button>
<button class="theme-preset-btn" data-preset="rose">
<div class="preset-color" style="background: hsl(340, 70%, 60%);"></div>
<span>Rose</span>
</button>
</div>
<div class="theme-actions-group">
<button class="customize-toggle-btn">
<span>Customize colors</span>
<svg class="chevron-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
<button class="button button-secondary reset-colors-btn">${i18n('reset_colors')}</button>
</div>
<div class="theme-sliders collapsed">
<div class="slider-container">
<label class="slider-label">
<span>${i18n('hue')}</span>
<span class="slider-value" id="hue-value">${svc_theme.get('hue')}°</span>
</label>
<input type="range" class="theme-slider hue-slider" id="hue-slider" name="hue" min="0" max="360" value="${svc_theme.get('hue')}">
</div>
<div class="slider-container">
<label class="slider-label">
<span>${i18n('saturation')}</span>
<span class="slider-value" id="sat-value">${svc_theme.get('sat')}%</span>
</label>
<input type="range" class="theme-slider sat-slider" id="sat-slider" name="sat" min="0" max="100" value="${svc_theme.get('sat')}">
</div>
<div class="slider-container">
<label class="slider-label">
<span>${i18n('lightness')}</span>
<span class="slider-value" id="lig-value">${svc_theme.get('lig')}%</span>
</label>
<input type="range" class="theme-slider lig-slider" id="lig-slider" name="lig" min="0" max="100" value="${svc_theme.get('lig')}">
</div>
<div class="slider-container">
<label class="slider-label">
<span>${i18n('transparency')}</span>
<span class="slider-value" id="alpha-value">${Math.round(svc_theme.get('alpha') * 100)}%</span>
</label>
<input type="range" class="theme-slider alpha-slider" id="alpha-slider" name="alpha" min="0" max="1" step="0.01" value="${svc_theme.get('alpha')}">
</div>
</div>
</div>
`;
const el_window = await UIWindow({
title: i18n('ui_colors'),
@@ -62,41 +117,163 @@ const UIWindowThemeDialog = async function UIWindowThemeDialog (options) {
show_in_taskbar: false,
window_class: 'window-alert',
dominant: true,
width: 350,
width: 420,
window_css:{
height: 'initial',
},
body_css: {
width: 'initial',
padding: '20px',
'background-color': `hsla(
var(--primary-hue),
var(--primary-saturation),
var(--primary-lightness),
var(--primary-alpha))`,
'backdrop-filter': 'blur(3px)',
'background-color': 'rgb(245 247 249)',
},
...options.window_options,
});
// Event handlers
$(el_window).find('.theme-slider').on('input', function(e) {
// Create a style element for dynamic slider backgrounds
const styleId = 'theme-slider-dynamic-styles';
let styleEl = document.getElementById(styleId);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = styleId;
document.head.appendChild(styleEl);
}
// Function to update slider backgrounds based on current state
const updateSliderBackgrounds = () => {
const hue = state.hue || 211;
const sat = state.sat || 51;
const lig = state.lig || 51;
// Update saturation slider - from gray to full saturation at current hue
const satGradient = `linear-gradient(to right, hsl(${hue}, 0%, 50%), hsl(${hue}, 100%, 50%))`;
// Update lightness slider - from black through current color to white
const ligGradient = `linear-gradient(to right, hsl(${hue}, ${sat}%, 0%), hsl(${hue}, ${sat}%, 50%), hsl(${hue}, ${sat}%, 100%))`;
// Update alpha slider - from transparent to current color
const currentColor = `hsl(${hue}, ${sat}%, ${lig}%)`;
const alphaGradient = `linear-gradient(to right, transparent, ${currentColor}), repeating-conic-gradient(#e0e0e0 0% 25%, #ffffff 0% 50%) 50% / 12px 12px`;
// Inject dynamic styles
styleEl.textContent = `
.sat-slider::-webkit-slider-runnable-track {
background: ${satGradient};
}
.sat-slider::-moz-range-track {
background: ${satGradient};
}
.lig-slider::-webkit-slider-runnable-track {
background: ${ligGradient};
}
.lig-slider::-moz-range-track {
background: ${ligGradient};
}
.alpha-slider::-webkit-slider-runnable-track {
background: ${alphaGradient};
}
.alpha-slider::-moz-range-track {
background: ${alphaGradient};
}
`;
};
// Initialize slider backgrounds
updateSliderBackgrounds();
// Theme presets
const presets = {
default: { hue: 210, sat: 41.18, lig: 93.33, alpha: 0.8 },
teal: { hue: 180, sat: 60, lig: 50, alpha: 0.9 },
purple: { hue: 270, sat: 60, lig: 55, alpha: 0.9 },
forest: { hue: 140, sat: 60, lig: 50, alpha: 0.9 },
sunset: { hue: 25, sat: 85, lig: 60, alpha: 0.9 },
rose: { hue: 340, sat: 70, lig: 60, alpha: 0.9 },
};
$(el_window).find('.theme-preset-btn').on('click', function(e) {
e.preventDefault();
const presetName = $(this).data('preset');
const preset = presets[presetName];
if (!preset) return;
// Update sliders
$(el_window).find('#hue-slider').val(preset.hue);
$(el_window).find('#sat-slider').val(preset.sat);
$(el_window).find('#lig-slider').val(preset.lig);
$(el_window).find('#alpha-slider').val(preset.alpha);
// Update displayed values
$(el_window).find('#hue-value').text(`${preset.hue}°`);
$(el_window).find('#sat-value').text(`${preset.sat}%`);
$(el_window).find('#lig-value').text(`${preset.lig}%`);
$(el_window).find('#alpha-value').text(`${Math.round(preset.alpha * 100)}%`);
// Apply theme
state = {
hue: preset.hue,
sat: preset.sat,
lig: preset.lig,
alpha: preset.alpha,
light_text: preset.lig < 70
};
svc_theme.apply(state);
updateSliderBackgrounds();
});
// Customize toggle handler
$(el_window).find('.customize-toggle-btn').on('click', function() {
const $sliders = $(el_window).find('.theme-sliders');
const $chevron = $(this).find('.chevron-icon');
$sliders.toggleClass('collapsed');
$chevron.toggleClass('rotated');
});
$(el_window).find('.theme-slider').on('input', function() {
const name = $(this).attr('name');
const value = parseFloat($(this).val());
state[name] = value;
if (name === 'lig') {
state.light_text = value < 60 ? true : false;
state.light_text = value < 70 ? true : false;
}
if (name === 'hue') {
$(el_window).find('#hue-value').text(`${Math.round(value)}°`);
} else if (name === 'sat') {
$(el_window).find('#sat-value').text(`${Math.round(value)}%`);
} else if (name === 'lig') {
$(el_window).find('#lig-value').text(`${Math.round(value)}%`);
} else if (name === 'alpha') {
$(el_window).find('#alpha-value').text(`${Math.round(value * 100)}%`);
}
svc_theme.apply(state);
updateSliderBackgrounds();
});
$(el_window).find('.reset-colors-btn').on('click', function() {
svc_theme.reset();
state = {};
$(el_window).find('#hue-slider').val(svc_theme.get('hue'));
$(el_window).find('#sat-slider').val(svc_theme.get('sat'));
$(el_window).find('#lig-slider').val(svc_theme.get('lig'));
$(el_window).find('#alpha-slider').val(svc_theme.get('alpha'));
state = {
hue: svc_theme.get('hue'),
sat: svc_theme.get('sat'),
lig: svc_theme.get('lig'),
alpha: svc_theme.get('alpha'),
light_text: svc_theme.get('light_text'),
};
$(el_window).find('#hue-slider').val(state.hue);
$(el_window).find('#sat-slider').val(state.sat);
$(el_window).find('#lig-slider').val(state.lig);
$(el_window).find('#alpha-slider').val(state.alpha);
$(el_window).find('#hue-value').text(`${state.hue}°`);
$(el_window).find('#sat-value').text(`${state.sat}%`);
$(el_window).find('#lig-value').text(`${state.lig}%`);
$(el_window).find('#alpha-value').text(`${Math.round(state.alpha * 100)}%`);
updateSliderBackgrounds();
});
return {};

File diff suppressed because it is too large Load Diff

View File

@@ -345,6 +345,7 @@ const en = {
tarring: "Tarring %strong%",
// === 2FA Setup ===
setup_2fa: 'Set Up Two-Factor Authentication',
setup2fa_1_step_heading: 'Open your authenticator app',
setup2fa_1_instructions: `
You can use any authenticator app that supports the Time-based One-Time Password (TOTP) protocol.
@@ -363,6 +364,28 @@ const en = {
setup2fa_5_confirmation_1: 'I have saved my recovery codes in a secure location',
setup2fa_5_confirmation_2: 'I am ready to enable 2FA',
setup2fa_5_button: 'Enable 2FA',
setup2fa_intro_title: 'Secure Your Account',
setup2fa_intro_description: 'Two-factor authentication adds an extra layer of security to your account by requiring a code from your phone in addition to your password.',
setup2fa_intro_what_youll_need: 'What you\'ll need',
setup2fa_intro_requirements: 'An authenticator app like Google Authenticator, Authy, or 1Password on your smartphone.',
setup2fa_intro_steps_title: 'What we\'ll do',
setup2fa_intro_step_1: 'Scan a QR code with your authenticator app',
setup2fa_intro_step_2: 'Verify the setup by entering a code',
setup2fa_intro_step_3: 'Save your recovery codes in a safe place',
setup2fa_scan_title: 'Scan QR Code',
setup2fa_scan_description: 'Open your authenticator app and scan this QR code to add your Puter account.',
setup2fa_manual_setup: 'Or enter this code manually:',
setup2fa_verify_title: 'Verify Setup',
setup2fa_verify_description: 'Enter the 6-digit code from your authenticator app to make sure everything is working.',
setup2fa_recovery_title: 'Save Your Recovery Codes',
setup2fa_recovery_warning: 'Keep these codes safe. You\'ll need them to access your account if you lose your device.',
setup2fa_codes_saved_confirmation: 'I have saved my recovery codes in a safe place',
code_invalid: 'Invalid code. Please try again.',
copied: 'Copied!',
back: 'Back',
continue: 'Continue',
verify_code: 'Verify Code',
finish: 'Finish',
// === 2FA Login ===
login2fa_otp_title: 'Enter 2FA Code',
@@ -510,6 +533,38 @@ const en = {
'set_as_background': 'Set as Desktop Background',
// Settings - Usage Tab
'view_usage_details': 'View usage details',
'hide_usage_details': 'Hide usage details',
'resource': 'Resource',
'resource_units': 'Units',
'resource_cost': 'Cost',
// Settings - Account Tab
'profile_picture': 'Profile Picture',
'toggle_password_visibility': 'Toggle password visibility',
// Settings - Personalization Tab
'option_auto': 'Auto',
'option_hide': 'Hide',
'option_show': 'Show',
// Settings - Sessions Manager
'unknown': 'Unknown',
// TODO: Better time localization might be needed in the future
'day': 'day',
'days': 'days',
'hour': 'hour',
'hours': 'hours',
'minute': 'minute',
'minutes': 'minutes',
'ago': 'ago',
'just_now': 'Just now',
'device_desktop': 'Desktop',
'device_mobile': 'Mobile',
'device_tablet': 'Tablet',
'current': 'Current',
'session_manager_description': 'Manage your active sessions across all devices. You can revoke access to any session except your current one.',
'error_user_or_path_not_found': 'User or path not found.',
'error_invalid_username': 'Invalid username.',
}

View File

@@ -21,7 +21,6 @@ import { Service } from "../definitions.js";
import AboutTab from '../UI/Settings/UITabAbout.js';
import UsageTab from '../UI/Settings/UITabUsage.js';
import AccountTab from '../UI/Settings/UITabAccount.js';
import SecurityTab from '../UI/Settings/UITabSecurity.js';
import PersonalizationTab from '../UI/Settings/UITabPersonalization.js';
import LanguageTag from '../UI/Settings/UITabLanguage.js';
import UIElement from "../UI/UIElement.js";
@@ -33,7 +32,6 @@ export class SettingsService extends Service {
;[
UsageTab,
AccountTab,
SecurityTab,
PersonalizationTab,
LanguageTag,
AboutTab,