feat: added support for theming

re #155
This commit is contained in:
ThinLiquid 2024-01-23 03:48:34 +00:00
parent d610c8a471
commit 11540b945a
No known key found for this signature in database
GPG key ID: 17538DC3DF6A7387
8 changed files with 132 additions and 202 deletions

View file

@ -115,7 +115,8 @@ window-area {
border-radius: 5px; border-radius: 5px;
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2);
overflow: hidden; overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid;
border-color: color-mix(in srgb, var(--text) 20%, transparent);
background: var(--crust); background: var(--crust);
transition: 0.2s opacity, 0.2s transform; transition: 0.2s opacity, 0.2s transform;
@ -154,7 +155,7 @@ launcher {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
top: 0; top: 0;
background: rgba(0, 0, 0, 0.5); background: color-mix(in srgb, var(--crust) 20%, transparent);
z-index: 99999999999999999999999; z-index: 99999999999999999999999;
width: calc(100vw + 20px); width: calc(100vw + 20px);
height: calc(100vh + 20px); height: calc(100vh + 20px);

View file

@ -57,6 +57,15 @@ export default class Kernel {
this.version = pkg.version this.version = pkg.version
} }
private async setTheme (themeName: string): Promise<void> {
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())
for (const color in colors) {
document.documentElement.style.setProperty(`--${color}`, colors[color])
}
}
async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise<void> { async boot (boot: HTML, progress: HTML, args: URLSearchParams): Promise<void> {
progress.style({ width: '0%' }) progress.style({ width: '0%' })
const bootArgs = args.toString().replace(/=($|&)/g, '=true ') const bootArgs = args.toString().replace(/=($|&)/g, '=true ')
@ -79,6 +88,11 @@ export default class Kernel {
}) })
if (this.config === false) return if (this.config === false) return
else progress.style({ width: '40%' }) else progress.style({ width: '40%' })
await this.setTheme(this.config.THEME)
document.addEventListener('theme_update', () => {
if (this.config === false) return
this.setTheme(this.config.THEME).catch(e => console.error(e))
})
const tmp = await handle('mount', 'Temporary Directory (/tmp)', { const tmp = await handle('mount', 'Temporary Directory (/tmp)', {
init: async () => { init: async () => {
if (this.fs === false) return false if (this.fs === false) return false

View file

@ -1,171 +0,0 @@
import HTML from '../HTML'
import { AppClosedEvent, AppOpenedEvent, Process } from '../types'
import { getTime } from '../utils'
import nullIcon from '../assets/icons/application-default-icon.svg'
import { parse } from 'js-ini'
import { v4 as uuid } from 'uuid'
const BootLoader: Process = {
config: {
name: 'Bootloader',
type: 'process',
targetVer: '1.0.0-indev.0'
},
run: async (process) => {
const splashScreen = await process.loadLibrary('lib/SplashScreen')
const splashElement = splashScreen.getElement()
splashElement.appendTo(document.body)
const { fs } = process
const wm = await process.loadLibrary('lib/WindowManager')
const launcher = await process.loadLibrary('lib/Launcher')
const config = Buffer.from(await fs.readFile('/etc/flow')).toString()
process.kernel.setConfig(parse(config))
if ('serviceWorker' in navigator) {
const registrations = await navigator.serviceWorker.getRegistrations()
for (const registration of registrations) {
await registration.unregister()
}
try {
await navigator.serviceWorker.register(`/uv-sw.js?url=${encodeURIComponent(btoa(process.kernel.config.SERVER))}&e=${uuid()}`, {
scope: '/service/'
})
} catch (e) {
console.error(e)
}
} else {
console.warn('Service workers are not supported.')
}
const input = new HTML('input').attr({
type: 'text',
placeholder: 'Search'
}).on('keyup', () => {
apps.elm.innerHTML = ''
renderApps().catch(e => console.error(e))
}).appendTo(launcher.element)
const apps = new HTML('apps').appendTo(launcher.element)
const renderApps = async (): Promise<void> => {
apps.html('')
const files = await fs.readdir('/home/Applications/')
files
.filter((x: string) => x.endsWith('.app') && ((input.elm as HTMLInputElement) !== null ? x.toLowerCase().includes((input.elm as HTMLInputElement).value.toLowerCase()) : true))
.forEach((file: string) => {
fs.readFile(`/home/Applications/${file}`).then(async (data: Uint8Array) => {
const path = Buffer.from(data).toString()
const executable = await process.kernel.getExecutable(path) as Process
const appElement = new HTML('app').on('click', () => {
process.launch(path).catch((e: any) => console.error(e))
launcher.toggle()
}).appendTo(apps)
new HTML('img').attr({
src: executable.config.icon ?? nullIcon,
alt: `${executable.config.name} icon`
}).appendTo(appElement)
new HTML('div').text(executable.config.name).appendTo(appElement)
}).catch((e: any) => console.error(e))
})
}
await renderApps()
document.addEventListener('fs_update', () => {
renderApps().catch(e => console.error(e))
})
launcher.element.on('click', (e: Event) => {
if (e.target !== e.currentTarget) return
launcher.toggle()
})
const statusBar = await process.loadLibrary('lib/StatusBar')
statusBar.element.html(`
<div class="outlined" data-toolbar-id="start"><span class="material-symbols-rounded">space_dashboard</span></div>
<div data-toolbar-id="apps"></div>
<flex></flex>
<div class="outlined" data-toolbar-id="plugins"><span class="material-symbols-rounded">expand_less</span></div>
<div class="outlined" data-toolbar-id="controls">
<span class="material-symbols-rounded battery">battery_2_bar</span>
<span class="material-symbols-rounded signal">signal_cellular_4_bar</span>
</div>
<div class="outlined" data-toolbar-id="calendar"></div>
`)
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<void> {
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)
await launcher.element.appendTo(document.body)
await wm.windowArea.appendTo(document.body)
splashElement.cleanup()
}
}
export default BootLoader

View file

@ -1,3 +1,4 @@
import path from 'path'
import { Directory, Errors, File, Permission, Stats } from '../types' import { Directory, Errors, File, Permission, Stats } from '../types'
export const defaultFS: { root: Directory } = { export const defaultFS: { root: Directory } = {
@ -176,13 +177,38 @@ export const defaultFS: { root: Directory } = {
deleteable: false, deleteable: false,
permission: Permission.SYSTEM, permission: Permission.SYSTEM,
children: { children: {
themes: {
type: 'directory',
deleteable: false,
permission: Permission.SYSTEM,
children: {
'Mocha.theme': {
type: 'file',
deleteable: true,
permission: Permission.USER,
content: Buffer.from(JSON.stringify({
name: 'Catpuccin Mocha',
colors: {
text: '#cdd6f4',
'surface-2': '#585b70',
'surface-1': '#45475a',
'surface-0': '#313244',
base: '#1e1e2e',
mantle: '#181825',
crust: '#11111b'
}
}))
}
}
},
flow: { flow: {
type: 'file', type: 'file',
deleteable: false, deleteable: false,
permission: Permission.ELEVATED, permission: Permission.ELEVATED,
content: Buffer.from([ content: Buffer.from([
'SERVER=https://server.flow-works.me', 'SERVER=https://server.flow-works.me',
'24HOUR=FALSE' '24HOUR=FALSE',
'THEME=Mocha'
].join('\n')) ].join('\n'))
}, },
hostname: { hostname: {
@ -211,42 +237,63 @@ export const defaultFS: { root: Directory } = {
} }
class VirtualFS { class VirtualFS {
private fileSystem: { root: Directory } = defaultFS private fileSystem: { root: Directory }
private db: IDBDatabase | null = null private db: IDBDatabase | null = null
private readonly addMissingFiles = async (): Promise<void> => {
const addMissingFiles = async (current: Directory, currentPath: string): Promise<void> => {
for (const child in current.children) {
const childPath = path.join(currentPath, child)
if (current.children[child].type === 'directory') {
if (!await this.exists(childPath)) {
await this.mkdir(childPath)
}
await addMissingFiles(current.children[child] as Directory, childPath)
} else if (current.children[child].type === 'file' && !await this.exists(childPath)) {
await this.writeFile(childPath, (current.children[child] as File).content)
}
}
}
await addMissingFiles(defaultFS.root, '')
}
async init (dbName = 'virtualfs'): Promise<VirtualFS> { async init (dbName = 'virtualfs'): Promise<VirtualFS> {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
indexedDB.deleteDatabase(dbName)
const request = indexedDB.open(dbName) const request = indexedDB.open(dbName)
request.onupgradeneeded = (event) => {
const target = event.target as IDBRequest
const db = target.result
db.createObjectStore('fs')
}
request.onerror = () => { request.onerror = () => {
reject(new Error('Failed to open database')) reject(new Error('Failed to open database'))
} }
request.onsuccess = () => { request.onsuccess = async (event) => {
this.db = request.result const target = event.target as IDBRequest
this.db = target.result
await navigator.storage.persist()
this.fileSystem = await this.read()
if (this.fileSystem == null) await this.write(defaultFS)
else await this.addMissingFiles()
console.log(this.fileSystem)
resolve(this) resolve(this)
} }
request.onupgradeneeded = () => {
const db = request.result
db.createObjectStore('fs')
}
}) })
} }
private setFileSystem (fileSystemObject: { root: Directory }): void { private readonly read = async (): Promise<{ root: Directory }> => {
this.fileSystem = fileSystemObject if (this.db == null) throw new Error('Database is null')
} const transaction = this.db.transaction(['fs'], 'readonly')
const store = transaction.objectStore('fs')
private readonly read = async (): Promise<any> => { const getRequest = store.get('fs')
const transaction = this.db?.transaction(['fs'], 'readonly')
const store = transaction?.objectStore('fs')
const getRequest = store?.get('fs')
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
if (getRequest == null) return getRequest.onsuccess = (event) => {
getRequest.onsuccess = () => { const target = event.target as IDBRequest
resolve(getRequest.result) resolve(target.result)
} }
getRequest.onerror = () => { getRequest.onerror = (event) => {
reject(getRequest.error) reject(getRequest.error)
} }
}) })
@ -258,12 +305,12 @@ class VirtualFS {
} }
private readonly save = async (): Promise<void> => { private readonly save = async (): Promise<void> => {
const transaction = this.db?.transaction(['fs'], 'readwrite') if (this.db == null) throw new Error('Database is null')
const store = transaction?.objectStore('fs') const transaction = this.db.transaction(['fs'], 'readwrite')
const putRequest = store?.put(this.fileSystem, 'fs') const store = transaction.objectStore('fs')
const putRequest = store.put(this.fileSystem, 'fs')
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
if (putRequest == null) return
putRequest.onsuccess = () => { putRequest.onsuccess = () => {
document.dispatchEvent(new CustomEvent('fs_update', {})) document.dispatchEvent(new CustomEvent('fs_update', {}))
resolve() resolve()
@ -371,8 +418,8 @@ class VirtualFS {
rmdir = async (path: string): Promise<void> => { rmdir = async (path: string): Promise<void> => {
const { current, filename } = await this.navigatePathParent(path) const { current, filename } = await this.navigatePathParent(path)
if (!current.deleteable) throw new Error(Errors.EPERM) if (!current.deleteable && path !== '/tmp') throw new Error(Errors.EPERM)
await this.handlePermissions(path) if (path !== '/tmp') await this.handlePermissions(path)
if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR) if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR)

View file

@ -74,6 +74,7 @@ const Files: Process = {
element.innerHTML += `${icon} <span style="flex:1;">${file}</span><span class="material-symbols-rounded delete">delete_forever</span><span class="material-symbols-rounded rename">edit</span>`; element.innerHTML += `${icon} <span style="flex:1;">${file}</span><span class="material-symbols-rounded delete">delete_forever</span><span class="material-symbols-rounded rename">edit</span>`;
(element.querySelector('.rename') as HTMLElement).onclick = async () => { (element.querySelector('.rename') as HTMLElement).onclick = async () => {
const value = prompt('Rename') const value = prompt('Rename')
console.log(value)
if (value != null) { if (value != null) {
await fs.rename(dir + seperator + file, dir + seperator + value) await fs.rename(dir + seperator + file, dir + seperator + value)
} }

View file

@ -27,12 +27,24 @@ const Settings: Process = {
const { fs } = process const { fs } = process
const HTML = await process.loadLibrary('lib/HTML') const HTML = await process.loadLibrary('lib/HTML')
const { Input, Button } = await process.loadLibrary('lib/Components') const { Input, Button, Dropdown } = await process.loadLibrary('lib/Components')
const render = async (config: any): Promise<void> => { const render = async (config: any): Promise<void> => {
win.content.innerHTML = '' win.content.innerHTML = ''
for (const item in config) { for (const item in config) {
const input = Input.new().attr({ console.log(config[item])
const input = item === 'THEME'
? Dropdown.new((await fs.readdir('/etc/themes')).map((theme: string) => theme.replace('.theme', '')))
: 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
}
input.attr({
value: config[item] value: config[item]
}) })
new HTML('div') new HTML('div')
@ -62,6 +74,9 @@ const Settings: Process = {
} }
}) })
) )
if (item === 'THEME') {
document.dispatchEvent(new CustomEvent('theme_update', {}))
}
}) })
.catch(e => console.error(e)) .catch(e => console.error(e))
}) })

View file

@ -48,6 +48,23 @@ const Components: Library = {
'font-size': size 'font-size': size
}) })
} }
},
Dropdown: {
new: (options: string[]) => {
const { HTML } = library
const dropdown = new HTML('select')
dropdown.style({
'border-radius': '5px',
padding: '2.5px',
background: 'var(--base)',
border: '1px solid var(--surface-1)'
}).appendMany(
...options.map((option) => {
return new HTML('option').text(option)
})
)
return dropdown
}
} }
} }
} }

View file

@ -74,6 +74,12 @@ const MIMETypes: Library = {
opensWith: ['apps/ImageViewer'], opensWith: ['apps/ImageViewer'],
icon: 'image' icon: 'image'
}, },
theme: {
type: 'application/x-flow-theme',
description: 'FlowOS Theme',
opensWith: ['apps/Editor'],
icon: 'palette'
},
txt: { txt: {
type: 'text/plain', type: 'text/plain',
description: 'Text Document', description: 'Text Document',