diff --git a/src/assets/style.less b/src/assets/style.less index 322e516..1f7ad2e 100644 --- a/src/assets/style.less +++ b/src/assets/style.less @@ -1,6 +1,7 @@ @import url(https://api.fontshare.com/v2/css?f[]=satoshi@1,2&display=swap); :root { + --primary: #89b4fa; --text: #cdd6f4; --surface-2: #585b70; --surface-1: #45475a; @@ -59,12 +60,30 @@ toolbar { background: var(--mantle); margin: 0; padding: 10px; + border-radius: 20px 20px 0 0; + border: 1px solid var(--surface-0); + box-shadow: 1px 3px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2); + user-select: none; & > div { &.outlined { padding: 10px; background: var(--base); border-radius: 10px; + border: 1px solid var(--surface-0); + box-shadow: 1px 3px 10px rgba(0, 0, 0, 0.1), 0 0px 10px rgba(0, 0, 0, 0.1); + align-items: center; + justify-content: center; + } + + &[data-toolbar-id="start"] { + cursor: pointer; + & * { + transition: color .1s ease-in-out; + &:hover { + color: var(--primary); + } + } } &[data-toolbar-id="controls"] { @@ -76,6 +95,7 @@ toolbar { &[data-toolbar-id="plugins"] { width: 24px; + cursor: pointer; padding: 10px 0; } @@ -164,6 +184,12 @@ launcher { margin: 0; left: 0; + &[style*="opacity: 1;"] { + apps * { + pointer-events: all; + } + } + apps { max-height: 70vh; padding: 20px; @@ -172,6 +198,8 @@ launcher { display: flex; flex-wrap: wrap; gap: 40px; + overflow: scroll; + pointer-events: none; app { flex: 1 0 21%; @@ -184,7 +212,22 @@ launcher { min-width: 125px; max-width: 125px; text-align: center; - overflow: hidden; + overflow: visible; + transition: transform 0.1s ease-in-out; + cursor: pointer; + + + &:hover { + transform: scale(1.1); + + div { + background: var(--surface-0); + } + + img { + filter: drop-shadow(0px 2px 10px rgba(0, 0, 0, 0.75)); + } + } img { width: 100%; @@ -198,7 +241,9 @@ launcher { width: fit-content; height: 100%; white-space: nowrap; - + padding: 5px 10px; + border-radius: 5px; + transition: background 0.1s ease-in-out; } } } diff --git a/src/bootloader.ts b/src/bootloader.ts index 6704eab..e249299 100644 --- a/src/bootloader.ts +++ b/src/bootloader.ts @@ -124,5 +124,5 @@ try { writeln() console.error(e.stack) writeln() - terminal.html(terminal.getHTML() + 'Would you like to reset the VirtualFS?') + terminal.html(terminal.getHTML() + 'Would you like to reset the VirtualFS?') } diff --git a/src/kernel.ts b/src/kernel.ts index 0d25438..80ce494 100644 --- a/src/kernel.ts +++ b/src/kernel.ts @@ -60,10 +60,11 @@ export default class Kernel { private async setTheme (themeName: string): Promise { if (this.fs === false) throw new Error('Filesystem hasn\'t been initiated.') const file = await this.fs.readFile(`/etc/themes/${themeName}.theme`) - const { colors } = JSON.parse(Buffer.from(file).toString()) + const { extras, colors } = JSON.parse(Buffer.from(file).toString()) for (const color in colors) { document.documentElement.style.setProperty(`--${color}`, colors[color]) } + document.documentElement.style.setProperty('--primary', extras[parse(Buffer.from(await this.fs.readFile('/etc/flow')).toString()).THEME_PRIMARY as string]) } async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise { diff --git a/src/system/Desktop.ts b/src/system/Desktop.ts index fb842a2..7f60154 100644 --- a/src/system/Desktop.ts +++ b/src/system/Desktop.ts @@ -1,6 +1,5 @@ import HTML from '../HTML' -import { AppClosedEvent, AppOpenedEvent, Process } from '../types' -import { getTime } from '../utils' +import { Process } from '../types' import nullIcon from '../assets/icons/application-default-icon.svg' const BootLoader: Process = { @@ -62,80 +61,10 @@ const BootLoader: Process = { const statusBar = await process.loadLibrary('lib/StatusBar') - statusBar.element.html(` -
space_dashboard
- -
- -
expand_less
-
- battery_2_bar - signal_cellular_4_bar -
-
- - `) - - setInterval((): any => { - getTime().then((time) => { - statusBar.element.qs('div[data-toolbar-id="calendar"]')?.text(time) - }).catch(e => console.error) - }, 1000) - statusBar.element.qs('div[data-toolbar-id="start"]')?.on('click', () => { launcher.toggle() }) - if ('getBattery' in navigator) { - (navigator as any).getBattery().then((battery: any) => { - statusBar.updateBatteryIcon(battery) - - battery.addEventListener('levelchange', () => { - statusBar.updateBatteryIcon(battery) - }) - - battery.addEventListener('chargingchange', () => { - statusBar.updateBatteryIcon(battery) - }) - }) - } else { - const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery') - if (batteryDiv != null) { - batteryDiv.innerHTML = 'battery_unknown' - } - } - - async function ping (startTime: number): Promise { - fetch(`${process.kernel.config.SERVER as string}/bare/`) - .then(() => { - const endTime = performance.now() - const pingTime = endTime - startTime - statusBar.updateIcon(pingTime) - }) - .catch(() => { - (document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = 'signal_cellular_connected_no_internet_4_bar' - }) - } - - setInterval((): any => ping(performance.now()), 10_000) - - document.addEventListener('app_opened', (e: AppOpenedEvent): void => { - new HTML('app').appendMany( - new HTML('img').attr({ - alt: `${e.detail.proc.config.name} icon`, - 'data-id': e.detail.token, - src: e.detail.proc.config.icon ?? nullIcon - }).on('click', () => { - e.detail.win.focus() - e.detail.win.toggleMin() - }) - ).appendTo(statusBar.element.qs('div[data-toolbar-id="apps"]')?.elm as HTMLElement) - }) - - document.addEventListener('app_closed', (e: AppClosedEvent): void => { - statusBar.element.qs('div[data-toolbar-id="apps"]')?.qs(`img[data-id="${e.detail.token}"]`)?.elm.parentElement?.remove() - }) - document.body.style.flexDirection = 'column-reverse' await statusBar.element.appendTo(document.body) diff --git a/src/system/VirtualFS.ts b/src/system/VirtualFS.ts index 373f81d..2344b20 100644 --- a/src/system/VirtualFS.ts +++ b/src/system/VirtualFS.ts @@ -1,3 +1,4 @@ +import { parse, stringify } from 'js-ini' import { Directory, Errors, File, Permission, Stats } from '../types' import path from 'path' const p = path @@ -159,12 +160,127 @@ export const defaultFS: { root: Directory } = { deleteable: false, permission: Permission.SYSTEM, children: { + 'Latte.theme': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from(JSON.stringify({ + name: 'Catppuccin Latté', + extras: { + rosewater: '#dc8a78', + flamingo: '#dd7878', + pink: '#ea76cb', + mauve: '#8839ef', + red: '#d20f39', + maroon: '#e64553', + peach: '#fe640b', + yellow: '#df8e1d', + green: '#40a02b', + teal: '#179299', + sky: '#04a5e5', + sapphire: '#209fb5', + blue: '#1e66f5', + lavender: '#7287fd' + }, + colors: { + text: '#4c4f69', + 'surface-2': '#acb0be', + 'surface-1': '#bcc0cc', + 'surface-0': '#ccd0da', + base: '#eff1f5', + mantle: '#e6e9ef', + crust: '#dce0e8' + } + })) + }, + 'Frappe.theme': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from(JSON.stringify({ + name: 'Catppuccin Frappé', + extras: { + rosewater: '#f2d5cf', + flamingo: '#eebebe', + pink: '#f4b8e4', + mauve: '#ca9ee6', + red: '#e78284', + maroon: '#ea999c', + peach: '#ef9f76', + yellow: '#e5c890', + green: '#a6d189', + teal: '#81c8be', + sky: '#99d1db', + sapphire: '#85c1dc', + blue: '#8caaee', + lavender: '#babbf1' + }, + colors: { + text: '#c6d0f5', + 'surface-2': '#626880', + 'surface-1': '#51576d', + 'surface-0': '#414559', + base: '#303446', + mantle: '#292c3c', + crust: '#232634' + } + })) + }, + 'Macchiato.theme': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from(JSON.stringify({ + name: 'Catppuccin Macchiato', + extras: { + rosewater: '#f4dbd6', + flamingo: '#f0c6c6', + pink: '#f5bde6', + mauve: '#c6a0f6', + red: '#ed8796', + maroon: '#ee99a0', + peach: '#f5a97f', + yellow: '#eed49f', + green: '#a6da95', + teal: '#8bd5ca', + sky: '#91d7e3', + sapphire: '#7dc4e4', + blue: '#8aadf4', + lavender: '#b7bdf8' + }, + colors: { + text: '#cad3f5', + 'surface-2': '#5b6078', + 'surface-1': '#494d64', + 'surface-0': '#363a4f', + base: '#24273a', + mantle: '#1e2030', + crust: '#181926' + } + })) + }, 'Mocha.theme': { type: 'file', deleteable: true, permission: Permission.USER, content: Buffer.from(JSON.stringify({ name: 'Catpuccin Mocha', + extras: { + rosewater: '#f5e0dc', + flamingo: '#f2cdcd', + pink: '#f5c2e7', + mauve: '#cba6f7', + red: '#f38ba8', + maroon: '#eba0ac', + peach: '#fab387', + yellow: '#f9e2af', + green: '#a6e3a1', + teal: '#94e2d5', + sky: '#89dceb', + sapphire: '#74c7ec', + blue: '#89b4fa', + lavender: '#b4befe' + }, colors: { text: '#cdd6f4', 'surface-2': '#585b70', @@ -175,6 +291,81 @@ export const defaultFS: { root: Directory } = { crust: '#11111b' } })) + }, + 'RosePine.theme': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from(JSON.stringify({ + name: 'Rosé Pine', + extras: { + love: '#eb6f92', + gold: '#f6c177', + rose: '#ebbcba', + pine: '#31748f', + foam: '#9ccfd8', + iris: '#c4a7e7' + }, + colors: { + text: '#e0def4', + 'surface-2': '#524f67', + 'surface-1': '#403d52', + 'surface-0': '#21202e', + base: '#26233a', + mantle: '#1f1d2e', + crust: '#191724' + } + })) + }, + 'RosePineMoon.theme': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from(JSON.stringify({ + name: 'Rosé Pine Moon', + extras: { + love: '#eb6f92', + gold: '#f6c177', + rose: '#ebbcba', + pine: '#31748f', + foam: '#9ccfd8', + iris: '#c4a7e7' + }, + colors: { + text: '#e0def4', + 'surface-2': '#2a283e', + 'surface-1': '#44415a', + 'surface-0': '#56526e', + base: '#393552', + mantle: '#2a273f', + crust: '#232136' + } + })) + }, + 'RosePineDawn.theme': { + type: 'file', + deleteable: true, + permission: Permission.USER, + content: Buffer.from(JSON.stringify({ + name: 'Rosé Pine Dawn', + extras: { + love: '#b4637a', + gold: '#ea9d34', + rose: '#d7827e', + pine: '#286983', + foam: '#56949f', + iris: '#907aa9' + }, + colors: { + text: '#575279', + 'surface-2': '#cecacd', + 'surface-1': '#dfdad9', + 'surface-0': '#f4ede8', + base: '#f2e9e1', + mantle: '#fffaf3', + crust: '#faf4ed' + } + })) } } }, @@ -184,8 +375,9 @@ export const defaultFS: { root: Directory } = { permission: Permission.ELEVATED, content: Buffer.from([ 'SERVER=https://server.flow-works.me', - '24HOUR=FALSE', - 'THEME=Mocha' + '24_HOUR=false', + 'THEME=Mocha', + 'THEME_PRIMARY=blue' ].join('\n')) }, hostname: { @@ -221,6 +413,10 @@ class VirtualFS { const addDirectoryRecursive = async (directory: Directory, directoryPath: string): Promise => { for (const [key, value] of Object.entries(directory.children)) { const path = p.join(directoryPath, key) + if (value.type === 'file' && path.startsWith('/etc/themes/')) { + await this.writeFile(path, Buffer.from(value.content).toString()) + return + } if (value.type === 'directory') { if (!await this.exists(path)) { await this.mkdir(path) @@ -232,6 +428,28 @@ class VirtualFS { } } await addDirectoryRecursive(defaultFS.root, '/') + + await this.readFile('/etc/flow').then(async (data: Uint8Array) => { + const dataString = Buffer.from(data).toString() + const config = parse(dataString) + + if (config.SERVER == null) { + config.SERVER = 'https://server.flow-works.me' + await this.writeFile('/etc/flow', stringify(config)) + } + if (config['24_HOUR'] == null) { + config['24_HOUR'] = 'FALSE' + await this.writeFile('/etc/flow', stringify(config)) + } + if (config.THEME == null) { + config.THEME = 'Mocha' + await this.writeFile('/etc/flow', stringify(config)) + } + if (config.THEME_PRIMARY == null) { + config.THEME_PRIMARY = 'blue' + await this.writeFile('/etc/flow', stringify(config)) + } + }) } async init (dbName = 'virtualfs'): Promise { diff --git a/src/system/apps/Settings.ts b/src/system/apps/Settings.ts index a32a947..17c54d3 100644 --- a/src/system/apps/Settings.ts +++ b/src/system/apps/Settings.ts @@ -7,7 +7,7 @@ const Settings: Process = { name: 'Settings', type: 'process', icon, - targetVer: '1.0.0-indev.0' + targetVer: '2.0.0' }, run: async process => { const win = await process @@ -32,21 +32,25 @@ const Settings: Process = { const render = async (config: any): Promise => { win.content.innerHTML = '' for (const item in config) { - console.log(config[item]) - const input = item === 'THEME' - ? Dropdown.new((await fs.readdir('/etc/themes')).map((theme: string) => theme.replace('.theme', ''))) - : Input.new() + let input = Input.new() - if (item === 'THEME') { - const text = config[item] - const $select = input.elm as HTMLSelectElement - const $options = Array.from($select.options) - const optionToSelect = $options.find(item => item.text === text) - if (optionToSelect != null) optionToSelect.selected = true + if (item === 'THEME_PRIMARY') { + const { extras } = JSON.parse(Buffer.from(await fs.readFile(`/etc/themes/${config.THEME as string}.theme`)).toString()) + input = Dropdown.new(Object.keys(extras)) + } else if (item === 'THEME') { + input = Dropdown.new((await fs.readdir('/etc/themes')).map((theme: string) => theme.replace('.theme', ''))) } - input.attr({ - value: config[item] - }) + + if (item === 'THEME_PRIMARY' || item === 'THEME') { + (input.elm as HTMLSelectElement).value = config[item] + } else { + input.attr({ + value: config[item] + }) + } + + console.log(input.getValue()) + new HTML('div') .appendMany( new HTML('label') @@ -64,7 +68,6 @@ const Settings: Process = { input, Button.new().text('Save').on('click', () => { config[item] = input.getValue() - process.kernel.setConfig(config) fs.writeFile('/etc/flow', stringify(config)) .then(() => { document.dispatchEvent( @@ -74,7 +77,7 @@ const Settings: Process = { } }) ) - if (item === 'THEME') { + if (item === 'THEME' || item === 'THEME_PRIMARY') { document.dispatchEvent(new CustomEvent('theme_update', {})) } }) diff --git a/src/system/lib/Components.ts b/src/system/lib/Components.ts index ce2f182..aeba3a1 100644 --- a/src/system/lib/Components.ts +++ b/src/system/lib/Components.ts @@ -20,21 +20,41 @@ const Components: Library = { padding: '2.5px', outline: 'none', background: 'transparent', - border: '1px solid var(--surface-0)' + border: '1px solid const(--surface-0)' }) return input } }, Button: { - new: () => { + new: (type: 'normal' | 'primary' = 'normal') => { + function shiftColor (col: string, amt: number): string { + const num = parseInt(col, 16) + const r = (num >> 16) + amt + const b = ((num >> 8) & 0x00FF) + amt + const g = (num & 0x0000FF) + amt + const newColor = g | (b << 8) | (r << 16) + return newColor.toString(16) + } + const { HTML } = library const button = new HTML('button') button.style({ 'border-radius': '5px', padding: '2.5px 5px', background: 'var(--base)', - border: '1px solid var(--surface-1)' + border: '1px solid var(--surface-0)' }) + if (type === 'normal') { + button.style({ + background: 'var(--base)' + }) + } else if (type === 'primary') { + button.style({ + background: 'var(--primary)', + color: 'var(--base)', + border: '1px solid #' + shiftColor(document.documentElement.style.getPropertyValue('--primary').replace('#', ''), -40) + '' + }) + } return button } }, @@ -57,7 +77,7 @@ const Components: Library = { 'border-radius': '5px', padding: '2.5px', background: 'var(--base)', - border: '1px solid var(--surface-1)' + border: '1px solid const(--surface-1)' }).appendMany( ...options.map((option) => { return new HTML('option').text(option) diff --git a/src/system/lib/Launcher.ts b/src/system/lib/Launcher.ts index 301fd2c..078e0a2 100644 --- a/src/system/lib/Launcher.ts +++ b/src/system/lib/Launcher.ts @@ -26,7 +26,7 @@ const Launcher: Library = { } else { Launcher.data.element.style({ opacity: '1', - 'backdrop-filter': 'blur(20px)', + 'backdrop-filter': 'blur(10px)', 'pointer-events': 'all' }) } diff --git a/src/system/lib/StatusBar.ts b/src/system/lib/StatusBar.ts index db2198e..0cd57a9 100644 --- a/src/system/lib/StatusBar.ts +++ b/src/system/lib/StatusBar.ts @@ -1,4 +1,6 @@ -import { Library } from '../../types' +import { AppClosedEvent, AppOpenedEvent, Library } from '../../types' +import nullIcon from '../../assets/icons/application-default-icon.svg' +import { getTime } from '../../utils' const StatusBar: Library = { config: { @@ -8,6 +10,75 @@ const StatusBar: Library = { }, init: (l, k, p) => { StatusBar.data.element = new l.HTML('toolbar') + StatusBar.data.element.html(` +
space_dashboard
+ +
+ +
expand_less
+
+ battery_2_bar + signal_cellular_4_bar +
+
+ + `) + + setInterval((): any => { + getTime().then((time) => { + StatusBar.data.element.qs('div[data-toolbar-id="calendar"]')?.text(time) + }).catch(e => console.error) + }, 1000) + + if ('getBattery' in navigator) { + (navigator as any).getBattery().then((battery: any) => { + StatusBar.data.updateBatteryIcon(battery) + + battery.addEventListener('levelchange', () => { + StatusBar.data.updateBatteryIcon(battery) + }) + + battery.addEventListener('chargingchange', () => { + StatusBar.data.updateBatteryIcon(battery) + }) + }) + } else { + const batteryDiv = document.querySelector('div[data-toolbar-id="controls"] > .battery') + if (batteryDiv != null) { + batteryDiv.innerHTML = 'battery_unknown' + } + } + + async function ping (startTime: number): Promise { + fetch(`${(k.config as any).SERVER as string}/bare/`) + .then(() => { + const endTime = performance.now() + const pingTime = endTime - startTime + StatusBar.data.updateIcon(pingTime) + }) + .catch(() => { + (document.querySelector('div[data-toolbar-id="controls"] > .signal') as HTMLElement).innerHTML = 'signal_cellular_connected_no_internet_4_bar' + }) + } + + setInterval((): any => ping(performance.now()), 10_000) + + document.addEventListener('app_opened', (e: AppOpenedEvent): void => { + new l.HTML('app').appendMany( + new l.HTML('img').attr({ + alt: `${e.detail.proc.config.name} icon`, + 'data-id': e.detail.token, + src: e.detail.proc.config.icon ?? nullIcon + }).on('click', () => { + e.detail.win.focus() + e.detail.win.toggleMin() + }) + ).appendTo(StatusBar.data.element.qs('div[data-toolbar-id="apps"]')?.elm as HTMLElement) + }) + + document.addEventListener('app_closed', (e: AppClosedEvent): void => { + StatusBar.data.element.qs('div[data-toolbar-id="apps"]')?.qs(`img[data-id="${e.detail.token}"]`)?.elm.parentElement?.remove() + }) }, data: { updateBatteryIcon (battery: any) { diff --git a/src/system/lib/WindowManager.ts b/src/system/lib/WindowManager.ts index 9d424d3..0bf4b9e 100644 --- a/src/system/lib/WindowManager.ts +++ b/src/system/lib/WindowManager.ts @@ -2,6 +2,7 @@ import HTML from '../../HTML' import FlowWindow from '../../structures/FlowWindow' import ProcessLib from '../../structures/ProcessLib' import { FlowWindowConfig, Library } from '../../types' +import nullIcon from '../../assets/icons/application-default-icon.svg' const WindowManager: Library = { config: { @@ -50,8 +51,8 @@ const WindowManager: Library = { const win = new FlowWindow(process, WindowManager.data, { title, icon: '', - width: 300, - height: 200, + width: 350, + height: 150, canResize: false }) const appOpenedEvent = { @@ -66,22 +67,50 @@ const WindowManager: Library = { return { value: await new Promise((resolve) => { - new HTML('h3').text(title).style({ margin: '0' }).appendTo(win.content) - new HTML('p').text(text).style({ margin: '0' }).appendTo(win.content) + win.content.style.padding = '10px' + win.content.style.display = 'flex' + win.content.style.flexDirection = 'column' + win.content.style.gap = '10px' + + const container = new HTML('div').style({ + display: 'flex', + gap: '10px', + justifyContent: 'center', + height: 'max-content' + }).appendTo(win.content) + + new HTML('img').attr({ src: process.process.config.icon ?? nullIcon }).style({ + height: '100%', + 'aspect-ratio': '1 / 1', + borderRadius: '50%', + alignSelf: 'center' + }).appendTo(container) + + const space = new HTML('div').style({ + display: 'flex', + gap: '10px', + 'flex-direction': 'column', + 'justify-content': 'center', + height: 'max-content' + }).appendTo(container) + + new HTML('h3').text(title).style({ margin: '0' }).appendTo(space) + new HTML('p').text(text).style({ margin: '0' }).appendTo(space) + const div = new HTML('div').style({ display: 'flex', gap: '10px', alignItems: 'right' }).appendTo(space) if (type === 'allow') document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent)) if (type === 'allow') { - Button.new().text('Allow').appendTo(win.content).on('click', () => { + Button.new('primary').text('Allow').appendTo(div).on('click', () => { resolve(true) win.close() }) - Button.new().text('Deny').appendTo(win.content).on('click', () => { + Button.new().text('Deny').appendTo(div).on('click', () => { resolve(false) win.close() }) } else if (type === 'ok') { - Button.new().text('OK').appendTo(win.content).on('click', () => { + Button.new('primary').text('OK').appendTo(div).on('click', () => { win.close() resolve(true) })