mirror of
https://github.com/HeyPuter/puter.git
synced 2026-01-05 20:50:22 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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)} • Server: ${html_encode(res.location)} • 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)} • Server: ${html_encode(res.location)} • 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();
|
||||
})
|
||||
});
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;"> (<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', '');
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"> × </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')}"> × </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;
|
||||
@@ -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"> × </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"> × </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')}"> × </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')}"> × </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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 = () => {
|
||||
@@ -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;
|
||||
56
src/gui/src/UI/Settings/helpers/build_settings_card.js
Normal file
56
src/gui/src/UI/Settings/helpers/build_settings_card.js
Normal 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>`;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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)',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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"> × </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">×</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',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"> × </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">×</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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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.',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user