diff --git a/puter-gui.json b/puter-gui.json
index 1d3579bc..af7691d9 100644
--- a/puter-gui.json
+++ b/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",
diff --git a/src/UI/Settings/UIWindowSettings.js b/src/UI/Settings/UIWindowSettings.js
index e155bbd9..6a8560d4 100644
--- a/src/UI/Settings/UIWindowSettings.js
+++ b/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 += `
`;
h += ``;
h += ``;
+ h += ``;
h += ``;
h += ``;
h += ``;
@@ -111,6 +113,18 @@ async function UIWindowSettings(options){
h += ``;
+ // Personalization
+ h += ``;
+ h += `
${i18n('personalization')}
`;
+ // change password button
+ h += `
`;
+ h += `
${i18n('ui_colors')}`;
+ h += `
`;
+ h += ``;
+ h += `
`;
+ h += `
`;
+ h += `
`;
+
// Language
h += ``;
h += `
${i18n('language')}
`;
@@ -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');
diff --git a/src/UI/UIWindowThemeDialog.js b/src/UI/UIWindowThemeDialog.js
new file mode 100644
index 00000000..6ebcf3d9
--- /dev/null
+++ b/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;
diff --git a/src/css/style.css b/src/css/style.css
index 8553d8fe..b1b349ea 100644
--- a/src/css/style.css
+++ b/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;
-}
\ No newline at end of file
+}
+
+.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;
+}
diff --git a/src/css/theme.css b/src/css/theme.css
new file mode 100644
index 00000000..3438204d
--- /dev/null
+++ b/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%)`)
+
+*/
\ No newline at end of file
diff --git a/src/definitions.js b/src/definitions.js
new file mode 100644
index 00000000..8bca70ef
--- /dev/null
+++ b/src/definitions.js
@@ -0,0 +1,3 @@
+export class Service {
+ //
+};
diff --git a/src/i18n/translations/en.js b/src/i18n/translations/en.js
index 5375cde5..3fed3189 100644
--- a/src/i18n/translations/en.js
+++ b/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",
}
};
diff --git a/src/initgui.js b/src/initgui.js
index 3e5f340e..b5d2afb3 100644
--- a/src/initgui.js
+++ b/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() {
diff --git a/src/services/ThemeService.js b/src/services/ThemeService.js
new file mode 100644
index 00000000..57e2a20c
--- /dev/null
+++ b/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,
+ ));
+ }
+}