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 += `
${i18n('about')}
`; h += `
${i18n('usage')}
`; h += `
${i18n('account')}
`; + h += `
${i18n('personalization')}
`; h += `
${i18n('language')}
`; h += `
${i18n('clock')}
`; 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, + )); + } +}