antosdk-apps/Blogger/main.ts

924 lines
39 KiB
TypeScript

// 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 application {
declare function renderMathInElement(el: HTMLElement):void;
declare var EasyMDE;
export class Blogger extends BaseApplication {
private user: GenericObject<any>;
private cvlist: GUI.tag.TreeViewTag;
private inputtags: HTMLInputElement;
private bloglist: GUI.tag.ListViewTag;
private seclist: GUI.tag.ListViewTag;
private tabcontainer: GUI.tag.TabContainerTag;
private editor: GenericObject<any>;
private previewOn: boolean;
// datatbase objects
private dbhandle: API.VFS.BaseFileHandle;
// database handles
private userdb: API.VFS.BaseFileHandle;
private cvcatdb: API.VFS.BaseFileHandle;
private cvsecdb: API.VFS.BaseFileHandle;
private blogdb: API.VFS.BaseFileHandle;
private subdb: API.VFS.BaseFileHandle;
private last_ctime: number;
constructor(args: any) {
super("Blogger", args);
this.previewOn = false;
}
private async init_db() {
try {
const f = await this.openDialog("FileDialog",{
title: __("Open/create new database"),
file: "Untitled.db"
});
var d_1 = f.file.path.asFileHandle();
if(f.file.type==="file") {
d_1=d_1.parent();
}
const target=`${d_1.path}/${f.name}`.asFileHandle();
this.dbhandle=`sqlite://${target.genealogy.join("/")}`.asFileHandle();
const tables = await this.dbhandle.read();
/**
* Init following tables if not exist:
* - user
* - cvcat
* - cvsec
* - blogdb
*/
if(!tables.user)
{
this.dbhandle.cache = {
address: "TEXT",
Phone: "TEXT",
shortbiblio: "TEXT",
fullname: "TEXT",
email: "TEXT",url: "TEXT",
photo: "TEXT"
}
const r = await this.dbhandle.write("user");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.cv_cat)
{
this.dbhandle.cache = {
publish: "NUMERIC",
name: "TEXT",
pid: "NUMERIC"
}
const r = await this.dbhandle.write("cv_cat");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.cv_sections)
{
this.dbhandle.cache = {
title: "TEXT",
start: "NUMERIC",
location: "TEXT",
end: "NUMERIC",
content: "TEXT",
subtitle: "TEXT",
publish: "NUMERIC",
cid: "NUMERIC"
}
const r = await this.dbhandle.write("cv_sections");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.blogs)
{
this.dbhandle.cache = {
tags: "TEXT",
content: "TEXT",
utime: "NUMERIC",
rendered: "TEXT",
title: "TEXT",
utimestr: "TEXT",
ctime: "NUMERIC",
ctimestr: "TEXT",
publish: "INTEGER DEFAULT 0",
}
const r = await this.dbhandle.write("blogs");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.st_similarity)
{
this.dbhandle.cache = {
pid: "NUMERIC",
sid: "NUMERIC",
score: "NUMERIC"
}
const r = await this.dbhandle.write("st_similarity");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.subscribers)
{
this.dbhandle.cache = {
name: "TEXT",
email: "TEXT"
}
const r = await this.dbhandle.write("subscribers");
if(r.error)
{
throw new Error(r.error as string);
}
}
this.userdb = `${this.dbhandle.path}@user`.asFileHandle();
this.cvcatdb = `${this.dbhandle.path}@cv_cat`.asFileHandle();
this.cvsecdb = `${this.dbhandle.path}@cv_sections`.asFileHandle();
this.blogdb = `${this.dbhandle.path}@blogs`.asFileHandle();
this.subdb = `${this.dbhandle.path}@subscribers`.asFileHandle();
this.last_ctime = 0;
this.bloglist.data = [];
this.loadBlogs();
}
catch(e) {
this.error(__("Unable to init database file: {0}",e.toString()),e);
this.dbhandle = undefined;
}
}
menu() {
return [
{
text: "__(Open/Create database)",
onmenuselect: (e) => {
this.init_db();
}
}
];
}
main() {
this.user = {};
this.cvlist = this.find("cv-list") as GUI.tag.TreeViewTag;
this.cvlist.ontreeselect = (d) => {
if (!d) { return; }
const {
data
} = d.data.item;
return this.CVSectionByCID(Number(data.id));
};
this.inputtags = this.find("input-tags") as HTMLInputElement;
this.bloglist = this.find("blog-list") as GUI.tag.ListViewTag;
this.seclist = this.find("cv-sec-list") as GUI.tag.ListViewTag;
let el = this.find("photo") as HTMLInputElement;
$(el)
.on("click", async (e: any) => {
try {
const ret = await this.openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
});
return el.value = ret.file.path;
} catch (e) {
return this.error(__("Unable to get file"), e);
}
});
this.tabcontainer = this.find("tabcontainer") as GUI.tag.TabContainerTag;
this.tabcontainer.ontabselect = (e) => {
return this.fetchData((e.data.container as GUI.tag.TileLayoutTag).aid);
};
(this.find("bt-user-save") as GUI.tag.ButtonTag).onbtclick = (e: any) => {
return this.saveUser();
};
(this.find("blog-load-more") as GUI.tag.ButtonTag).onbtclick = (e) => {
this.loadBlogs();
}
(this.find("cv-cat-add") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const tree = await this.fetchCVCat();
const d = await this.openDialog(new blogger.BloggerCategoryDialog(), {
title: __("Add category"),
tree
});
this.cvcatdb.cache = {
name: d.value,
pid: d.p.id,
publish: 1
};
const r = await this.cvcatdb.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.refreshCVCat();
}
catch(e)
{
this.error(__("cv-cat-add: {0}", e.toString()), e);
}
};
(this.find("cv-cat-edit") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.cvlist.selectedItem;
if (!sel) { return; }
const cat = sel.data;
if (!cat) { return; }
const tree = await this.fetchCVCat();
const d = await this.openDialog(new blogger.BloggerCategoryDialog(), {
title: __("Edit category"),
tree, cat
});
const handle = cat.$vfs;
handle.cache = {
id: cat.id,
publish: cat.publish,
pid: d.p.id,
name: d.value
};
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.refreshCVCat();
}
catch(e)
{
this.error(__("cv-cat-edit: {0}", e.toString()), e);
}
};
(this.find("cv-cat-del") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.cvlist.selectedItem;
if (!sel) { return; }
const cat = sel.data;
if (!cat) { return; }
const d = await this.openDialog("YesNoDialog", {
title: __("Delete category"),
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", cat.name)
});
if (!d) { return; }
await this.deleteCVCat(cat);
}
catch(e)
{
this.error(__("cv-cat-del: {0}", e.toString()), e);
}
};
(this.find("cv-sec-add") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try
{
const sel = this.cvlist.selectedItem;
if (!sel) { return; }
const cat = sel.data;
if (!cat || (cat.id === "0")) { return this.toast(__("Please select a category")); }
const d = await this.openDialog(new blogger.BloggerCVSectionDiaglog(), {
title: __("New section entry for {0}", cat.name)
});
d.cid = Number(cat.id);
d.start = Number(d.start);
d.end = Number(d.end);
this.cvsecdb.cache = d;
// d.publish = 1
const r = await this.cvsecdb.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.CVSectionByCID(Number(cat.id));
}
catch(e)
{
this.error(__("cv-sec-add: {0}", e.toString()), e);
}
};
(this.find("cv-sec-move") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.seclist.selectedItem;
if (!sel) { return this.toast(__("Please select a section to move")); }
const sec = sel.data;
const handle = sec.$vfs;
console.log(handle);
const tree = await this.fetchCVCat();
const d = await this.openDialog(new blogger.BloggerCategoryDialog(), {
title: __("Move to"),
tree,
selonly: true
});
handle.cache = {
id: sec.id,
cid: d.p.id
};
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.CVSectionByCID(sec.cid);
this.seclist.unselect();
}
catch(e)
{
this.error(__("cv-sec-move: {0}", e.toString()), e);
}
};
(this.find("cv-sec-edit") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.seclist.selectedItem;
if (!sel) { return this.toast(__("Please select a section to edit")); }
const sec = sel.data;
const d = await this.openDialog(new blogger.BloggerCVSectionDiaglog(), {
title: __("Modify section entry"),
section: sec
});
d.cid = Number(sec.cid);
d.start = Number(d.start);
d.end = Number(d.end);
const handle = sec.$vfs;
handle.cache = d;
//d.publish = Number sec.publish
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.CVSectionByCID(Number(sec.cid));
}
catch(e)
{
this.error(__("cv-sec-edit: {0}", e.toString()), e);
}
};
this.seclist.onitemclose = (evt) => {
if (!evt) { return; }
const data = evt.data.item.data;
this.openDialog("YesNoDialog", {
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", data.title)
}).then(async (b: any) => {
if (!b) { return; }
try {
const r = await this.cvsecdb.remove({
where: {
id: data.id
}
});
if(r.error)
{
throw new Error(r.error as string);
}
return this.seclist.delete(evt.data.item);
} catch(e) {
return this.error(__("Cannot delete the section: {0}",e.toString()),e);
}
});
return false;
};
this.editor = new EasyMDE({
element: this.find("markarea"),
autoDownloadFontAwesome: false,
autofocus: true,
tabSize: 4,
indentWithTabs: true,
toolbar: [
{
name: __("New"),
className: "fa fa-file",
action: (e: any) => {
this.bloglist.unselect();
return this.clearEditor();
}
},
{
name: __("Save"),
className: "fa fa-save",
action: (e: any) => {
return this.saveBlog();
}
}
, "|", "bold", "italic", "heading", "|", "quote", "code",
"unordered-list", "ordered-list", "|", "link",
"image", "table", "horizontal-rule",
{
name: "image",
className: "fa fa-file-image-o",
action: (_) => {
return this.openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
}).then((d) => {
return d.file.path.asFileHandle().publish()
.then((r: { result: any; }) => {
const doc = this.editor.codemirror.getDoc();
return doc.replaceSelection(`![](${this._api.handle.shared}/${r.result})`);
}).catch((e: any) => this.error(__("Cannot export file for embedding to text"), e));
});
}
},
{
name: "Youtube",
className: "fa fa-youtube",
action: (e: any) => {
const doc = this.editor.codemirror.getDoc();
return doc.replaceSelection("[[youtube:]]");
}
},
"|",
{
name: __("Preview"),
className: "fa fa-eye no-disable",
action: (e: any) => {
this.previewOn = !this.previewOn;
EasyMDE.togglePreview(e);
///console.log @select ".editor-preview editor-preview-active"
renderMathInElement(this.find("editor-container"));
}
},
"|",
{
name: __("Send mail"),
className: "fa fa-paper-plane",
action: async (e: any) => {
try {
const d = await this.subdb.read();
const sel = this.bloglist.selectedItem;
if (!sel) { return this.error(__("No post selected")); }
const data = sel.data;
await this.openDialog(new blogger.BloggerSendmailDiaglog(), {
title: __("Send mail"),
content: this.editor.value(),
mails: d,
id: data.id
});
this.toast(__("Emails sent"));
}
catch(e)
{
this.error(__("Error sending mails: {0}", e.toString()), e);
}
}
},
"|",
{
name: __("TFIDF analyse"),
className: "fa fa-area-chart",
action: async (e: any) => {
try {
const q = await this.openDialog("PromptDialog",{
title: __("TFIDF Analyse"),
text: __("Max number of related posts to keep per post?"),
value: "5"
});
const data = {
path: `${this.meta().path}/api/ai/analyse.lua`,
parameters: {
dbpath: this.dbhandle.info.file.path,
top: parseInt(q)
}
};
const d = await this._api.apigateway(data, false);
if (d.error) {
throw new Error(d.error);
}
this.toast(d.result);
}
catch(e)
{
this.error(__("Error analysing posts: {0}", e.toString()), e);
}
}
}
]
});
this.bloglist.onlistselect = async (e: any) => {
const el = this.bloglist.selectedItem;
if (!el) { return; }
const sel = el.data;
if (!sel) { return; }
try {
const result=await this.blogdb.read({
where: {
id: Number(sel.id)
}
});
if(!result || result.length == 0)
{
throw new Error(__("No record found for ID {}", sel.id).__());
}
const r = result[0];
this.editor.value(r.content);
this.inputtags.value=r.tags;
return (this.find("blog-publish") as GUI.tag.SwitchTag).swon=Number(r.publish)? true:false;
}
catch(e_1) {
return this.error(__("Cannot fetch the entry content"),e_1);
}
};
this.bloglist.onitemclose = (e) => {
if (!e) { return; }
const el = e.data.item;
const data = el.data;
this.openDialog("YesNoDialog", {
title: __("Delete a post"),
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete this post ?")
}).then(async (b: any) => {
if (!b) { return; }
const r = await this.blogdb.remove({
where: {
id: Number(data.id)
}
});
if(r.error)
{
throw new Error(r.error as string);
}
this.bloglist.delete(el);
this.bloglist.unselect();
return this.clearEditor();
});
return false;
};
this.bindKey("CTRL-S", () => {
const sel = this.tabcontainer.selectedTab;
if (!sel || ((sel.container as GUI.tag.TileLayoutTag).aid !== "blog-container")) { return; }
return this.saveBlog();
});
this.on("resize", () => {
return this.resizeContent();
});
this.resizeContent();
return this.init_db();
}
// @fetchData 0
// USER TAB
private fetchData(idx: any) {
switch (idx) {
case "user-container": //user info
return this.userdb.read()
.then((d) => {
if(!d || d.length == 0)
{
return;
}
this.user = d[0];
const inputs = this.select("[input-class='user-input']");
return inputs.map((i,v) => ($(v)).val(this.user[(v as HTMLInputElement).name]));
}).catch((e: any) => this.error(__("Cannot fetch user data"), e));
case "cv-container": // category
return this.refreshCVCat();
default:
this.last_ctime = 0;
this.bloglist.data = [];
return this.loadBlogs();
}
}
private async saveUser() {
try {
const inputs = this.select("[input-class='user-input']");
for (let v of inputs) { this.user[(v as HTMLInputElement).name] = ($(v)).val(); }
if (!this.user.fullname || (this.user.fullname === "")) { return this.toast(__("Full name must be entered")); }
//console.log @user
let fp = this.userdb;
if(this.user && this.user.id)
{
fp = `${this.userdb.path}@${this.user.id}`.asFileHandle();
}
fp.cache = this.user
const r = await fp.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
if(!this.user.id)
{
this.user.id = r.result;
}
this.toast(__("User data updated"));
}
catch(e)
{
this.error(__("Cannot save user data: {0}", e.toString()), e);
}
}
// PORFOLIO TAB
private refreshCVCat() {
return this.fetchCVCat().then((data: any) => {
this.cvlist.data = data;
return this.cvlist.expandAll();
}).catch((e: any) => this.error(__("Unable to load categories"), e));
}
private fetchCVCat() {
return new Promise(async (resolve, reject) => {
try {
const data = {
text: "Porfolio",
id: 0,
nodes: []
};
const filter = {
order: ["name$asc"]
};
const d = await this.cvcatdb.read(filter);
this.catListToTree(d, data, 0);
resolve(data);
}
catch(e)
{
reject(__e(e));
}
});
}
//it = (@cvlist.find "pid", "2")[0]
//@cvlist.set "selectedItem", it
private catListToTree(table: GenericObject<any>[], data: GenericObject<any>, id: number) {
const result = table.filter((e) => {
return e.pid == id
});
if (result.length === 0) {
return data.nodes = null;
}
for(let v of result)
{
v.nodes = [];
v.text = v.name;
this.catListToTree(table, v, v.id);
data.nodes.push(v);
}
}
private deleteCVCat(cat: GenericObject<any>): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
let v: any;
const ids = [];
var func = function (c: GenericObject<any>) {
ids.push(c.id);
if (c.nodes) {
c.nodes.map((v) => func(v));
}
};
func(cat);
// delete all content
let r = await this.cvsecdb.remove({
where: {
$or: {
cid: ids
}
}
});
if(r.error)
{
throw new Error(r.error as string);
}
r = await this.cvcatdb.remove({
where: {
$or: {
id: ids
}
}
});
if(r.error)
{
throw new Error(r.error as string);
}
await this.refreshCVCat();
this.seclist.data = [];
}
catch(e)
{
reject(__e(e));
}
});
}
private CVSectionByCID(cid: number): Promise<any> {
return new Promise( async (resolve, reject)=> {
try {
const d = await this.cvsecdb.read({
where: {cid},
order: [ "start$desc" ]
});
const items = [];
(this.find("cv-sec-status") as GUI.tag.LabelTag).text = __("Found {0} sections", d.length);
for (let v of d) {
v.closable = true;
v.tag = "afx-blogger-cvsection-item";
v.start = Number(v.start);
v.end = Number(v.end);
if (v.start < 1000) { v.start = undefined; }
if (v.end < 1000) { v.end = undefined; }
items.push(v);
}
this.seclist.data = items;
}
catch(e)
{
reject(__e(e));
}
});
}
// blog
private async saveBlog() {
try {
let sel = undefined;
const selel = this.bloglist.selectedItem;
if (selel) { sel = selel.data; }
const tags = this.inputtags.value;
const content = this.editor.value();
const title = (new RegExp("^#+(.*)\n", "g")).exec(content);
if (!title || (title.length !== 2)) { return this.toast(__("Please insert a title in the text: beginning with heading")); }
if (tags === "") { return this.toast(__("Please enter tags")); }
const d = new Date();
const data: GenericObject<any> = {
content,
title: title[1].trim(),
tags,
ctime: sel ? sel.ctime : d.timestamp(),
ctimestr: sel ? sel.ctimestr : d.toString(),
utime: d.timestamp(),
utimestr: d.toString(),
rendered: this.process(this.editor.options.previewRender(content)),
publish: (this.find("blog-publish") as GUI.tag.SwitchTag).swon ? 1 : 0
};
let handle = this.blogdb;
if (sel) {
data.id = sel.id;
handle = sel.$vfs;
}
//save the data
handle.cache = data;
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
if(!sel)
{
this.last_ctime = 0;
this.bloglist.data = [];
await this.loadBlogs();
}
else
{
//data.text = data.title;
selel.data = data;
}
}
catch(e)
{
this.error(__("Cannot save blog: {0}", e.toString()), e);
}
}
private process(text: string) {
// find video tag and rendered it
let found: any;
const embed = (id: any) => `\
<iframe
class = "embeded-video"
width="560" height="315"
src="https://www.youtube.com/embed/${id}"
frameborder="0" allow="encrypted-media" allowfullscreen
></iframe>\
`;
const re = /\[\[youtube:([^\]]*)\]\]/g;
const replace = [];
while ((found = re.exec(text)) !== null) {
replace.push(found);
}
if (!(replace.length > 0)) { return text; }
let ret = "";
let begin = 0;
for (let it of replace) {
ret += text.substring(begin, it.index);
ret += embed(it[1]);
begin = it.index + it[0].length;
}
ret += text.substring(begin, text.length);
//console.log ret
return ret;
}
private clearEditor() {
this.editor.value("");
this.inputtags.value = "";
return (this.find("blog-publish") as GUI.tag.SwitchTag).swon = false;
}
// load blog
private loadBlogs(): Promise<any> {
return new Promise( async (ok, reject)=> {
try {
const filter: GenericObject<any> = {
order: ["ctime$desc"],
fields: [
"id",
"title",
"ctimestr",
"ctime",
"utime",
"utimestr"
],
limit: 10,
};
if(this.last_ctime)
{
filter.where = { ctime$lt: this.last_ctime};
}
const r = await this.blogdb.read(filter);
if(r.length == 0)
{
this.toast(__("No more record to load"));
return;
}
this.last_ctime = r[r.length - 1].ctime;
for (let v of r) {
v.tag = "afx-blogger-post-item";
this.bloglist.push(v);
}
this.clearEditor();
return this.bloglist.selected = -1;
}
catch(e)
{
reject(__e(e));
}
});
}
private resizeContent() {
const container = this.find("editor-container");
const children = ($(".EasyMDEContainer", container)).children();
const titlebar = (($(this.scheme)).find(".afx-window-top"))[0];
const toolbar = children[0];
const statusbar = children[3];
const cheight = ($(this.scheme)).height() - ($(titlebar)).height() - ($(toolbar)).height() - ($(statusbar)).height() - 90;
return ($(children[1])).css("height", cheight + "px");
}
}
Blogger.singleton = true;
Blogger.dependencies = [
"pkg://SimpleMDE/main.js",
"pkg://SimpleMDE/main.css",
"pkg://Katex/main.js",
"pkg://Katex/main.css",
"pkg://SQLiteDB/libsqlite.js",
];
}
}