antos-frontend/src/core/gui.ts
Xuan Sang LE 1c32f2010c Switch from coffee script to typescrit
- Better tooling support
- Stronger type checking
- Unit test with jest
- JSDocc support
2020-05-29 22:22:00 +02:00

1092 lines
36 KiB
TypeScript

/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// AnTOS Web desktop is is licensed under the GNU General Public
// License v3.0, see the LICENCE file for more information
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
//along with this program. If not, see https://www.gnu.org/licenses/.
namespace OS {
export namespace GUI {
/**
*
*
* @export
* @interface ShortcutType
*/
export interface ShortcutType {
ALT: GenericObject<(e: JQuery.MouseDownEvent) => void>;
CTRL: GenericObject<(e: JQuery.MouseDownEvent) => void>;
SHIFT: GenericObject<(e: JQuery.MouseDownEvent) => void>;
META: GenericObject<(e: JQuery.MouseDownEvent) => void>;
}
/**
*
*
* @export
* @interface BasicItemType
*/
export interface BasicItemType {
text: string;
children?: BasicItemType[];
nodes?: BasicItemType[];
[propName: string]: any;
}
export var workspace: string = "#desktop";
export var fullscreen = false;
export var dialog: BaseDialog;
var shortcut: ShortcutType = {
ALT: {},
CTRL: {},
SHIFT: {},
META: {},
};
/**
*
*
* @export
* @param {string} html
* @param {BaseModel} app
* @param {(Element | string)} parent
*/
export function htmlToScheme(
html: string,
app: BaseModel,
parent: Element | string
): void {
const scheme = $.parseHTML(html);
if (app.scheme) {
$(app.scheme).remove();
}
$(parent as GenericObject<any>).append(scheme);
app.scheme = scheme[0] as HTMLElement;
app.scheme.uify(app.observable);
app.main();
app.show();
}
/**
*
*
* @export
* @param {string} path
* @param {BaseModel} app
* @param {(HTMLElement | string)} parent
*/
export function loadScheme(
path: string,
app: BaseModel,
parent: HTMLElement | string
): void {
path.asFileHandle()
.read()
.then(function (x) {
if (!x) {
return;
}
htmlToScheme(x, app, parent);
})
.catch((e) => {
announcer.oserror(__("Cannot load scheme: {0}", path), e);
});
}
/**
*
*
* @export
*/
export function clearTheme(): void {
$("head link#ostheme").attr("href", "");
}
/**
*
*
* @export
* @param {string} name
* @param {boolean} force
*/
export function loadTheme(name: string, force: boolean): void {
if (force) {
clearTheme();
}
const path = `resources/themes/${name}/${name}.css`;
$("head link#ostheme").attr("href", path);
}
/**
*
*
* @export
* @param {(string | BaseDialog)} d
* @param {GenericObject<any>} data
* @returns {Promise<any>}
*/
export function openDialog(
d: string | BaseDialog,
data: GenericObject<any>
): Promise<any> {
return new Promise(function (resolve, reject) {
if (dialog) {
dialog.show();
return resolve();
}
if (typeof d === "string") {
if (!dialogs[d]) {
const ex = API.throwe("Dialog");
return reject(ex);
}
dialog = new dialogs[d]();
} else {
dialog = d as GUI.BaseDialog;
}
dialog.parent = undefined;
dialog.handle = resolve;
dialog.pid = -1;
dialog.data = data;
return dialog.init();
});
}
/**
*
*
* @export
* @param {string} mime
* @returns {API.PackageMetaType[]}
*/
export function appsByMime(mime: string): API.PackageMetaType[] {
const metas: API.PackageMetaType[] = [];
for (let k in setting.system.packages) {
const v = setting.system.packages[k];
if (v && v.app) {
metas.push(v);
}
}
let m: API.PackageMetaType;
const mimes: Array<string[]> = [];
for (m of Array.from(metas)) {
if (m) {
mimes.push(m.mimes);
}
}
const apps: API.PackageMetaType[] = [];
// search app by mimes
const f = function (arr: string[], idx: number) {
try {
return arr.filter(function (m, i) {
if (mime.match(new RegExp(m, "g"))) {
if (apps.indexOf(metas[idx]) >= 0) {
return false;
}
apps.push(metas[idx]);
return false;
}
return false;
});
} catch (e) {
return announcer.osfail(
__("Error find app by mimes {0}", mime),
e
);
}
};
let arr: string[];
for (let i = 0; i < mimes.length; i++) {
arr = mimes[i];
if (arr) {
f(arr, i);
}
}
return apps;
}
/**
*
*
* @export
* @returns {{
* [index: string]: API.PackageMetaType;
* }}
*/
export function appsWithServices(): {
[index: string]: API.PackageMetaType;
} {
const o: { [index: string]: API.PackageMetaType } = {};
for (let k in setting.system.packages) {
const v = setting.system.packages[k];
if (v && v.services && v.services.length > 0) {
o[k] = v;
}
}
return o;
}
/**
*
*
* @export
* @param {AppArgumentsType} it
* @returns {void}
*/
export function openWith(it: AppArgumentsType): void {
if (!it) {
return;
}
if (it.type === "app" && it.app) {
return launch(it.app, []);
}
if (it.type === "app") {
return announcer.osinfo(
__("Application {0} is not executable", it.text)
);
}
const apps = appsByMime(it.type === "dir" ? "dir" : it.mime);
if (apps.length === 0) {
return announcer.osinfo(
__("No application available to open {0}", it.filename)
);
}
if (apps.length === 1) {
return launch(apps[0].app, [it]);
}
const list = Array.from(apps).map((e) => ({
text: e.app,
icon: e.icon,
iconclass: e.iconclass,
}));
openDialog("SelectionDialog", {
title: __("Open with"),
data: list,
}).then((d) => launch(d.text, [it]));
}
/**
*
*
* @export
* @param {string} app
* @param {AppArgumentsType[]} args
* @returns {void}
*/
export function forceLaunch(
app: string,
args: AppArgumentsType[]
): void {
console.warn(
"This method is used for developing only, please use the launch method instead"
);
unloadApp(app);
return launch(app, args);
}
/**
*
*
* @param {string} app
*/
function unloadApp(app: string): void {
PM.killAll(app, true);
if (app[app] && app[app].style) {
$(app[app].style).remove();
}
delete app[app];
}
/**
*
*
* @param {string} app
* @returns {Promise<string>}
*/
function loadApp(app: string): Promise<string> {
return new Promise(async function (resolve, reject) {
let path: string;
if (setting.system.packages[app].path) {
path = setting.system.packages[app].path;
}
const js = path + "/main.js";
try {
const d = await js.asFileHandle().read("script");
try {
const data: API.PackageMetaType = await `${path}/package.json`
.asFileHandle()
.read("json");
data.path = path;
if (application[app]) {
application[app].meta = data;
}
if (data.services) {
for (let v of data.services) {
application[v].meta = data;
}
}
//load css file
const css = `${path}/main.css`;
try {
const d_1 = await css.asFileHandle().onready();
const stamp = new Date().timestamp();
const el = $("<link>", {
rel: "stylesheet",
type: "text/css",
href: `${API.handle.get}/${css}?stamp=${stamp}`,
}).appendTo("head");
if (application[app]) {
application[app].style = el[0];
}
return resolve(app);
} catch (e) {
return resolve(app);
}
} catch (e_1) {
return reject(__e(e_1));
}
} catch (e_2) {
return reject(__e(e_2));
}
});
}
/**
*
*
* @export
* @param {string} ph
* @returns {Promise<PM.ProcessType>}
*/
export function pushService(ph: string): Promise<PM.ProcessType> {
return new Promise(async function (resolve, reject) {
const arr = ph.split("/");
const srv = arr[1];
const app = arr[0];
if (application[srv]) {
try {
const d = await OS.PM.createProcess(
srv,
application[srv]
);
return resolve(d);
} catch (e) {
return reject(__e(e));
}
} else {
try {
await loadApp(app);
if (!application[srv]) {
return reject(
API.throwe(__("Service not found: {0}", ph))
);
}
try {
const d_1 = await PM.createProcess(
srv,
application[srv]
);
return resolve(d_1);
} catch (e_1) {
return reject(__e(e_1));
}
} catch (e_2) {
return reject(__e(e_2));
}
}
});
}
/**
*
*
* @export
* @param {string[]} srvs
* @returns {Promise<any>}
*/
export function pushServices(srvs: string[]): Promise<any> {
return new Promise(function (resolve, reject) {
if (!(srvs.length > 0)) {
return resolve();
}
const srv = srvs.splice(0, 1)[0];
return pushService(srv)
.then((d: any) =>
pushServices(srvs)
.then(() => resolve())
.catch((e) => reject(__e(e)))
)
.catch(function (e: Error) {
announcer.osfail(__("Unable to load: {0}", srv), e);
return pushServices(srvs)
.then(() => resolve())
.catch((e) => reject(__e(e)));
});
});
}
/**
*
*
* @export
* @param {string} app
* @param {AppArgumentsType[]} args
*/
export function launch(app: string, args: AppArgumentsType[]): void {
if (!application[app]) {
// first load it
loadApp(app)
.then((a) =>
PM.createProcess(app, application[app], args)
.catch((e) =>
announcer.osfail(
__("Unable to launch: {0}", app),
e
)
)
)
.catch((e) =>
announcer.osfail(__("Unable to launch: {0}", app), e)
);
} else {
// now launch it
if (application[app]) {
PM.createProcess(
app,
application[app],
args
).catch((e: Error) =>
announcer.osfail(__("Unable to launch: {0}", app), e)
);
} else {
announcer.osfail(
__("Unable to find: {0}", app),
API.throwe("Application not found")
);
}
}
}
/**
*
*
* @export
* @param {BaseApplication} app
* @param {API.PackageMetaType} meta
* @returns {void}
*/
export function dock(
app: OS.application.BaseApplication,
meta: API.PackageMetaType
): void {
// dock an application to a dock
// create a data object
const data = {
icon: null,
iconclass: meta.iconclass || "",
app,
onbtclick() {
return app.toggle();
},
};
// TODO: this path is not good, need to create a blob of it
if (meta.icon) {
data.icon = `${meta.path}/${meta.icon}`;
}
// TODO: add default app icon class in system setting
// so that it can be themed
if (!meta.icon && !meta.iconclass) {
data.iconclass = "fa fa-cogs";
}
const dock = $("#sysdock")[0] as tag.AppDockTag;
app.init();
return app.one("rendered", function () {
dock.newapp(data);
app.sysdock = dock;
app.appmenu = $(
"[data-id = 'appmenu']",
"#syspanel"
)[0] as tag.MenuTag;
app.subscribe("systemlocalechange", function (name) {
app.updateLocale(name);
return app.update();
});
return app.subscribe("appregistry", function (m) {
if (m.name === app.name) {
return app.applySetting(m.data.m);
}
});
});
}
/**
*
*
* @export
*/
export function toggleFullscreen(): void {
const el = document.documentElement;
if (fullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
}
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
}
if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
if (document.cancelFullScreen) {
document.cancelFullScreen();
}
} else {
if (el.requestFullscreen) {
el.requestFullscreen();
}
if (el.mozRequestFullScreen) {
el.mozRequestFullScreen();
}
if (el.webkitRequestFullscreen) {
el.webkitRequestFullscreen();
}
if (el.msRequestFullscreen) {
el.msRequestFullscreen();
}
}
}
/**
*
*
* @export
* @param {BaseApplication} app
* @returns
*/
export function undock(app: application.BaseApplication) {
return ($("#sysdock")[0] as tag.AppDockTag).removeapp(app);
}
/**
*
*
* @export
* @param {BaseService} srv
* @returns {void}
*/
export function attachservice(srv: application.BaseService): void {
($("#syspanel")[0] as tag.SystemPanelTag).attachservice(srv);
srv.init();
return srv.subscribe("systemlocalechange", (name) => srv.update());
}
/**
*
*
* @export
* @param {BaseService} srv
* @returns {void}
*/
export function detachservice(srv: application.BaseService): void {
return ($("#syspanel")[0] as tag.SystemPanelTag).detachservice(srv);
}
/**
*
*
* @param {JQuery.MouseEventBase} event
* @returns {void}
*/
function bindContextMenu(event: JQuery.MouseEventBase): void {
var handle = function (e: HTMLElement) {
if (e.contextmenuHandle) {
return e.contextmenuHandle(
event,
$("#contextmenu")[0] as tag.MenuTag
);
} else {
const p = $(e).parent().get(0);
if (p !== $("#workspace").get(0)) {
return handle(p);
}
}
};
handle(event.target);
return event.preventDefault();
}
/**
*
*
* @export
* @param {string} k
* @param {(e: JQuery.MouseDownEvent) => void} f
* @returns {void}
*/
export function bindKey(
k: string,
f: (e: JQuery.MouseDownEvent) => void
): void {
const arr = k.split("-");
if (arr.length !== 2) {
return;
}
const fnk = arr[0].toUpperCase();
const c = arr[1].toUpperCase();
if (!shortcut[fnk]) {
return;
}
shortcut[fnk][c] = f;
}
/**
*
*
* @export
* @param {setting.WPSettingType} obj
*/
export function wallpaper(obj: setting.WPSettingType): void {
if (obj) {
setting.appearance.wp = obj;
}
const wp = setting.appearance.wp;
$("body")
.css("background-image", `url(${API.handle.get}/${wp.url})`)
.css("background-size", wp.size)
.css("background-repeat", wp.repeat);
}
/**
*
*
* @param {JQuery<HTMLElement>} el
* @param {string} text
* @param {JQuery.MouseEventBase} e
* @returns {void}
*/
function showTooltip(
el: JQuery<HTMLElement>,
text: string,
e: JQuery.MouseEventBase
): void {
let left: number, top: number;
const label = $("#systooltip")[0] as tag.LabelTag;
var cb = function (ev: JQuery.MouseEventBase) {
if ($(ev.target).closest(el).length === 0) {
$(label).hide();
return $(document).off("mousemove", cb);
}
};
$(document).on("mousemove", cb);
const arr = text.split(/:(.+)/);
let tip = text;
if (arr.length > 1) {
tip = arr[1];
}
const offset = $(el).offset();
const w = $(el).width();
const h = $(el).height();
label.text = tip;
$(label).show();
switch (arr[0]) {
case "cr": // center right of the element
left = offset.left + w + 5;
top = offset.top + h / 2 - $(label).height() / 2;
break;
case "ct": //ceter top
left = offset.left + w / 2 - $(label).width() / 2;
top = offset.top - $(label).height() - 5;
break;
default:
if (!e) {
return;
}
top = e.clientY + 5;
left = e.clientX + 5;
}
$(label)
.css("top", top + "px")
.css("left", left + "px");
}
/**
*
*
* @param {tag.FloatListTag} desktop
*/
function dkfetch(desktop: tag.FloatListTag): void {
const file = setting.desktop.path.asFileHandle();
const fn = () =>
file.read().then(function (d) {
if (d.error) {
return announcer.osfail(d.error, API.throwe(d.error));
}
const items = [];
$.each(d.result, function (i, v) {
if (
v.filename[0] === "." &&
!setting.desktop.showhidden
) {
return;
}
v.text = v.filename;
//v.text = v.text.substring(0,9) + "..." ifv.text.length > 10
v.iconclass = v.type;
return items.push(v);
});
desktop.data = items;
return desktop.calibrate();
});
file.onready()
.then(() => fn())
.catch(async function (e) {
// try to create the path
console.log(`${file.path} not found`);
const name = file.basename;
try {
const r = await file.parent().asFileHandle().mk(name);
return API.throwe("OS.VFS");
} catch (e_1) {
return announcer.osfail(e_1.toString(), e_1);
}
});
}
/**
*
*
*/
function initDM(): void {
const scheme = $.parseHTML(schemes.ws);
$("#wrapper").append(scheme);
announcer.observable.one("sysdockloaded", () => {
$(window).bind("keydown", function (event) {
const dock = $("#sysdock")[0] as tag.AppDockTag;
if (!dock) {
return;
}
const app = dock.selectedApp;
//return true unless app
const c = String.fromCharCode(event.which).toUpperCase();
let fnk = undefined;
if (event.ctrlKey) {
fnk = "CTRL";
} else if (event.metaKey) {
fnk = "META";
} else if (event.shiftKey) {
fnk = "SHIFT";
} else if (event.altKey) {
fnk = "ALT";
}
if (!fnk) {
return;
}
const r = app ? app.shortcut(fnk, c, event) : true;
if (!r) {
return event.preventDefault();
}
if (!shortcut[fnk]) {
return;
}
if (!shortcut[fnk][c]) {
return;
}
shortcut[fnk][c](event);
return event.preventDefault();
})
}
);
// system menu and dock
$("#syspanel")[0].uify(undefined);
$("#sysdock")[0].uify(undefined);
$("#systooltip")[0].uify(undefined);
$("#contextmenu")[0].uify(undefined);
$("#workspace").contextmenu((e) => bindContextMenu(e));
// tooltip
$(document).mouseover(function (e) {
const el: any = $(e.target).closest("[tooltip]");
if (!(el.length > 0)) {
return;
}
return showTooltip(
el as JQuery<HTMLElement>,
$(el).attr("tooltip"),
e
);
});
const fp = setting.desktop.path.asFileHandle();
// desktop default file manager
const desktop = $(workspace)[0] as tag.FloatListTag;
desktop.onready = function (e: tag.FloatListTag) {
e.observable = OS.announcer.observable;
window.onresize = function () {
announcer.trigger("desktopresize", undefined);
return e.calibrate();
};
desktop.onlistselect = function (d: TagEventType) {
($("#sysdock")[0] as tag.AppDockTag).selectedApp = null;
};
desktop.onlistdbclick = function (d: TagEventType) {
($("#sysdock")[0] as tag.AppDockTag).selectedApp = null;
const it = desktop.selectedItem;
return openWith(it.data as AppArgumentsType);
};
//($ "#workingenv").on "click", (e) ->
// desktop[0].set "selected", -1
$(desktop).on("click", function (e) {
let el = $(e.target).parent();
if (!(el.length > 0)) {
return;
}
el = el.parent();
if (!(el.length > 0)) {
return;
}
if (el[0] !== desktop) {
return;
}
desktop.unselect();
($("#sysdock")[0] as tag.AppDockTag).selectedApp = null;
});
desktop.contextmenuHandle = function (e, m) {
if (e.target.tagName.toUpperCase() === "UL") {
desktop.unselect();
}
($("#sysdock")[0] as tag.AppDockTag).selectedApp = null;
let menu = [
{ text: __("Open"), dataid: "desktop-open" },
{ text: __("Refresh"), dataid: "desktop-refresh" },
];
menu = menu.concat(
(() => {
const result = [];
for (let k in setting.desktop.menu) {
const v = setting.desktop.menu[k];
result.push(v);
}
return result;
})()
);
m.items = menu;
m.onmenuselect = function (evt: TagEventType) {
if(!evt.data || !evt.data.item) return;
const item = evt.data.item.data;
switch (item.dataid) {
case "desktop-open":
var it = desktop.selectedItem;
if (it) {
return openWith(
it.data as AppArgumentsType
);
}
let arg = setting.desktop.path.asFileHandle() as AppArgumentsType;
arg.mime = "dir";
arg.type = "dir";
return openWith(arg);
case "desktop-refresh":
return dkfetch(desktop);
default:
if (item.app) {
return launch(item.app, item.args);
}
}
};
return m.show(e);
};
dkfetch(desktop);
announcer.observable.on("VFS", function (d) {
if (["read", "publish", "download"].includes(d.data.m)) {
return;
}
if (
d.data.file.hash() === fp.hash() ||
d.data.file.parent().hash() === fp.hash()
) {
return dkfetch(desktop);
}
});
return announcer.ostrigger("desktoploaded", undefined);
};
// mount it
desktop.uify(undefined);
}
/**
*
*
* @export
*/
export function refreshDesktop(): void {
dkfetch($(workspace)[0] as tag.FloatListTag);
}
/**
*
*
* @export
*/
export function login(): void {
const scheme = $.parseHTML(schemes.login);
$("#wrapper").append(scheme);
$("#btlogin").click(async function () {
const data: API.UserLoginType = {
username: $("#txtuser").val() as string,
password: $("#txtpass").val() as string,
};
try {
const d = await API.handle.login(data);
if (d.error) {
return $("#login_error").html(d.error as string);
}
return startAntOS(d.result);
} catch (e) {
return $("#login_error").html("Login: server error");
}
});
$("#txtpass").keyup(function (e) {
if (e.which === 13) {
return $("#btlogin").click();
}
});
$("#txtuser").keyup(function (e) {
if (e.which === 13) {
return $("#btlogin").click();
}
});
}
/**
*
*
* @export
* @param {*} conf
*/
export function startAntOS(conf: any): void {
// clean up things
OS.cleanup();
// get setting from conf
OS.systemSetting(conf);
// load theme
loadTheme(setting.appearance.theme, true);
wallpaper(undefined);
OS.announcer.observable.one("syspanelloaded", async function () {
// TODO load packages list then build system menu
OS.announcer.observable.on("systemlocalechange", (name) =>
$("#syspanel")[0].update()
);
const ret = await API.packages.cache();
if (ret.result) {
return API.packages.fetch().then(function (r) {
let v: API.PackageMetaType;
if (r.result) {
const result = r.result as GenericObject<
API.PackageMetaType
>;
for (let k in result) {
v = result[k];
v.text = v.name;
v.filename = k;
v.type = "app";
v.mime = "antos/app";
if (v.icon) {
v.icon = `${v.path}/${v.icon}`;
}
if (!v.iconclass && !v.icon) {
v.iconclass = "fa fa-adn";
}
}
setting.system.packages = result
? result
: undefined;
}
// GUI.refreshSystemMenu()
// GUI.buildSystemMenu()
// push startup services
// TODO: get services list from user setting
pushServices(
(() => {
const result = [];
for (let v of Array.from(
setting.system.startup.services
)) {
result.push(v);
}
return result;
})()
);
return Array.from(
setting.system.startup.apps
).map((a) => launch(a, []));
});
}
});
//GUI.launch "DummyApp"
// initDM
API.setLocale(setting.system.locale).then(() => initDM());
Ant.OS.announcer.observable.on("error", function(d) {
console.log(d.data.e)
});
Ant.OS.announcer.observable.on("fail", function(d) {
console.log(d.data.e)
});
}
export const schemes: GenericObject<string> = {};
schemes.ws = `\
<afx-sys-panel id = "syspanel"></afx-sys-panel>
<div id = "workspace">
<afx-apps-dock id="sysdock"></afx-apps-dock>
<afx-float-list id = "desktop" dir="vertical" ></afx-float-list>
</div>
<afx-menu id="contextmenu" data-id="contextmenu" context="true" style="display:none;"></afx-menu>
<afx-label id="systooltip" data-id="systooltip" style="display:none;position:absolute;"></afx-label>
<textarea id="clipboard"></textarea>\
`;
schemes.login = `\
<div id = "login_form">
<p>Welcome to AntOS, please login</p>
<input id = "txtuser" type = "text" value = "demo" />
<input id = "txtpass" type = "password" value = "demo" />
<button id = "btlogin">Login</button>
<div id = "login_error"></div>
</div>\
`;
}
}