Merge pull request #162 from Flow-Works/dev
[🪢] Merge `dev` into `master`
This commit is contained in:
commit
971bf1ca9a
13 changed files with 312 additions and 268 deletions
|
|
@ -115,7 +115,8 @@ window-area {
|
|||
border-radius: 5px;
|
||||
box-shadow: 0 10px 10px rgba(0, 0, 0, 0.2), 0 0px 10px rgba(0, 0, 0, 0.2);
|
||||
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);
|
||||
transition: 0.2s opacity, 0.2s transform;
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ launcher {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: color-mix(in srgb, var(--crust) 20%, transparent);
|
||||
z-index: 99999999999999999999999;
|
||||
width: calc(100vw + 20px);
|
||||
height: calc(100vh + 20px);
|
||||
|
|
|
|||
|
|
@ -57,6 +57,15 @@ export default class Kernel {
|
|||
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> {
|
||||
progress.style({ width: '0%' })
|
||||
const bootArgs = args.toString().replace(/=($|&)/g, '=true ')
|
||||
|
|
@ -79,6 +88,11 @@ export default class Kernel {
|
|||
})
|
||||
if (this.config === false) return
|
||||
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)', {
|
||||
init: async () => {
|
||||
if (this.fs === false) return false
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -32,7 +32,7 @@ const UserAccessControl: Process = {
|
|||
}
|
||||
}
|
||||
|
||||
wm.createModal('User Account Control', message, process)
|
||||
wm.createModal('allow', 'User Account Control', message, process)
|
||||
.then(async ({ value, win }: {
|
||||
value: boolean
|
||||
win: FlowWindow
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Directory, Errors, File, Permission, Stats } from '../types'
|
||||
import path from 'path'
|
||||
const p = path
|
||||
|
||||
export const defaultFS: { root: Directory } = {
|
||||
root: {
|
||||
|
|
@ -75,6 +77,12 @@ export const defaultFS: { root: Directory } = {
|
|||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('apps/Settings')
|
||||
},
|
||||
'ThemeMaker.app': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('apps/ThemeMaker')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -95,47 +103,17 @@ export const defaultFS: { root: Directory } = {
|
|||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/Info.app')
|
||||
},
|
||||
'Manager.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/Manager.app')
|
||||
},
|
||||
'Store.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/Store.app')
|
||||
},
|
||||
'TaskManager.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/TaskManager.app')
|
||||
},
|
||||
'Browser.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/Browser.app')
|
||||
},
|
||||
'ImageViewer.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/ImageViewer.app')
|
||||
},
|
||||
'Files.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/Files.app')
|
||||
},
|
||||
'Editor.lnk': {
|
||||
type: 'file',
|
||||
deleteable: true,
|
||||
permission: Permission.USER,
|
||||
content: Buffer.from('/home/Applications/Editor.app')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -176,13 +154,38 @@ export const defaultFS: { root: Directory } = {
|
|||
deleteable: false,
|
||||
permission: Permission.SYSTEM,
|
||||
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: {
|
||||
type: 'file',
|
||||
deleteable: false,
|
||||
permission: Permission.ELEVATED,
|
||||
content: Buffer.from([
|
||||
'SERVER=https://server.flow-works.me',
|
||||
'24HOUR=FALSE'
|
||||
'24HOUR=FALSE',
|
||||
'THEME=Mocha'
|
||||
].join('\n'))
|
||||
},
|
||||
hostname: {
|
||||
|
|
@ -211,42 +214,62 @@ export const defaultFS: { root: Directory } = {
|
|||
}
|
||||
|
||||
class VirtualFS {
|
||||
private fileSystem: { root: Directory } = defaultFS
|
||||
private fileSystem: { root: Directory }
|
||||
private db: IDBDatabase | null = null
|
||||
|
||||
private async addMissingFiles (): Promise<void> {
|
||||
const addDirectoryRecursive = async (directory: Directory, directoryPath: string): Promise<void> => {
|
||||
for (const [key, value] of Object.entries(directory.children)) {
|
||||
const path = p.join(directoryPath, key)
|
||||
if (value.type === 'directory') {
|
||||
if (!await this.exists(path)) {
|
||||
await this.mkdir(path)
|
||||
}
|
||||
await addDirectoryRecursive(value, path)
|
||||
} else if (value.type === 'file' && !await this.exists(path)) {
|
||||
await this.writeFile(path, Buffer.from(value.content).toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
await addDirectoryRecursive(defaultFS.root, '/')
|
||||
}
|
||||
|
||||
async init (dbName = 'virtualfs'): Promise<VirtualFS> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
indexedDB.deleteDatabase(dbName)
|
||||
const request = indexedDB.open(dbName)
|
||||
request.onupgradeneeded = (event) => {
|
||||
const target = event.target as IDBRequest
|
||||
const db = target.result
|
||||
db.createObjectStore('fs')
|
||||
}
|
||||
request.onerror = () => {
|
||||
reject(new Error('Failed to open database'))
|
||||
}
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
request.onsuccess = async (event) => {
|
||||
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()
|
||||
resolve(this)
|
||||
}
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
db.createObjectStore('fs')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private setFileSystem (fileSystemObject: { root: Directory }): void {
|
||||
this.fileSystem = fileSystemObject
|
||||
}
|
||||
|
||||
private readonly read = async (): Promise<any> => {
|
||||
const transaction = this.db?.transaction(['fs'], 'readonly')
|
||||
const store = transaction?.objectStore('fs')
|
||||
const getRequest = store?.get('fs')
|
||||
private readonly read = async (): Promise<{ root: Directory }> => {
|
||||
if (this.db == null) throw new Error('Database is null')
|
||||
const transaction = this.db.transaction(['fs'], 'readonly')
|
||||
const store = transaction.objectStore('fs')
|
||||
const getRequest = store.get('fs')
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (getRequest == null) return
|
||||
getRequest.onsuccess = () => {
|
||||
resolve(getRequest.result)
|
||||
getRequest.onsuccess = (event) => {
|
||||
const target = event.target as IDBRequest
|
||||
resolve(target.result)
|
||||
}
|
||||
|
||||
getRequest.onerror = () => {
|
||||
getRequest.onerror = (event) => {
|
||||
reject(getRequest.error)
|
||||
}
|
||||
})
|
||||
|
|
@ -258,12 +281,12 @@ class VirtualFS {
|
|||
}
|
||||
|
||||
private readonly save = async (): Promise<void> => {
|
||||
const transaction = this.db?.transaction(['fs'], 'readwrite')
|
||||
const store = transaction?.objectStore('fs')
|
||||
const putRequest = store?.put(this.fileSystem, 'fs')
|
||||
if (this.db == null) throw new Error('Database is null')
|
||||
const transaction = this.db.transaction(['fs'], 'readwrite')
|
||||
const store = transaction.objectStore('fs')
|
||||
const putRequest = store.put(this.fileSystem, 'fs')
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
if (putRequest == null) return
|
||||
putRequest.onsuccess = () => {
|
||||
document.dispatchEvent(new CustomEvent('fs_update', {}))
|
||||
resolve()
|
||||
|
|
@ -371,8 +394,8 @@ class VirtualFS {
|
|||
rmdir = async (path: string): Promise<void> => {
|
||||
const { current, filename } = await this.navigatePathParent(path)
|
||||
|
||||
if (!current.deleteable) throw new Error(Errors.EPERM)
|
||||
await this.handlePermissions(path)
|
||||
if (!current.deleteable && path !== '/tmp') throw new Error(Errors.EPERM)
|
||||
if (path !== '/tmp') await this.handlePermissions(path)
|
||||
|
||||
if (current.children[filename].type !== 'directory') throw new Error(Errors.ENOTDIR)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,26 +12,6 @@ interface EditorConfig {
|
|||
path: string
|
||||
}
|
||||
|
||||
const fileLanguageMap: {
|
||||
[key: string]: string
|
||||
} = {
|
||||
c: 'clike',
|
||||
cpp: 'clike',
|
||||
java: 'clike',
|
||||
cs: 'clike',
|
||||
ts: 'typescript',
|
||||
js: 'javascript',
|
||||
mjs: 'javascript',
|
||||
cjs: 'javascript',
|
||||
jsx: 'jsx',
|
||||
tsx: 'tsx',
|
||||
html: 'html',
|
||||
md: 'markdown',
|
||||
css: 'css',
|
||||
xml: 'xml',
|
||||
py: 'python'
|
||||
}
|
||||
|
||||
const Editor: Process = {
|
||||
config: {
|
||||
name: 'Editor',
|
||||
|
|
@ -40,6 +20,8 @@ const Editor: Process = {
|
|||
targetVer: '1.0.0-indev.0'
|
||||
},
|
||||
run: async (process) => {
|
||||
const MIMETypes = await process.loadLibrary('lib/MIMETypes')
|
||||
|
||||
if (Object.keys(process.data).length > 0) {
|
||||
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||
return wm.createWindow({
|
||||
|
|
@ -153,8 +135,44 @@ const Editor: Process = {
|
|||
}
|
||||
})
|
||||
|
||||
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
|
||||
const language = fileLanguageMap[fileExtension] ?? 'text'
|
||||
const fileExtension = (data.path.split('.').pop() as string).toLowerCase()
|
||||
console.log('owo ' + fileExtension, MIMETypes)
|
||||
const mime = fileExtension in MIMETypes ? MIMETypes[fileExtension].type : 'text/plain'
|
||||
let language = 'text'
|
||||
|
||||
switch (mime) {
|
||||
case 'text/markdown':
|
||||
language = 'markdown'
|
||||
break
|
||||
case 'text/css':
|
||||
language = 'css'
|
||||
break
|
||||
case 'text/html':
|
||||
language = 'html'
|
||||
break
|
||||
case 'text/javascript':
|
||||
language = 'javascript'
|
||||
break
|
||||
case 'text/jsx':
|
||||
language = 'jsx'
|
||||
break
|
||||
case 'application/x-flow-theme':
|
||||
case 'application/json':
|
||||
language = 'clike'
|
||||
break
|
||||
case 'text/typescript':
|
||||
language = 'typescript'
|
||||
break
|
||||
case 'text/tsx':
|
||||
language = 'tsx'
|
||||
break
|
||||
case 'application/python':
|
||||
language = 'python'
|
||||
break
|
||||
default:
|
||||
language = 'text'
|
||||
break
|
||||
}
|
||||
|
||||
const value = Buffer.from(await fs.readFile(data.path)).toString()
|
||||
const editor = fullEditor(
|
||||
|
|
@ -169,6 +187,7 @@ const Editor: Process = {
|
|||
const style = document.createElement('style')
|
||||
style.textContent = `
|
||||
.prism-code-editor {
|
||||
color: var(--text);
|
||||
border-radius: 10px 10px 0 0;
|
||||
caret-color: var(--text);
|
||||
font-weight: 400;
|
||||
|
|
@ -208,6 +227,9 @@ const Editor: Process = {
|
|||
document.addEventListener('fs_update', () => {
|
||||
render().catch(e => console.error(e))
|
||||
})
|
||||
document.addEventListener('theme_update', () => {
|
||||
render().catch(e => console.error(e))
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.querySelector('.rename') as HTMLElement).onclick = async () => {
|
||||
const value = prompt('Rename')
|
||||
console.log(value)
|
||||
if (value != null) {
|
||||
await fs.rename(dir + seperator + file, dir + seperator + value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,12 +27,24 @@ const Settings: Process = {
|
|||
const { fs } = process
|
||||
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> => {
|
||||
win.content.innerHTML = ''
|
||||
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]
|
||||
})
|
||||
new HTML('div')
|
||||
|
|
@ -62,6 +74,9 @@ const Settings: Process = {
|
|||
}
|
||||
})
|
||||
)
|
||||
if (item === 'THEME') {
|
||||
document.dispatchEvent(new CustomEvent('theme_update', {}))
|
||||
}
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
})
|
||||
|
|
|
|||
77
src/system/apps/ThemeMaker.ts
Normal file
77
src/system/apps/ThemeMaker.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { Process } from '../../types'
|
||||
import icon from '../../assets/icons/theme-config.svg'
|
||||
|
||||
const ThemeConfig: Process = {
|
||||
config: {
|
||||
name: 'Theme Maker',
|
||||
type: 'process',
|
||||
icon,
|
||||
targetVer: '2.0.0'
|
||||
},
|
||||
run: async (process) => {
|
||||
const wm = await process.loadLibrary('lib/WindowManager')
|
||||
const HTML = await process.loadLibrary('lib/HTML')
|
||||
const { Input, Button } = await process.loadLibrary('lib/Components')
|
||||
|
||||
const win = wm.createWindow({
|
||||
title: 'Theme Maker',
|
||||
icon,
|
||||
width: 600,
|
||||
height: 200
|
||||
}, process)
|
||||
|
||||
const content = new HTML(win.content)
|
||||
|
||||
const items = [
|
||||
'crust',
|
||||
'mantle',
|
||||
'base',
|
||||
'surface-0',
|
||||
'surface-1',
|
||||
'surface-2',
|
||||
'text'
|
||||
]
|
||||
|
||||
const name = Input.new().attr({
|
||||
value: 'My Theme'
|
||||
})
|
||||
|
||||
content.appendMany(
|
||||
new HTML('div')
|
||||
.appendMany(
|
||||
new HTML('label').text('Name: '),
|
||||
name
|
||||
),
|
||||
...items.map((item) => {
|
||||
return new HTML('div')
|
||||
.appendMany(
|
||||
new HTML('label').text(`${item[0].toUpperCase() + item.slice(1)}: `),
|
||||
Input.new().attr({
|
||||
type: 'color',
|
||||
id: item,
|
||||
value: '#000000'
|
||||
})
|
||||
)
|
||||
}),
|
||||
Button.new().text('Create').on('click', () => {
|
||||
const theme: {
|
||||
[key: string]: any
|
||||
} = {
|
||||
name: name.getValue(),
|
||||
colors: {}
|
||||
}
|
||||
items.forEach((item) => {
|
||||
theme.colors[item] = content.qs(`#${item}`)?.getValue()
|
||||
})
|
||||
process.fs.writeFile(`/etc/themes/${theme.name.replace(/\s/g, '') as string}.theme`, JSON.stringify(theme))
|
||||
.then(() => {
|
||||
wm.createModal('ok', 'Theme Manager', 'Theme created successfully.', process)
|
||||
.catch(e => console.error(e))
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ThemeConfig
|
||||
|
|
@ -48,6 +48,23 @@ const Components: Library = {
|
|||
'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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,30 @@ const MIMETypes: Library = {
|
|||
opensWith: ['apps/ImageViewer'],
|
||||
icon: 'image'
|
||||
},
|
||||
cjs: {
|
||||
type: 'application/javascript',
|
||||
description: 'CommonJS Module',
|
||||
opensWith: ['apps/Editor'],
|
||||
icon: 'code'
|
||||
},
|
||||
htm: {
|
||||
type: 'text/html',
|
||||
description: 'HTML Document',
|
||||
opensWith: ['apps/Editor'],
|
||||
icon: 'code'
|
||||
},
|
||||
html: {
|
||||
type: 'text/html',
|
||||
description: 'HTML Document',
|
||||
opensWith: ['apps/Editor'],
|
||||
icon: 'code'
|
||||
},
|
||||
js: {
|
||||
type: 'text/javascript',
|
||||
description: 'JavaScript File',
|
||||
opensWith: ['apps/Editor'],
|
||||
icon: 'code'
|
||||
},
|
||||
lnk: {
|
||||
type: 'application/x-ms-shortcut',
|
||||
description: 'Windows Shortcut',
|
||||
|
|
@ -56,6 +80,12 @@ const MIMETypes: Library = {
|
|||
opensWith: ['apps/Editor'],
|
||||
icon: 'markdown'
|
||||
},
|
||||
mjs: {
|
||||
type: 'text/javascript',
|
||||
description: 'JavaScript Module',
|
||||
opensWith: ['apps/Editor'],
|
||||
icon: 'code'
|
||||
},
|
||||
mp4: {
|
||||
type: 'video/mp4',
|
||||
description: 'MP4 Video',
|
||||
|
|
@ -74,6 +104,12 @@ const MIMETypes: Library = {
|
|||
opensWith: ['apps/ImageViewer'],
|
||||
icon: 'image'
|
||||
},
|
||||
theme: {
|
||||
type: 'application/x-flow-theme',
|
||||
description: 'FlowOS Theme',
|
||||
opensWith: ['apps/Editor'],
|
||||
icon: 'palette'
|
||||
},
|
||||
txt: {
|
||||
type: 'text/plain',
|
||||
description: 'Text Document',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const WindowManager: Library = {
|
|||
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||
return win
|
||||
},
|
||||
createModal: async (title: string, text: string, process: ProcessLib) => {
|
||||
createModal: async (type: 'allow' | 'ok', title: string, text: string, process: ProcessLib) => {
|
||||
const win = new FlowWindow(process, WindowManager.data, {
|
||||
title,
|
||||
icon: '',
|
||||
|
|
@ -61,22 +61,31 @@ const WindowManager: Library = {
|
|||
win
|
||||
}
|
||||
}
|
||||
document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
||||
|
||||
const { Button } = await process.loadLibrary('lib/Components')
|
||||
|
||||
return {
|
||||
value: await new Promise((resolve) => {
|
||||
new HTML('h3').text(title).appendTo(win.content)
|
||||
new HTML('p').text(text).appendTo(win.content)
|
||||
Button.new().text('Allow').appendTo(win.content).on('click', () => {
|
||||
resolve(true)
|
||||
win.close()
|
||||
})
|
||||
Button.new().text('Deny').appendTo(win.content).on('click', () => {
|
||||
resolve(false)
|
||||
win.close()
|
||||
})
|
||||
new HTML('h3').text(title).style({ margin: '0' }).appendTo(win.content)
|
||||
new HTML('p').text(text).style({ margin: '0' }).appendTo(win.content)
|
||||
|
||||
if (type === 'allow') document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
||||
|
||||
if (type === 'allow') {
|
||||
Button.new().text('Allow').appendTo(win.content).on('click', () => {
|
||||
resolve(true)
|
||||
win.close()
|
||||
})
|
||||
Button.new().text('Deny').appendTo(win.content).on('click', () => {
|
||||
resolve(false)
|
||||
win.close()
|
||||
})
|
||||
} else if (type === 'ok') {
|
||||
Button.new().text('OK').appendTo(win.content).on('click', () => {
|
||||
win.close()
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
|
||||
WindowManager.data.windows.push(win)
|
||||
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ export interface WindowManager {
|
|||
windows: FlowWindow[]
|
||||
getHighestZIndex: () => number
|
||||
createWindow: (config: FlowWindowConfig, process: ProcessLib) => FlowWindow
|
||||
createModal: (title: string, text: string, process: ProcessLib) => Promise<ModalData>
|
||||
createModal: (type: 'allow' | 'ok', title: string, text: string, process: ProcessLib) => Promise<ModalData>
|
||||
}
|
||||
|
||||
export interface Launcher {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue