Răsfoiți Sursa

Merge pull request #240 from HeyPuter/eric/gui-personalization

Puter Theme Color Setting
Nariman Jelveh 1 an în urmă
părinte
comite
ea61799b3d

+ 2 - 1
puter-gui.json

@@ -19,7 +19,8 @@
     "css_paths": [
         "/css/normalize.css",
         "/lib/jquery-ui-1.13.2/jquery-ui.min.css",
-        "/css/style.css"
+        "/css/style.css",
+        "/css/theme.css"
     ],
     "js_paths": [
         "/src/initgui.js",

+ 18 - 0
src/UI/Settings/UIWindowSettings.js

@@ -25,6 +25,7 @@ import UIWindowChangeUsername from '../UIWindowChangeUsername.js'
 import changeLanguage from "../../i18n/i18nChangeLanguage.js"
 import UIWindowConfirmUserDeletion from './UIWindowConfirmUserDeletion.js';
 import UITabAbout from './UITabAbout.js';
+import UIWindowThemeDialog from '../UIWindowThemeDialog.js';
 
 async function UIWindowSettings(options){
     return new Promise(async (resolve) => {
@@ -39,6 +40,7 @@ async function UIWindowSettings(options){
                 h += `<div class="settings-sidebar-item disable-user-select active" data-settings="about" style="background-image: url(${icons['logo-outline.svg']});">${i18n('about')}</div>`;
                 h += `<div class="settings-sidebar-item disable-user-select" data-settings="usage" style="background-image: url(${icons['speedometer-outline.svg']});">${i18n('usage')}</div>`;
                 h += `<div class="settings-sidebar-item disable-user-select" data-settings="account" style="background-image: url(${icons['user.svg']});">${i18n('account')}</div>`;
+                h += `<div class="settings-sidebar-item disable-user-select" data-settings="personalization" style="background-image: url(${icons['palette-outline.svg']});">${i18n('personalization')}</div>`;
                 h += `<div class="settings-sidebar-item disable-user-select" data-settings="language" style="background-image: url(${icons['language.svg']});">${i18n('language')}</div>`;
                 h += `<div class="settings-sidebar-item disable-user-select" data-settings="clock" style="background-image: url(${icons['clock.svg']});">${i18n('clock')}</div>`;
             h += `</div>`;
@@ -111,6 +113,18 @@ async function UIWindowSettings(options){
 
                 h += `</div>`;
 
+                // Personalization
+                h += `<div class="settings-content" data-settings="personalization">`;
+                    h += `<h1>${i18n('personalization')}</h1>`;
+                    // change password button
+                    h += `<div class="settings-card">`;
+                        h += `<strong>${i18n('ui_colors')}</strong>`;
+                        h += `<div style="flex-grow:1;">`;
+                            h += `<button class="button change-ui-colors" style="float:right;">${i18n('change_ui_colors')}</button>`;
+                        h += `</div>`;
+                    h += `</div>`;
+                h += `</div>`;
+
                 // Language
                 h += `<div class="settings-content" data-settings="language">`;
                     h += `<h1>${i18n('language')}</h1>`;
@@ -306,6 +320,10 @@ async function UIWindowSettings(options){
             UIWindowChangeUsername();
         })
 
+        $(el_window).find('.change-ui-colors').on('click', function (e) {
+            UIWindowThemeDialog();
+        })
+
         $(el_window).on('click', '.settings-sidebar-item', function(){
             const $this = $(this);
             const settings = $this.attr('data-settings');

+ 145 - 0
src/UI/UIWindowThemeDialog.js

@@ -0,0 +1,145 @@
+import UIWindow from "./UIWindow.js";
+
+const UIWindowThemeDialog = async function UIWindowThemeDialog () {
+    const services = globalThis.services;
+    const svc_theme = services.get('theme');
+
+    const w = await UIWindow({
+        title: i18n('ui_colors'),
+        icon: null,
+        uid: null,
+        is_dir: false,
+        message: 'message',
+        // body_icon: options.body_icon,
+        // backdrop: options.backdrop ?? false,
+        is_resizable: false,
+        is_droppable: false,
+        has_head: true,
+        stay_on_top: true,
+        selectable_body: false,
+        draggable_body: true,
+        allow_context_menu: false,
+        show_in_taskbar: false,
+        window_class: 'window-alert',
+        dominant: true,
+        body_content: '',
+        width: 350,
+        // parent_uuid: options.parent_uuid,
+        // ...options.window_options,
+        window_css:{
+            height: 'initial',
+        },
+        body_css: {
+            width: 'initial',
+            padding: '20px',
+            // 'background-color': `hsla(
+            //     var(--primary-hue),
+            //     calc(max(var(--primary-saturation) - 15%, 0%)),
+            //     calc(min(100%,var(--primary-lightness) + 20%)), .91)`,
+            'background-color': `hsla(
+                var(--primary-hue),
+                var(--primary-saturation),
+                var(--primary-lightness),
+                var(--primary-alpha))`,
+            'backdrop-filter': 'blur(3px)',
+        }
+    });
+    const w_body = w.querySelector('.window-body');
+
+    const Button = ({ label }) => {
+        const el = document.createElement('button');
+        el.textContent = label;
+        el.classList.add('button', 'button-block');
+        return {
+            appendTo (parent) {
+                parent.appendChild(el);
+                return this;
+            },
+            onPress (cb) {
+                el.addEventListener('click', cb);
+                return this;
+            },
+        };
+    }
+
+    const Slider = ({ name, label, min, max, initial, step }) => {
+        label = label ?? name;
+        const wrap = document.createElement('div');
+        const label_el = document.createElement('label');
+        label_el.textContent = label;
+        wrap.appendChild(label_el);
+        const el = document.createElement('input');
+        wrap.appendChild(el);
+        el.type = 'range';
+        el.min = min;
+        el.max = max;
+        el.defaultValue = initial ?? min;
+        el.step = step ?? 1;
+        el.classList.add('theme-dialog-slider');
+
+        return {
+            appendTo (parent) {
+                parent.appendChild(wrap);
+                return this;
+            },
+            onChange (cb) {
+                el.addEventListener('input', e => {
+                    e.meta = { name, label };
+                    cb(e);
+                });
+                return this;
+            },
+        };
+    };
+
+    const state = {};
+
+    const slider_ch = (e) => {
+        state[e.meta.name] = e.target.value;
+        svc_theme.apply(state);
+    };
+
+    Button({ label: i18n('reset_colors') })
+        .appendTo(w_body)
+        .onPress(() => {
+            svc_theme.reset();
+        })
+        ;
+
+    Slider({
+        label: i18n('hue'),
+        name: 'hue', min: 0, max: 360,
+        initial: svc_theme.get('hue'),
+    })
+        .appendTo(w_body)
+        .onChange(slider_ch)
+        ;
+    Slider({
+        label: i18n('saturation'),
+        name: 'sat', min: 0, max: 100,
+        initial: svc_theme.get('sat'),
+    })
+        .appendTo(w_body)
+        .onChange(slider_ch)
+        ;
+    Slider({
+        label: i18n('lightness'),
+        name: 'lig', min: 0, max: 100,
+        initial: svc_theme.get('lig'),
+    })
+        .appendTo(w_body)
+        .onChange(slider_ch)
+        ;
+    Slider({
+        label: i18n('transparency'),
+        name: 'alpha', min: 0, max: 1, step: 0.01,
+        initial: svc_theme.get('alpha'),
+    })
+        .appendTo(w_body)
+        .onChange(slider_ch)
+        ;
+
+    return {};
+}
+
+export default UIWindowThemeDialog;

+ 91 - 7
src/css/style.css

@@ -22,6 +22,28 @@
     user-select: none;
 }
 
+:root {
+    --primary-hue: 210;
+    --primary-saturation: 41.18%;
+    --primary-lightness: 93.33%;
+    --primary-alpha: 0.8;
+
+    --window-head-hue: var(--primary-hue);
+    --window-head-saturation: var(--primary-saturation);
+    --window-head-lightness: var(--primary-lightness);
+    --window-head-alpha: var(--primary-alpha);
+
+    --window-sidebar-hue: var(--primary-hue);
+    --window-sidebar-saturation: var(--primary-saturation);
+    --window-sidebar-lightness: var(--primary-lightness);
+    --window-sidebar-alpha: calc(min(1, 0.11 + var(--primary-alpha)));
+
+    --taskbar-hue: var(--primary-hue);
+    --taskbar-saturation: var(--primary-saturation);
+    --taskbar-lightness: var(--primary-lightness);
+    --taskbar-alpha: calc(0.73 * var(--primary-alpha));
+}
+
 html, body {
     /* disables two fingers back/forward swipe */
     overscroll-behavior-x: none;
@@ -827,7 +849,12 @@ span.header-sort-icon img {
 .window-head {
     overflow: hidden !important;
     padding: 0;
-    background-color: rgba(231, 238, 245, .95);
+    background-color: hsla(
+        var(--window-head-hue),
+        var(--window-head-saturation),
+        var(--window-head-lightness),
+        calc(0.5 + 0.5 * var(--window-head-alpha))
+    );
     filter: grayscale(80%);
     box-shadow: inset 0px -4px 5px -7px rgb(0 0 0 / 64%);
     display: flex;
@@ -1025,7 +1052,12 @@ span.header-sort-icon img {
     border-right: 1px solid #CCC;
     padding: 15px 10px;
     box-sizing: border-box;
-    background-color: rgba(231, 238, 245, .95);
+    background-color: hsla(
+        var(--window-sidebar-hue),
+        var(--window-sidebar-saturation),
+        var(--window-sidebar-lightness),
+        calc(0.5 + 0.5*var(--window-sidebar-alpha))
+    );
     overflow-y: scroll;
     margin-top: 1px;
 }
@@ -2019,7 +2051,12 @@ label {
     bottom: 0;
     left: 0;
     width: 100%;
-    background-color: rgba(231, 238, 245, .9);
+    background-color: hsla(
+        var(--taskbar-hue),
+        var(--taskbar-saturation),
+        var(--taskbar-lightness),
+        calc(0.5 + 0.5*var(--taskbar-alpha))
+    );
     display: flex;
     justify-content: center;
     z-index: 99999;
@@ -2126,7 +2163,12 @@ label {
 
 @supports ((backdrop-filter: blur())) {
     .taskbar {
-        background-color: #ffffff94;
+        background-color: hsla(
+            var(--taskbar-hue),
+            var(--taskbar-saturation),
+            var(--taskbar-lightness),
+            var(--taskbar-alpha)
+        );
         backdrop-filter: blur(10px);
     }
 
@@ -2646,7 +2688,12 @@ label {
 
 @supports ((backdrop-filter: blur())) {
     .window-head {
-        background-color: rgba(231, 238, 245, .80);
+        background-color: hsla(
+            var(--window-head-hue),
+            var(--window-head-saturation),
+            var(--window-head-lightness),
+            var(--window-head-alpha)
+        );
         backdrop-filter: blur(10px);
     }
 
@@ -2656,7 +2703,13 @@ label {
     }
 
     .window-sidebar {
-        background-color: rgb(231 238 245 / 91%);
+        /* background-color: var(--puter-window-background); */
+        background-color: hsla(
+            var(--window-sidebar-hue),
+            var(--window-sidebar-saturation),
+            var(--window-sidebar-lightness),
+            var(--window-sidebar-alpha)
+        );
         backdrop-filter: blur(10px);
     }
 
@@ -3618,4 +3671,35 @@ label {
 .confirm-user-deletion-password{
     width: 100%; 
     margin-bottom: 20px;
-}
+}
+
+.theme-dialog-slider {
+  --webkit-appearance: none;
+  width: 100%;
+  height: 25px;
+  background: #d3d3d3;
+  outline: none;
+  opacity: 0.7;
+  --webkit-transition: .2s;
+  transition: opacity .2s;
+}
+
+.theme-dialog-slider:hover {
+  opacity: 1;
+}
+
+.theme-dialog-slider::-webkit-slider-thumb {
+  --webkit-appearance: none;
+  appearance: none;
+  width: 25px;
+  height: 25px;
+  background: #04AA6D;
+  cursor: pointer;
+}
+
+.theme-dialog-slider::-moz-range-thumb {
+  width: 25px;
+  height: 25px;
+  background: #04AA6D;
+  cursor: pointer;
+}

+ 7 - 0
src/css/theme.css

@@ -0,0 +1,7 @@
+/* used for pseudo-stylesheet */
+
+/*
+
+hue = 320; ss.addRule('.taskbar, .window-head, .window-sidebar', `background-color: hsl(${hue}, 65.1%, 70.78%)`)
+
+*/

+ 3 - 0
src/definitions.js

@@ -0,0 +1,3 @@
+export class Service {
+    //
+};

+ 9 - 0
src/i18n/translations/en.js

@@ -37,11 +37,13 @@ const en = {
         change_email: "Change Email",
         change_language: "Change Language",
         change_password: "Change Password",
+        change_ui_colors: "Change UI Colors",
         change_username: "Change Username",
         close_all_windows: "Close All Windows",
         close_all_windows_and_log_out: 'Close Windows and Log Out',
         change_always_open_with: "Do you want to always open this type of file with",
         color: 'Color',
+        hue: 'Hue',
         confirm_account_for_free_referral_storage_c2a: 'Create an account and confirm your email address to receive 1 GB of free storage. Your friend will get 1 GB of free storage too.',
         confirm_delete_multiple_items: 'Are you sure you want to permanently delete these items?',
         confirm_delete_single_item: 'Do you want to permanently delete this item?',
@@ -105,6 +107,7 @@ const en = {
         keep_in_taskbar: 'Keep in Taskbar',
         language: "Language",
         license: "License",
+        lightness: 'Lightness',
         loading: 'Loading',
         log_in: "Log In",
         log_into_another_account_anyway: 'Log into another account anyway',
@@ -137,6 +140,7 @@ const en = {
         passwords_do_not_match: '`New Password` and `Confirm New Password` do not match.',
         paste: 'Paste',
         paste_into_folder: "Paste Into Folder",
+        personalization: "Personalization",
         pick_name_for_website: "Pick a name for your website:",
         picture: "Picture",
         plural_suffix: 's',
@@ -163,7 +167,9 @@ const en = {
         replace: 'Replace',
         replace_all: 'Replace All',
         resend_confirmation_code: "Re-send Confirmation Code",
+        reset_colors: "Reset Colors",
         restore: "Restore",
+        saturation: 'Saturation',
         save_account: 'Save account',
         save_account_to_get_copy_link: "Please create an account to proceed.",
         save_account_to_publish: 'Please create an account to proceed.',
@@ -194,9 +200,11 @@ const en = {
         terms: "Terms",
         text_document: 'Text document',
         tos_fineprint: `By clicking 'Create Free Account' you agree to Puter's {{link=terms}}Terms of Service{{/link}} and {{link=privacy}}Privacy Policy{{/link}}.`,
+        transparency: "Transparency",
         trash: 'Trash',
         type: 'Type',
         type_confirm_to_delete_account: "Type 'confirm' to delete your account.",
+        ui_colors: "UI Colors",
         undo: 'Undo',
         unlimited: 'Unlimited',
         unzip: "Unzip",
@@ -211,6 +219,7 @@ const en = {
         yes_release_it: 'Yes, Release It',
         you_have_been_referred_to_puter_by_a_friend: "You have been referred to Puter by a friend!",
         zip: "Zip",
+        storage_puter_used: "used by Puter",
     }
 };
 

+ 23 - 0
src/initgui.js

@@ -34,6 +34,27 @@ import update_last_touch_coordinates from './helpers/update_last_touch_coordinat
 import update_title_based_on_uploads from './helpers/update_title_based_on_uploads.js';
 import PuterDialog from './UI/PuterDialog.js';
 import determine_active_container_parent from './helpers/determine_active_container_parent.js';
+import { ThemeService } from './services/ThemeService.js';
+import UIWindowThemeDialog from './UI/UIWindowThemeDialog.js';
+
+const launch_services = async function () {
+    const services_l_ = [];
+    const services_m_ = {};
+    globalThis.services = {
+        get: (name) => services_m_[name],
+    };
+
+    const register = (name, instance) => {
+        services_l_.push([name, instance]);
+        services_m_[name] = instance;
+    }
+
+    register('theme', new ThemeService());
+
+    for (const [_, instance] of services_l_) {
+        await instance._init();
+    }
+};
 
 window.initgui = async function(){
     let url = new URL(window.location);
@@ -1947,6 +1968,8 @@ window.initgui = async function(){
         // go to home page
         window.location.replace("/");
     });    
+
+    await launch_services();
 }
 
 function requestOpenerOrigin() {

+ 115 - 0
src/services/ThemeService.js

@@ -0,0 +1,115 @@
+import UIAlert from "../UI/UIAlert.js";
+import { Service } from "../definitions.js";
+
+const PUTER_THEME_DATA_FILENAME = '~/.__puter_gui.json';
+
+const SAVE_COOLDOWN_TIME = 1000;
+
+const default_values = {
+    sat: 41.18,
+    hue: 210,
+    lig: 93.33,
+    alpha: 0.8,
+};
+
+export class ThemeService extends Service {
+    async _init () {
+        this.state = {
+            sat: 41.18,
+            hue: 210,
+            lig: 93.33,
+            alpha: 0.8,
+        };
+        this.root = document.querySelector(':root');
+        // this.ss = new CSSStyleSheet();
+        // document.adoptedStyleSheets.push(this.ss);
+
+        this.save_cooldown_ = undefined;
+
+        let data = undefined;
+        try {
+            data = await puter.fs.read(PUTER_THEME_DATA_FILENAME);
+            if ( typeof data === 'object' ) {
+                data = await data.text();
+            }
+        } catch (e) {
+            if ( e.code !== 'subject_does_not_exist' ) {
+                // TODO: once we have an event log,
+                //       log this error to the event log
+                console.error(e);
+
+                // We don't show an alert becuase it's likely
+                // other things also aren't working.
+            }
+        }
+
+        if ( data ) try {
+            data = JSON.parse(data.toString());
+        } catch (e) {
+            data = undefined;
+            console.error(e);
+
+            UIAlert({
+                title: 'Error loading theme data',
+                message: `Could not parse "${PUTER_THEME_DATA_FILENAME}": ` +
+                    e.message,
+            });
+        }
+
+        if ( data && data.colors ) {
+            this.state = {
+                ...this.state,
+                ...data.colors,
+            };
+            this.reload_();
+        }
+    }
+
+    reset () {
+        this.state = default_values;
+        this.reload_();
+        puter.fs.delete(PUTER_THEME_DATA_FILENAME);
+    }
+
+    apply (values) {
+        this.state = {
+            ...this.state,
+            ...values,
+        };
+        this.reload_();
+        this.save_();
+    }
+
+    get (key) { return this.state[key]; }
+
+    reload_ () {
+        // debugger;
+        const s = this.state;
+        // this.ss.replace(`
+        //     .taskbar, .window-head, .window-sidebar {
+        //         background-color: hsla(${s.hue}, ${s.sat}%, ${s.lig}%, ${s.alpha});
+        //     }
+        // `)
+        // this.root.style.setProperty('--puter-window-background', `hsla(${s.hue}, ${s.sat}%, ${s.lig}%, ${s.alpha})`);
+        this.root.style.setProperty('--primary-hue', s.hue);
+        this.root.style.setProperty('--primary-saturation', s.sat + '%');
+        this.root.style.setProperty('--primary-lightness', s.lig + '%');
+        this.root.style.setProperty('--primary-alpha', s.alpha);
+    }
+
+    save_ () {
+        if ( this.save_cooldown_ ) {
+            clearTimeout(this.save_cooldown_);
+        }
+        this.save_cooldown_ = setTimeout(() => {
+            this.commit_save_();
+        }, SAVE_COOLDOWN_TIME);
+    }
+    commit_save_ () {
+        puter.fs.write(PUTER_THEME_DATA_FILENAME, JSON.stringify(
+            { colors: this.state },
+            undefined,
+            4,
+        ));
+    }
+}