Compare commits

..

No commits in common. "ece744aafe7e7eba049fb706a0d16a2f9fa8799b" and "5da22eed8e6fc9305d311bd469d03d085c628f62" have entirely different histories.

2 changed files with 438 additions and 479 deletions

View File

@ -3,8 +3,7 @@
# @date 2023/08/08 14:07:26 # @date 2023/08/08 14:07:26
import gi, os, sys, json import gi, os, sys, json, shutil, hashlib, random
import webbrowser, shutil, hashlib, random
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
gi.require_version("WebKit2", "4.1") gi.require_version("WebKit2", "4.1")
@ -289,25 +288,6 @@ class WebEngine(WebKit2.WebView):
return (_error, output) return (_error, output)
def _shell_handler(self, params = {}):
_error = None
output = None
path = params.get('path')
match(params.get('action')):
case 'openExternal':
webbrowser.open(params.get('url'))
case 'showItemInFolder':
os.system(f"xdg-open '{path}'")
case 'openPath':
os.system(f"xdg-open '{path}'")
case 'trashItem':
pass
def _fs_handler(self, params = {}): def _fs_handler(self, params = {}):
_error = None _error = None

View File

@ -3,509 +3,488 @@
* @author yutent<yutent.io@gmail.com> * @author yutent<yutent.io@gmail.com>
* @date 2023/07/21 17:38:11 * @date 2023/07/21 17:38:11
*/ */
!(function () {
const MIME_TYPES = {
html: 'text/html',
json: 'application/json',
js: 'application/javascript',
htm: 'text/html',
txt: 'text/plain',
css: 'text/css',
webp: 'image/webp',
jpg: 'image/jpg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml',
ico: 'image/ico',
mp3: 'audio/mpeg',
ogg: 'audio/ogg',
m4a: 'audio/m4a',
amr: 'audio/amr',
mp4: 'video/mp4',
webm: 'video/webm',
wasm: 'application/wasm',
asm: 'application/asm',
zip: 'application/zip',
'7z': 'application/x-7z-compressed',
eot: 'application/vnd.ms-fontobject',
ttf: 'font/ttf',
otf: 'font/otf',
woff: 'font/woff',
woff2: 'font/woff2',
xls: 'application/vnd.ms-excel',
doc: 'application/msword',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
const KEYS_MAP = {
shift: '<Shift>',
ctrl: '<Ctrl>',
alt: '<Alt>',
super: '<Super>'
}
const NO_CALLBACK = false const MIME_TYPES = {
const CALL_ONCE = true html: 'text/html',
json: 'application/json',
js: 'application/javascript',
htm: 'text/html',
txt: 'text/plain',
css: 'text/css',
webp: 'image/webp',
jpg: 'image/jpg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml',
ico: 'image/ico',
mp3: 'audio/mpeg',
ogg: 'audio/ogg',
m4a: 'audio/m4a',
amr: 'audio/amr',
mp4: 'video/mp4',
webm: 'video/webm',
wasm: 'application/wasm',
asm: 'application/asm',
zip: 'application/zip',
'7z': 'application/x-7z-compressed',
eot: 'application/vnd.ms-fontobject',
ttf: 'font/ttf',
otf: 'font/otf',
woff: 'font/woff',
woff2: 'font/woff2',
xls: 'application/vnd.ms-excel',
doc: 'application/msword',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
const KEYS_MAP = {
shift: '<Shift>',
ctrl: '<Ctrl>',
alt: '<Alt>',
super: '<Super>'
}
const __events__ = Symbol('events') const NO_CALLBACK = false
const CALL_ONCE = true
function defer() { function defer() {
let obj = {} let obj = {}
obj.promise = new Promise((resolve, reject) => { obj.promise = new Promise((resolve, reject) => {
obj.resolve = resolve obj.resolve = resolve
obj.reject = reject obj.reject = reject
})
return obj
}
function rand(prefix = 'cb_') {
return prefix + Math.random().toString().slice(2)
}
function handler(event, data = {}, need = CALL_ONCE) {
let _ = defer()
let callback
if (need === NO_CALLBACK) {
_.resolve(true)
} else {
callback = rand()
native.$once(callback, (err, res) => {
if (err) {
_.reject(err)
} else {
_.resolve(res)
}
}) })
return obj
} }
function rand(prefix = 'cb_') { window.webkit.messageHandlers.app.postMessage({
return prefix + Math.random().toString().slice(2) event,
data,
callback
})
return _.promise
}
function base64(str = '') {
return btoa(str).replace(/[+=\/]/g, '')
}
function _postMessage(data = {}, uuid = null) {
let ev = new Event('message')
Object.assign(ev, {
data,
source: {
postMessage(msg) {
native.children.postMessage(msg, uuid)
}
}
})
window.dispatchEvent(ev)
}
function readonly(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
return value
},
enumerable: false
})
}
class NativeImage {
#origin
constructor(obj) {
this.#origin = obj
this.width = obj.width
this.height = obj.height
this.type = MIME_TYPES[obj.filepath.split('.').pop()]
} }
function handler(event, data = {}, need = CALL_ONCE) { toJSON() {
return this.#origin
}
export(type, base64) {
let _ = defer() let _ = defer()
let callback let canvas = document.createElement('canvas')
canvas.width = this.width
canvas.height = this.height
let ctx = canvas.getContext('2d')
let imgData = ctx.getImageData(0, 0, this.width, this.height)
let data = imgData.data
if (need === NO_CALLBACK) { for (let i = 0; i < this.#origin.bytes.length; i += 4) {
_.resolve(true) imgData.data[i] = this.#origin.bytes[i]
imgData.data[i + 1] = this.#origin.bytes[i + 1]
imgData.data[i + 2] = this.#origin.bytes[i + 2]
imgData.data[i + 3] = this.#origin.bytes[i + 3]
}
ctx.putImageData(imgData, 0, 0)
if (base64) {
return canvas.toDataURL(type || this.type, 1)
} else { } else {
callback = rand() canvas.toBlob(_.resolve, type || this.type, 1)
native.$once(callback, (err, res) => { return _.promise
if (err) {
_.reject(err)
} else {
_.resolve(res)
}
})
}
window.webkit.messageHandlers.app.postMessage({
event,
data,
callback
})
return _.promise
}
function base64(str = '') {
return btoa(str).replace(/[+=\/]/g, '')
}
function _postMessage(data = {}, uuid = null) {
let ev = new Event('message')
Object.assign(ev, {
data,
source: {
postMessage(msg) {
native.children.postMessage(msg, uuid)
}
}
})
window.dispatchEvent(ev)
}
function readonly(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
return value
},
enumerable: false
})
}
function _extend(origin, options = {}) {
for (let k in options) {
readonly(origin, k, options[k])
} }
} }
class NativeImage { toPNG() {
#origin return this.export('image/png')
}
constructor(obj) { toJPEG() {
this.#origin = obj return this.export('image/jpeg')
this.width = obj.width }
this.height = obj.height
this.type = MIME_TYPES[obj.filepath.split('.').pop()] toDataURL(type) {
return this.export(type, true)
}
}
class EventEmitter {
//
__events__ = Object.create(null)
$on(name, fn) {
if (this.__events__[name]) {
this.__events__[name].push(fn)
} else {
this.__events__[name] = [fn]
} }
}
toJSON() { $once(name, fn) {
return this.#origin fn.__once__ = true
} this.$on(name, fn)
}
export(type, base64) { $off(name, fn) {
let _ = defer() if (this.__events__[name]) {
let canvas = document.createElement('canvas') if (fn) {
canvas.width = this.width this.__events__[name] = this.__events__[name].filter(it => it !== fn)
canvas.height = this.height
let ctx = canvas.getContext('2d')
let imgData = ctx.getImageData(0, 0, this.width, this.height)
let data = imgData.data
for (let i = 0; i < this.#origin.bytes.length; i += 4) {
imgData.data[i] = this.#origin.bytes[i]
imgData.data[i + 1] = this.#origin.bytes[i + 1]
imgData.data[i + 2] = this.#origin.bytes[i + 2]
imgData.data[i + 3] = this.#origin.bytes[i + 3]
}
ctx.putImageData(imgData, 0, 0)
if (base64) {
return canvas.toDataURL(type || this.type, 1)
} else { } else {
canvas.toBlob(_.resolve, type || this.type, 1) this.__events__[name] = []
return _.promise
} }
} }
toPNG() {
return this.export('image/png')
}
toJPEG() {
return this.export('image/jpeg')
}
toDataURL(type) {
return this.export(type, true)
}
} }
class Native { $emit(name, ...args) {
// if (this.__events__[name]) {
[__events__] = Object.create(null) for (let fn of this.__events__[name]) {
try {
$on(name, fn) { fn.apply(this, args)
if (this[__events__][name]) { if (fn.__once__) {
this[__events__][name].push(fn) this.$off(name, fn)
} else {
this[__events__][name] = [fn]
}
}
$once(name, fn) {
fn.__once__ = true
this.$on(name, fn)
}
$off(name, fn) {
if (this[__events__][name]) {
if (fn) {
this[__events__][name] = this[__events__][name].filter(
it => it !== fn
)
} else {
this[__events__][name] = []
}
}
}
$emit(name, ...args) {
if (this[__events__][name]) {
for (let fn of this[__events__][name]) {
try {
fn.apply(this, args)
if (fn.__once__) {
this.$off(name, fn)
}
} catch (e) {
console.error(e)
} }
} catch (e) {
console.error(e)
} }
} }
} }
$destroy() {
this[__events__] = Object.create(null)
}
} }
readonly(window, 'native', new Native()) $destroy() {
this.__events__ = Object.create(null)
}
}
native.$on('opener_message', (data, uuid) => _postMessage(data, uuid)) readonly(window, 'native', new EventEmitter())
_extend(native, { native.$on('opener_message', (data, uuid) => _postMessage(data, uuid))
env: '{{env}}',
app: {
name: '{{app_name}}',
version: '{{app_version}}',
quit() {
return handler('app', { action: 'quit' }, NO_CALLBACK)
},
relaunch() {
return handler('app', { action: 'relaunch' }, NO_CALLBACK)
},
getLocale() {
return native.env.LANG
}
},
shell: {
openExternal(url) {},
showItemInFolder(path) {},
openPath(path) {},
trashItem(path) {}
},
fs: {
access(filepath, mode = 'r') {
return handler('fs', { action: 'access', mode, filepath })
},
read(filepath, mode = 'r') { Object.assign(native, {
return handler('fs', { action: 'read', mode, filepath }).then(r => env: '{{env}}',
mode.includes('b') ? new Uint8Array(r) : r app: {
) name: '{{app_name}}',
}, version: '{{app_version}}',
write(filepath, content = '', mode = 'w') { quit() {
return handler('fs', { return handler('app', { action: 'quit' }, NO_CALLBACK)
action: 'write',
mode,
filepath,
content
})
},
append(filepath, content = '', mode = 'a+') {
return handler('fs', {
action: 'write',
mode,
filepath,
content
})
},
exists(filepath) {
return handler('fs', { action: 'exists', filepath })
},
list(filepath) {
return handler('fs', { action: 'list', filepath })
},
isfile(filepath) {
return handler('fs', { action: 'isfile', filepath })
},
isdir(filepath) {
return handler('fs', { action: 'isdir', filepath })
},
remove(filepath) {
return handler('fs', { action: 'remove', filepath })
},
rename(filepath, target) {
return handler('fs', { action: 'rename', filepath, target })
},
copy(filepath, target) {
return handler('fs', { action: 'copy', filepath, target })
},
mkdir(filepath) {
return handler('fs', { action: 'mkdir', filepath })
}
}, },
image(filepath) { relaunch() {
return handler('image', { value: filepath }).then(r => new NativeImage(r)) return handler('app', { action: 'relaunch' }, NO_CALLBACK)
}, },
clipboard: { getLocale() {
readText() { return native.env.LANG
return handler('clipboard', { action: 'wait_for_text' }) }
}, },
writeText(value) { fs: {
return handler('clipboard', { action: 'set_text', value }, NO_CALLBACK) access(filepath, mode = 'r') {
}, return handler('fs', { action: 'access', mode, filepath })
readImage() {
return handler('clipboard', { action: 'wait_for_image' }).then(r =>
r ? new NativeImage(r) : r
)
},
writeImage(value) {
// 暂不知原因, postMessage传给Gtk后, JSON.stringify()并未读取toJSON的结果
if (typeof value === 'object') {
value = value.toJSON()
}
return handler('clipboard', { action: 'set_image', value }, NO_CALLBACK)
},
clear() {
return handler('clipboard', { action: 'clear' }, NO_CALLBACK)
}
}, },
screen: {
getAllDisplays() {
return handler('monitor', { action: 'get-all' })
},
getPrimaryDisplay() {
return handler('monitor', { action: 'get-primary' })
}
},
globalShortcut: {
get enabled() {
return handler('keybinder', { action: 'supported' })
},
register(keyMap, callback) {
let shortcut_callback = base64(keyMap)
native.$off(shortcut_callback)
native.$on(shortcut_callback, callback)
return handler('keybinder', {
action: 'register',
value: keyMap,
shortcut_callback
})
},
unregister(keyMap) {
let shortcut_callback = base64(keyMap)
native.$off(shortcut_callback)
return handler('keybinder', { action: 'unregister', value: keyMap })
},
unregisterAll(keyMaps) {
for (let it of keyMaps) {
this.unregister(it)
}
}
},
tray: {
create() {
//
},
remove() {
//
},
/** read(filepath, mode = 'r') {
* 设置普通状态的tray图标, 只需要传名称, 自动会去当前主题下去找 return handler('fs', { action: 'read', mode, filepath }).then(r =>
*/ mode.includes('b') ? new Uint8Array(r) : r
set_icon(name) { )
return handler('tray', { action: 'set_icon', value: name })
},
/**
* 设置警示图标, 同上
*/
set_attention_icon(name) {
return handler('tray', { action: 'set_attention_icon', value: name })
},
/**
*
*/
set_title(title) {
return handler('tray', { action: 'set_title', value: title })
},
/**
* 修改tray图标状态
* @param status <Number> 0: 隐藏, 1: 显示, 2: 重要(对应上面的attention_icon)
*/
set_status(status) {
return handler('tray', { action: 'set_status', value: status })
}
}, },
opener: { write(filepath, content = '', mode = 'w') {
postMessage(data = {}) { return handler('fs', {
return handler('opener', { action: 'postmessage', data }, NO_CALLBACK) action: 'write',
} mode,
filepath,
content
})
}, },
children: { append(filepath, content = '', mode = 'a+') {
postMessage(data = {}, uuid = null) { return handler('fs', {
return handler( action: 'write',
'children', mode,
{ action: 'postmessage', data, uuid }, filepath,
NO_CALLBACK content
) })
} },
exists(filepath) {
return handler('fs', { action: 'exists', filepath })
},
list(filepath) {
return handler('fs', { action: 'list', filepath })
},
isfile(filepath) {
return handler('fs', { action: 'isfile', filepath })
},
isdir(filepath) {
return handler('fs', { action: 'isdir', filepath })
},
remove(filepath) {
return handler('fs', { action: 'remove', filepath })
},
rename(filepath, target) {
return handler('fs', { action: 'rename', filepath, target })
},
copy(filepath, target) {
return handler('fs', { action: 'copy', filepath, target })
},
mkdir(filepath) {
return handler('fs', { action: 'mkdir', filepath })
}
},
image(filepath) {
return handler('image', { value: filepath }).then(r => new NativeImage(r))
},
clipboard: {
readText() {
return handler('clipboard', { action: 'wait_for_text' })
},
writeText(value) {
return handler('clipboard', { action: 'set_text', value }, NO_CALLBACK)
},
readImage() {
return handler('clipboard', { action: 'wait_for_image' }).then(r =>
r ? new NativeImage(r) : r
)
},
writeImage(value) {
// 暂不知原因, postMessage传给Gtk后, JSON.stringify()并未读取toJSON的结果
if (typeof value === 'object') {
value = value.toJSON()
}
return handler('clipboard', { action: 'set_image', value }, NO_CALLBACK)
},
clear() {
return handler('clipboard', { action: 'clear' }, NO_CALLBACK)
}
},
screen: {
getAllDisplays() {
return handler('monitor', { action: 'get-all' })
},
getPrimaryDisplay() {
return handler('monitor', { action: 'get-primary' })
}
},
globalShortcut: {
get enabled() {
return handler('keybinder', { action: 'supported' })
},
register(keyMap, callback) {
let shortcut_callback = base64(keyMap)
native.$off(shortcut_callback)
native.$on(shortcut_callback, callback)
return handler('keybinder', {
action: 'register',
value: keyMap,
shortcut_callback
})
},
unregister(keyMap) {
let shortcut_callback = base64(keyMap)
native.$off(shortcut_callback)
return handler('keybinder', { action: 'unregister', value: keyMap })
},
unregisterAll(keyMaps) {
for (let it of keyMaps) {
this.unregister(it)
}
}
},
tray: {
create() {
//
},
remove() {
//
}, },
window: {
uuid: '{{uuid}}',
create(options = {}) {
return handler('window', { action: 'create', options })
},
close() { /**
return handler('window', { action: 'close' }) * 设置普通状态的tray图标, 只需要传名称, 自动会去当前主题下去找
}, */
set_icon(name) {
isVisible() { return handler('tray', { action: 'set_icon', value: name })
return handler('window', { action: 'is_visible' })
},
toggleVisible() {
handler('window', { action: 'toggle_visible' }, NO_CALLBACK)
},
hide() {
handler('window', { action: 'hide' }, NO_CALLBACK)
},
show() {
handler('window', { action: 'show' }, NO_CALLBACK)
},
fullscreen() {
handler('window', { action: 'fullscreen' }, NO_CALLBACK)
},
unfullscreen() {
handler('window', { action: 'unfullscreen' }, NO_CALLBACK)
},
maximize() {
handler('window', { action: 'maximize' }, NO_CALLBACK)
},
unmaximize() {
handler('window', { action: 'unmaximize' }, NO_CALLBACK)
},
setTitle(title = '') {
handler('window', { action: 'set_title', value: title }, NO_CALLBACK)
},
resize(width = 0, height = 0) {
handler(
'window',
{ action: 'resize', value: { width, height } },
NO_CALLBACK
)
},
move(x = 0, y = 0) {
handler('window', { action: 'move', value: { x, y } }, NO_CALLBACK)
},
setOpacity(opacity = 1) {
handler(
'window',
{ action: 'set_opacity', value: opacity },
NO_CALLBACK
)
},
alwayOnTop(setting = true) {
handler(
'window',
{ action: 'set_keep_above', value: setting },
NO_CALLBACK
)
},
alwayOnBotttom(setting = true) {
handler(
'window',
{ action: 'set_keep_below', value: setting },
NO_CALLBACK
)
}
}, },
notify({ title, summary, icon, progress = 0, urgency = 0, callback }) {
let eventName /**
if (callback) { * 设置警示图标, 同上
eventName = rand() */
native.$once(eventName, callback) set_attention_icon(name) {
} return handler('tray', { action: 'set_attention_icon', value: name })
},
/**
*
*/
set_title(title) {
return handler('tray', { action: 'set_title', value: title })
},
/**
* 修改tray图标状态
* @param status <Number> 0: 隐藏, 1: 显示, 2: 重要(对应上面的attention_icon)
*/
set_status(status) {
return handler('tray', { action: 'set_status', value: status })
}
},
opener: {
postMessage(data = {}) {
return handler('opener', { action: 'postmessage', data }, NO_CALLBACK)
}
},
children: {
postMessage(data = {}, uuid = null) {
return handler(
'children',
{ action: 'postmessage', data, uuid },
NO_CALLBACK
)
}
},
window: {
uuid: '{{uuid}}',
create(options = {}) {
return handler('window', { action: 'create', options })
},
close() {
return handler('window', { action: 'close' })
},
isVisible() {
return handler('window', { action: 'is_visible' })
},
toggleVisible() {
handler('window', { action: 'toggle_visible' }, NO_CALLBACK)
},
hide() {
handler('window', { action: 'hide' }, NO_CALLBACK)
},
show() {
handler('window', { action: 'show' }, NO_CALLBACK)
},
fullscreen() {
handler('window', { action: 'fullscreen' }, NO_CALLBACK)
},
unfullscreen() {
handler('window', { action: 'unfullscreen' }, NO_CALLBACK)
},
maximize() {
handler('window', { action: 'maximize' }, NO_CALLBACK)
},
unmaximize() {
handler('window', { action: 'unmaximize' }, NO_CALLBACK)
},
setTitle(title = '') {
handler('window', { action: 'set_title', value: title }, NO_CALLBACK)
},
resize(width = 0, height = 0) {
handler( handler(
'notify', 'window',
{ title, summary, icon, progress, urgency, callback: eventName }, { action: 'resize', value: { width, height } },
NO_CALLBACK NO_CALLBACK
) )
}, },
move(x = 0, y = 0) {
md5(value = '') { handler('window', { action: 'move', value: { x, y } }, NO_CALLBACK)
return handler('md5', { value })
}, },
setOpacity(opacity = 1) {
proxy: { handler('window', { action: 'set_opacity', value: opacity }, NO_CALLBACK)
disable() {
return handler('proxy', { action: 'disable' }, NO_CALLBACK)
},
system() {
return handler('proxy', { action: 'system' }, NO_CALLBACK)
},
custom(url = '', ignore = null) {
return handler('proxy', { action: 'enable', url, ignore }, NO_CALLBACK)
}
}, },
alwayOnTop(setting = true) {
handler(
'window',
{ action: 'set_keep_above', value: setting },
NO_CALLBACK
)
},
alwayOnBotttom(setting = true) {
handler(
'window',
{ action: 'set_keep_below', value: setting },
NO_CALLBACK
)
}
},
notify({ title, summary, icon, progress = 0, urgency = 0, callback }) {
let eventName
if (callback) {
eventName = rand()
native.$once(eventName, callback)
}
handler(
'notify',
{ title, summary, icon, progress, urgency, callback: eventName },
NO_CALLBACK
)
},
handler md5(value = '') {
}) return handler('md5', { value })
})() },
proxy: {
disable() {
return handler('proxy', { action: 'disable' }, NO_CALLBACK)
},
system() {
return handler('proxy', { action: 'system' }, NO_CALLBACK)
},
custom(url = '', ignore = null) {
return handler('proxy', { action: 'enable', url, ignore }, NO_CALLBACK)
}
},
handler
})