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;
|
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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 }: {
|
.then(async ({ value, win }: {
|
||||||
value: boolean
|
value: boolean
|
||||||
win: FlowWindow
|
win: FlowWindow
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { Directory, Errors, File, Permission, Stats } from '../types'
|
import { Directory, Errors, File, Permission, Stats } from '../types'
|
||||||
|
import path from 'path'
|
||||||
|
const p = path
|
||||||
|
|
||||||
export const defaultFS: { root: Directory } = {
|
export const defaultFS: { root: Directory } = {
|
||||||
root: {
|
root: {
|
||||||
|
|
@ -75,6 +77,12 @@ export const defaultFS: { root: Directory } = {
|
||||||
deleteable: true,
|
deleteable: true,
|
||||||
permission: Permission.USER,
|
permission: Permission.USER,
|
||||||
content: Buffer.from('apps/Settings')
|
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,
|
permission: Permission.USER,
|
||||||
content: Buffer.from('/home/Applications/Info.app')
|
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': {
|
'Browser.lnk': {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
deleteable: true,
|
deleteable: true,
|
||||||
permission: Permission.USER,
|
permission: Permission.USER,
|
||||||
content: Buffer.from('/home/Applications/Browser.app')
|
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': {
|
'Files.lnk': {
|
||||||
type: 'file',
|
type: 'file',
|
||||||
deleteable: true,
|
deleteable: true,
|
||||||
permission: Permission.USER,
|
permission: Permission.USER,
|
||||||
content: Buffer.from('/home/Applications/Files.app')
|
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,
|
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 +214,62 @@ 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 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> {
|
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()
|
||||||
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 +281,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 +394,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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,26 +12,6 @@ interface EditorConfig {
|
||||||
path: string
|
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 = {
|
const Editor: Process = {
|
||||||
config: {
|
config: {
|
||||||
name: 'Editor',
|
name: 'Editor',
|
||||||
|
|
@ -40,6 +20,8 @@ const Editor: Process = {
|
||||||
targetVer: '1.0.0-indev.0'
|
targetVer: '1.0.0-indev.0'
|
||||||
},
|
},
|
||||||
run: async (process) => {
|
run: async (process) => {
|
||||||
|
const MIMETypes = await process.loadLibrary('lib/MIMETypes')
|
||||||
|
|
||||||
if (Object.keys(process.data).length > 0) {
|
if (Object.keys(process.data).length > 0) {
|
||||||
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
const win = await process.loadLibrary('lib/WindowManager').then((wm: any) => {
|
||||||
return wm.createWindow({
|
return wm.createWindow({
|
||||||
|
|
@ -153,8 +135,44 @@ const Editor: Process = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const fileExtension = data.path.split('.').pop()?.toLowerCase() as string
|
const fileExtension = (data.path.split('.').pop() as string).toLowerCase()
|
||||||
const language = fileLanguageMap[fileExtension] ?? 'text'
|
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 value = Buffer.from(await fs.readFile(data.path)).toString()
|
||||||
const editor = fullEditor(
|
const editor = fullEditor(
|
||||||
|
|
@ -169,6 +187,7 @@ const Editor: Process = {
|
||||||
const style = document.createElement('style')
|
const style = document.createElement('style')
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.prism-code-editor {
|
.prism-code-editor {
|
||||||
|
color: var(--text);
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
caret-color: var(--text);
|
caret-color: var(--text);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
@ -208,6 +227,9 @@ const Editor: Process = {
|
||||||
document.addEventListener('fs_update', () => {
|
document.addEventListener('fs_update', () => {
|
||||||
render().catch(e => console.error(e))
|
render().catch(e => console.error(e))
|
||||||
})
|
})
|
||||||
|
document.addEventListener('theme_update', () => {
|
||||||
|
render().catch(e => console.error(e))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return
|
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.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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
})
|
})
|
||||||
|
|
|
||||||
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
|
'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'],
|
opensWith: ['apps/ImageViewer'],
|
||||||
icon: 'image'
|
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: {
|
lnk: {
|
||||||
type: 'application/x-ms-shortcut',
|
type: 'application/x-ms-shortcut',
|
||||||
description: 'Windows Shortcut',
|
description: 'Windows Shortcut',
|
||||||
|
|
@ -56,6 +80,12 @@ const MIMETypes: Library = {
|
||||||
opensWith: ['apps/Editor'],
|
opensWith: ['apps/Editor'],
|
||||||
icon: 'markdown'
|
icon: 'markdown'
|
||||||
},
|
},
|
||||||
|
mjs: {
|
||||||
|
type: 'text/javascript',
|
||||||
|
description: 'JavaScript Module',
|
||||||
|
opensWith: ['apps/Editor'],
|
||||||
|
icon: 'code'
|
||||||
|
},
|
||||||
mp4: {
|
mp4: {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
description: 'MP4 Video',
|
description: 'MP4 Video',
|
||||||
|
|
@ -74,6 +104,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',
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ const WindowManager: Library = {
|
||||||
WindowManager.data.windowArea.elm.appendChild(win.element)
|
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||||
return win
|
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, {
|
const win = new FlowWindow(process, WindowManager.data, {
|
||||||
title,
|
title,
|
||||||
icon: '',
|
icon: '',
|
||||||
|
|
@ -61,22 +61,31 @@ const WindowManager: Library = {
|
||||||
win
|
win
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
|
||||||
|
|
||||||
const { Button } = await process.loadLibrary('lib/Components')
|
const { Button } = await process.loadLibrary('lib/Components')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value: await new Promise((resolve) => {
|
value: await new Promise((resolve) => {
|
||||||
new HTML('h3').text(title).appendTo(win.content)
|
new HTML('h3').text(title).style({ margin: '0' }).appendTo(win.content)
|
||||||
new HTML('p').text(text).appendTo(win.content)
|
new HTML('p').text(text).style({ margin: '0' }).appendTo(win.content)
|
||||||
Button.new().text('Allow').appendTo(win.content).on('click', () => {
|
|
||||||
resolve(true)
|
if (type === 'allow') document.dispatchEvent(new CustomEvent('app_opened', appOpenedEvent))
|
||||||
win.close()
|
|
||||||
})
|
if (type === 'allow') {
|
||||||
Button.new().text('Deny').appendTo(win.content).on('click', () => {
|
Button.new().text('Allow').appendTo(win.content).on('click', () => {
|
||||||
resolve(false)
|
resolve(true)
|
||||||
win.close()
|
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.windows.push(win)
|
||||||
WindowManager.data.windowArea.elm.appendChild(win.element)
|
WindowManager.data.windowArea.elm.appendChild(win.element)
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ export interface WindowManager {
|
||||||
windows: FlowWindow[]
|
windows: FlowWindow[]
|
||||||
getHighestZIndex: () => number
|
getHighestZIndex: () => number
|
||||||
createWindow: (config: FlowWindowConfig, process: ProcessLib) => FlowWindow
|
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 {
|
export interface Launcher {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue