Blogger: migrate code to typescript,use SQLiteDB as database access API

This commit is contained in:
DanyLE 2023-02-23 16:59:33 +01:00
parent c8ddd5ec6e
commit 5dec0a2b56
24 changed files with 1355 additions and 2029 deletions

View File

@ -6,6 +6,7 @@ Blackend for my blog at https://blog.iohub.dev
## Change logs
### v0.2.x-a
* Patch 10: Migrate code to typescript, use SQLiteDB lib for database access
* Patch 9: Update to use the new MDE library
* Patch 8: Support for antOS 2.0.x
* Patch 7: Fix sendmail API security bug

View File

@ -6,6 +6,7 @@ Blackend for my blog at https://blog.iohub.dev
## Change logs
### v0.2.x-a
* Patch 10: Migrate code to typescript, use SQLiteDB lib for database access
* Patch 9: Update to use the new MDE library
* Patch 8: Support for antOS 2.0.x
* Patch 7: Fix sendmail API security bug

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version": "0.2.9-a",
"version": "0.2.10-a",
"category": "Internet",
"iconclass": "fa fa-book",
"dependencies": [
@ -36,7 +36,6 @@
"No email selected": "No email selected",
"Unable to send mail to: {0}": "Unable to send mail to: {0}",
"Error sending mail: {0}": "Error sending mail: {0}",
"Cannot fetch subscribers data: {0}": "Cannot fetch subscribers data: {0}",
"Open/create new database": "Open/create new database",
"Unable to init database file: {0}": "Unable to init database file: {0}",
"Select image file": "Select image file",
@ -65,28 +64,20 @@
"No post selected": "No post selected",
"Emails sent": "Emails sent",
"Error sending mails: {0}": "Error sending mails: {0}",
"No record found for ID {}": "No record found for ID {}",
"Cannot fetch the entry content": "Cannot fetch the entry content",
"Delete a post": "Delete a post",
"Do you really want to delete this post ?": "Do you really want to delete this post ?",
"Cannot fetch user data": "Cannot fetch user data",
"Full name must be entered": "Full name must be entered",
"User data updated": "User data updated",
"Cannot save user data": "Cannot save user data",
"Cannot save user data: {0}": "Cannot save user data: {0}",
"Unable to load categories": "Unable to load categories",
"Found {0} sections": "Found {0} sections",
"Please insert a title in the text: beginning with heading": "Please insert a title in the text: beginning with heading",
"Please enter tags": "Please enter tags",
"Cannot save blog: {0}": "Cannot save blog: {0}",
"Cannot add new category": "Cannot add new category",
"Unable to fetch categories": "Unable to fetch categories",
"Cannot Edit category": "Cannot Edit category",
"Cannot save section: {0}": "Cannot save section: {0}",
"Cannot move section": "Cannot move section",
"Cannot delete the category: {0} [{1}]": "Cannot delete the category: {0} [{1}]",
"Cannot delete all content of: {0} [{1}]": "Cannot delete all content of: {0} [{1}]",
"No post found: {0}": "No post found: {0}",
"Created: {0}": "Created: {0}",
"Updated: {0}": "Updated: {0}",
"No more record to load": "No more record to load",
"Full name": "Full name",
"Address": "Address",
"Phone": "Phone",
@ -95,7 +86,10 @@
"Photo": "Photo",
"Short biblio": "Short biblio",
"Categories": "Categories",
"Tags": "Tags"
"Load more": "Load more",
"Tags": "Tags",
"Created: {0}": "Created: {0}",
"Updated: {0}": "Updated: {0}"
}
}
}

View File

@ -1,5 +1,5 @@
<afx-app-window data-id = "blogger-win" apptitle="Blogger" width="650" height="500">
<afx-hbox >
<afx-hbox padding="5">
<afx-tab-container data-id = "tabcontainer" dir = "row" tabbarwidth= "40">
<afx-hbox data-id="user-container" data-height="100%" iconclass="fa fa-user-circle">
@ -65,7 +65,10 @@
<afx-hbox data-id = "blog-container" data-height="100%" iconclass="fa fa-book">
<afx-list-view data-id = "blog-list" min-width="100" data-width="200"></afx-list-view>
<afx-vbox>
<afx-list-view data-id = "blog-list" min-width="100" data-width="200"></afx-list-view>
<afx-button data-id = "blog-load-more" text = "__(Load more)" iconclass_end = "bi bi-chevron-double-right" data-height="content"></afx-button>
</afx-vbox>
<afx-resizer data-width = "3"></afx-resizer>
<afx-vbox>
<div data-id = "editor-container">

Binary file not shown.

View File

@ -1,30 +0,0 @@
<afx-app-window data-id = "blogger-cv-sec-win" apptitle="Porforlio section" width="450" height="400">
<afx-vbox padding="5">
<afx-hbox data-height = "30" >
<afx-label data-width= "70" text = "__(Title)"></afx-label>
<input type = "text" name="title" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(Subtitle)" data-width= "70"></afx-label>
<input type = "text" name="subtitle" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(Location)" data-width= "70"></afx-label>
<input type = "text" name="location" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(From)" data-width= "70"></afx-label>
<input type = "text" name="start" input-class = "user-input"></input>
<afx-label text = "To:" style="text-align:center;" data-width= "70"></afx-label>
<input type = "text" name="end" input-class = "user-input"></input>
</afx-hbox>
<afx-label data-height = "30" text = "Content" style = "margin-left:5px;"></afx-label>
<div data-id="editor-container">
<textarea name="content" data-id = "contentarea" ></textarea>
</div>
<div data-height = "35" style="text-align: right;">
<afx-switch data-id = "section-publish" data-width="30"></afx-switch>
<afx-button iconclass = "fa fa-save" data-id = "bt-cv-sec-save" text = "__(Save)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>

View File

@ -1,189 +0,0 @@
# 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/.
class BloggerCategoryDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "BloggerCategoryDialog", BloggerCategoryDialog.scheme
main: () ->
super.main()
@tree = @find "tree"
@txtinput = @find "txtinput"
(@find "bt-ok").onbtclick = (e) =>
sel = @tree.selectedItem
return @notify __("Please select a parent category") unless sel
seldata = sel.data
val = @txtinput.value
return @notify __("Please enter category name") if val is "" and not @data.selonly
return @notify __("Parent can not be the category itself") if @data.cat and @data.cat.id is seldata.id
@handle { p: seldata, value: val } if @handle
@quit()
(@find "bt-cancel").onbtclick = (e) =>
@quit()
if @data and @data.tree
if @data and @data.cat
@txtinput.value = @data.cat.name
if @data.cat.pid is "0"
seldata = @data.tree
else
seldata = @findDataByID @data.cat.pid, @data.tree.nodes
seldata.selected = true if seldata
@tree.data = @data.tree
@tree.expandAll()
# TODO set selected category name
findDataByID: (id, list) ->
for data in list
return data if data.id is id
if data.nodes
@findDataByID id, data.nodes
return undefined
BloggerCategoryDialog.scheme = """
<afx-app-window width='300' height='400'>
<afx-vbox padding="5">
<afx-label text="__(Pick a parent)" data-height="25" class="lbl-header" ></afx-label>
<afx-tree-view data-id="tree" ></afx-tree-view>
<afx-label text="__(Category name)" data-height="25" class="lbl-header" ></afx-label>
<input type="text" data-height="25" data-id = "txtinput"/ >
<afx-hbox data-height = '35'>
<div style=' text-align:right;'>
<afx-button data-id = "bt-ok" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>
"""
# This dialog is use for cv section editing
class BloggerCVSectionDiaglog extends this.OS.GUI.BasicDialog
constructor: (parent) ->
file = "#{parent.meta().path}/cvsection.html".asFileHandle()
super "BloggerCVSectionDiaglog", file
main: () ->
super.main()
@editor = new EasyMDE
autoDownloadFontAwesome: false
element: @find "contentarea"
status: false
toolbar: false
($ (@select '[class = "CodeMirror-scroll"]')[0]).css "min-height", "50px"
($ (@select '[class="CodeMirror cm-s-paper CodeMirror-wrap"]')[0]).css "min-height", "50px"
inputs = @select "[input-class='user-input']"
(($ v).val @data.section[v.name] for v in inputs ) if @data and @data.section
@editor.value @data.section.content if @data and @data.section
(@find "section-publish").swon = (if @data and @data.section and Number(@data.section.publish) then true else false)
(@find "bt-cv-sec-save").onbtclick = (e) =>
data = {}
data[v.name] = ($ v).val() for v in inputs
data.content = @editor.value()
return @notify __("Title or content must not be blank") if data.title is "" and data.content is ""
#return @notify "Content must not be blank" if data.content is ""
data.id = @data.section.id if @data and @data.section
val = (@find "section-publish").swon
if val is true
data.publish = 1
else
data.publish = 0
@handle data if @handle
@quit()
@on "resize", () => @resizeContent()
@resizeContent()
resizeContent: () ->
container = @find "editor-container"
children = ($ ".EasyMDEContainer", container).children()
cheight = ($ container).height() - 30
($ children[0]).css("height", cheight + "px")
# this dialog is for send mail
class BloggerSendmailDiaglog extends this.OS.GUI.BasicDialog
constructor: (parent) ->
file = "#{parent.meta().path}/sendmail.html".asFileHandle()
super "BloggerSendmailDiaglog", file
main: () ->
super.main()
@subdb = new @.parent._api.DB("subscribers")
@maillinglist = @find "email-list"
title = (new RegExp "^#+(.*)\n", "g").exec @data.content
(@find "mail-title").value = title[1]
content = (@data.content.substring 0, 500) + "..."
(@find "contentarea").value = BloggerSendmailDiaglog.template.format @data.id, content
@subdb.find {}
.then (d) =>
for v in d
v.text = v.name
v.switch = true
v.checked = true
@maillinglist. items = d
.catch (e) =>
@error __("Cannot fetch subscribers data: {0}", e.toString()), e
(@find "bt-sendmail").onbtclick = (e) =>
items = @maillinglist.items
emails = []
for v in items
if v.checked is true
console.log v.email
emails.push v.email
return @notify __("No email selected") if emails.length is 0
# send the email
data =
path: "#{@parent.path()}/sendmail.lua",
parameters:
to: emails,
title: (@find "mail-title").value,
content: (@find "contentarea").value
@_api.apigateway data, false
.then (d) =>
return @notify __("Unable to send mail to: {0}", d.result.join(",")) if d.error
@quit()
.catch (e) =>
console.log e
@error __("Error sending mail: {0}", e.toString()), e
BloggerSendmailDiaglog.template = """
Hello,
Xuan Sang LE has just published a new post on his blog: https://blog.lxsang.me/post/id/{0}
==========
{1}
==========
Read the full article via:
https://blog.lxsang.me/post/id/{0}
You receive this email because you have been subscribed to his blog.
Have a nice day,
Sent from Blogger, an AntOS application
"""

278
Blogger/dialogs.ts Normal file
View File

@ -0,0 +1,278 @@
// 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 {
export namespace blogger {
declare var EasyMDE;
export class BloggerCategoryDialog extends OS.GUI.BasicDialog {
private tree: OS.GUI.tag.TreeViewTag;
private txtinput: HTMLInputElement;
constructor() {
super("BloggerCategoryDialog", BloggerCategoryDialog.scheme);
}
main() {
super.main();
this.tree = this.find("tree") as OS.GUI.tag.TreeViewTag;
this.txtinput = this.find("txtinput") as HTMLInputElement;
(this.find("bt-ok") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
const sel = this.tree.selectedItem;
if (!sel) { return this.notify(__("Please select a parent category")); }
const seldata = sel.data;
const val = this.txtinput.value;
if ((val === "") && !this.data.selonly) { return this.notify(__("Please enter category name")); }
if (this.data.cat && (this.data.cat.id === seldata.id)) { return this.notify(__("Parent can not be the category itself")); }
if (this.handle) { this.handle({ p: seldata, value: val }); }
return this.quit();
};
(this.find("bt-cancel") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
return this.quit();
};
if (this.data && this.data.tree) {
if (this.data && this.data.cat) {
let seldata: GenericObject<any>;
this.txtinput.value = this.data.cat.name;
if (this.data.cat.pid === "0") {
seldata = this.data.tree;
} else {
seldata = this.findDataByID(this.data.cat.pid, this.data.tree.nodes);
}
if (seldata) { seldata.selected = true; }
}
this.tree.data = this.data.tree;
return this.tree.expandAll();
}
}
// TODO set selected category name
findDataByID(id: number, list: GenericObject<any>[]) {
for (let data of list) {
if (data.id === id) { return data; }
if (data.nodes) {
this.findDataByID(id, data.nodes);
}
}
return undefined;
}
}
BloggerCategoryDialog.scheme = `\
<afx-app-window width='300' height='400'>
<afx-vbox padding="5">
<afx-label text="__(Pick a parent)" data-height="25" class="lbl-header" ></afx-label>
<afx-tree-view data-id="tree" ></afx-tree-view>
<afx-label text="__(Category name)" data-height="25" class="lbl-header" ></afx-label>
<input type="text" data-height="25" data-id = "txtinput"/ >
<afx-hbox data-height = '35'>
<div style=' text-align:right;'>
<afx-button data-id = "bt-ok" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>\s
`;
// This dialog is use for cv section editing
export class BloggerCVSectionDiaglog extends OS.GUI.BasicDialog {
private editor: GenericObject<any>;
constructor() {
super("BloggerCVSectionDiaglog");
}
main() {
super.main();
this.editor = new EasyMDE({
autoDownloadFontAwesome: false,
element: this.find("contentarea"),
status: false,
toolbar: false
});
($((this.select('[class = "CodeMirror-scroll"]'))[0])).css("min-height", "50px");
($((this.select('[class="CodeMirror cm-s-paper CodeMirror-wrap"]'))[0])).css("min-height", "50px");
const inputs = this.select("[input-class='user-input']");
if (this.data && this.data.section) { for (let v of inputs) { ($(v)).val(this.data.section[(v as HTMLInputElement).name]); } }
if (this.data && this.data.section) { this.editor.value(this.data.section.content); }
(this.find("section-publish") as OS.GUI.tag.SwitchTag).swon = (this.data && this.data.section && Number(this.data.section.publish) ? true : false);
(this.find("bt-cv-sec-save") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
const data: GenericObject<any> = {};
for (let v of inputs) { data[(v as HTMLInputElement).name] = ($(v)).val(); }
data.content = this.editor.value();
if ((data.title === "") && (data.content === "")) { return this.notify(__("Title or content must not be blank")); }
//return @notify "Content must not be blank" if data.content is ""
if (this.data && this.data.section) { data.id = this.data.section.id; }
const val = (this.find("section-publish") as OS.GUI.tag.SwitchTag).swon;
if (val === true) {
data.publish = 1;
} else {
data.publish = 0;
}
if (this.handle) { this.handle(data); }
return this.quit();
};
this.on("resize", () => this.resizeContent());
return this.resizeContent();
}
resizeContent() {
const container = this.find("editor-container");
const children = ($(".EasyMDEContainer", container)).children();
const cheight = ($(container)).height() - 30;
return ($(children[0])).css("height", cheight + "px");
}
}
BloggerCVSectionDiaglog.scheme = `\
<afx-app-window data-id = "blogger-cv-sec-win" apptitle="Porforlio section" width="450" height="400">
<afx-vbox padding="5">
<afx-hbox data-height = "30" >
<afx-label data-width= "70" text = "__(Title)"></afx-label>
<input type = "text" name="title" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(Subtitle)" data-width= "70"></afx-label>
<input type = "text" name="subtitle" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(Location)" data-width= "70"></afx-label>
<input type = "text" name="location" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(From)" data-width= "70"></afx-label>
<input type = "text" name="start" input-class = "user-input"></input>
<afx-label text = "To:" style="text-align:center;" data-width= "70"></afx-label>
<input type = "text" name="end" input-class = "user-input"></input>
</afx-hbox>
<afx-label data-height = "30" text = "Content" style = "margin-left:5px;"></afx-label>
<div data-id="editor-container">
<textarea name="content" data-id = "contentarea" ></textarea>
</div>
<div data-height = "35" style="text-align: right;">
<afx-switch data-id = "section-publish" data-width="30"></afx-switch>
<afx-button iconclass = "fa fa-save" data-id = "bt-cv-sec-save" text = "__(Save)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>`;
// this dialog is for send mail
export class BloggerSendmailDiaglog extends OS.GUI.BasicDialog {
static template: string;
private maillinglist: OS.GUI.tag.ListViewTag;
// TODO: convert to SQLite handle
private subdb: API.VFS.BaseFileHandle;
constructor() {
super("BloggerSendmailDiaglog");
}
main() {
super.main();
this.maillinglist = this.find("email-list") as OS.GUI.tag.ListViewTag;
const title = (new RegExp("^#+(.*)\n", "g")).exec(this.data.content);
(this.find("mail-title") as HTMLInputElement).value = title[1];
const content = (this.data.content.substring(0, 500)) + "...";
(this.find("contentarea") as HTMLTextAreaElement).value = BloggerSendmailDiaglog.template.format(this.data.id, content);
const mlist = this.data.mails.map((el)=> {
return {
text: el.name,
email: el.email,
switch: true,
checked: true
}
})
this.maillinglist.data = mlist;
return (this.find("bt-sendmail") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
const items = this.maillinglist.data;
const emails = [];
for (let v of items) {
if (v.checked === true) {
console.log(v.email);
emails.push(v.email);
}
}
if (emails.length === 0) { return this.notify(__("No email selected")); }
// send the email
const data = {
path: `${this.meta().path}/sendmail.lua`,
parameters: {
to: emails,
title: (this.find("mail-title") as HTMLInputElement).value,
content: (this.find("contentarea") as HTMLTextAreaElement).value
}
};
return this._api.apigateway(data, false)
.then((d: { error: any; result: { join: (arg0: string) => any; }; }) => {
if (d.error) { return this.notify(__("Unable to send mail to: {0}", d.result.join(","))); }
return this.quit();
}).catch((e) => {
console.log(e);
return this.error(__("Error sending mail: {0}", e.toString()), e);
});
};
}
}
BloggerSendmailDiaglog.scheme = `\
<afx-app-window data-id = "blogger-send-mail-win" apptitle="Send mail" width="500" height="400" resizable = "false">
<afx-hbox>
<afx-menu data-width="150" data-id="email-list"></afx-menu>
<afx-resizer data-width="3"></afx-resizer>
<div data-width="5"></div>
<afx-vbox >
<div data-height="5"></div>
<afx-label data-height="20" text = "__(Title)"></afx-label>
<input type = "text" data-height="20" name="title" data-id = "mail-title"></input>
<afx-label data-height = "20" text = "Content" ></afx-label>
<textarea name="content" data-id = "contentarea" ></textarea>
<div data-height="5"></div>
<afx-hbox data-height = "30">
<div></div>
<afx-button iconclass = "fa fa-paper-plane" data-id = "bt-sendmail" data-width="60" text = "__(Send)"></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width="5"></div>
</afx-hbox>
</afx-app-window>`;
BloggerSendmailDiaglog.template = `\
Hello,
Xuan Sang LE has just published a new post on his blog: https://blog.iohub.dev/post/id/{0}
==========
{1}
==========
Read the full article via:
https://blog.iohub.dev/post/id/{0}
You receive this email because you have been subscribed to his blog.
Have a nice day,
Sent from Blogger, an AntOS application\
`;
}
}
}

View File

@ -1,506 +0,0 @@
# 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/.
class Blogger extends this.OS.application.BaseApplication
constructor: (args) ->
super "Blogger", args
main: () ->
@user = {}
@cvlist = @find "cv-list"
@cvlist.ontreeselect = (d) =>
return unless d
data = d.data.item.data
@CVSectionByCID Number(data.id)
@inputtags = @.find "input-tags"
@bloglist = @find "blog-list"
@seclist = @find "cv-sec-list"
el = @find("photo")
$(el)
.click (e) =>
@openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
})
.then (d) =>
el.value = d.file.path
.catch (e) => @error __("Unable to get file"), e
@userdb = new @_api.DB("user")
@cvcatdb = new @_api.DB("cv_cat")
@cvsecdb = new @_api.DB("cv_sections")
@blogdb = new @_api.DB("blogs")
@tabcontainer = @find "tabcontainer"
@tabcontainer.ontabselect = (e) =>
@fetchData e.data.container.aid
(@find "bt-user-save").onbtclick = (e) =>
@saveUser()
(@find "cv-cat-add").onbtclick = (e) =>
fn = (tree) =>
@openDialog(new BloggerCategoryDialog(), {
title: __("Add category"),
tree: tree
}).then (d) =>
c =
name: d.value,
pid: d.p.id,
publish: 1
@cvcatdb.save c
.then (r) =>
@refreshCVCat()
.catch (e) => @error __("Cannot add new category"), e
.catch (e) => @error e.toString(), e
@fetchCVCat()
.then (tree) => fn(tree)
.catch (e) =>
data =
text: "Porfolio",
id:"0",
nodes: []
fn(data)
@error __("Unable to fetch categories"), e
(@find "cv-cat-edit").onbtclick = (e) =>
sel = @cvlist.selectedItem
return unless sel
cat = sel.data
return unless cat
@fetchCVCat().then (tree) =>
@openDialog(new BloggerCategoryDialog(), {
title: __("Edit category"),
tree: tree, cat: cat
}).then (d) =>
c =
id: cat.id,
publish: cat.publish,
pid: d.p.id,
name: d.value
@cvcatdb.save c
.then (r) =>
@refreshCVCat()
.catch (e) =>
@error __("Cannot Edit category"), e
.catch (e) => @error __("Unable to fetch categories"), e
(@find "cv-cat-del").onbtclick = (e) =>
sel = @cvlist.selectedItem
return unless sel
cat = sel.data
return unless cat
@openDialog("YesNoDialog", {
title: __("Delete category") ,
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", cat.name)
}).then (d) =>
return unless d
@deleteCVCat cat
.catch (e) => @error e.toString(), e
(@find "cv-sec-add").onbtclick = (e) =>
sel = @cvlist.selectedItem
return unless sel
cat = sel.data
return @notify __("Please select a category") unless cat and cat.id isnt "0"
@openDialog(new BloggerCVSectionDiaglog(@), {
title: __("New section entry for {0}", cat.name)
}).then (d) =>
d.cid = Number cat.id
d.start = Number d.start
d.end = Number d.end
# d.publish = 1
@cvsecdb.save d
.then (r) =>
@CVSectionByCID Number(cat.id)
.catch (e) => @error __("Cannot save section: {0}", e.toString()), e
(@find "cv-sec-move").onbtclick = (e) =>
sel = (@find "cv-sec-list").selectedItem
return @notify __("Please select a section to move") unless sel
sec = sel.data
@fetchCVCat().then (tree) =>
@openDialog(new BloggerCategoryDialog(),{
title: __("Move to"),
tree: tree,
selonly: true
}).then (d) =>
c =
id: sec.id,
cid: d.p.id
@cvsecdb.save c
.then (r) =>
@CVSectionByCID(sec.cid)
(@find "cv-sec-list").unselect()
.catch (e) => @error __("Cannot move section"), e
(@find "cv-sec-edit").onbtclick = (e) =>
sel = (@find "cv-sec-list").selectedItem
return @notify __("Please select a section to edit") unless sel
sec = sel.data
@openDialog(new BloggerCVSectionDiaglog(@), {
title: __("Modify section entry"),
section: sec
}).then (d) =>
d.cid = Number sec.cid
d.start = Number d.start
d.end = Number d.end
#d.publish = Number sec.publish
@cvsecdb.save d
.then (r) =>
@CVSectionByCID Number(sec.cid)
.catch (e) => return @error __("Cannot save section: {0}", e.toString()), e
@seclist.onitemclose = (e) =>
return unless e
data = e.data.item.data
@openDialog("YesNoDialog", {
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", data.title)
}).then (b) =>
return unless b
@cvsecdb.delete data.id
.then (r) =>
@seclist.delete e.data.item
.catch (e) => @error __("Cannot delete the section: {0}", e.toString()), e
return false
@editor = new EasyMDE
element: @find "markarea"
autoDownloadFontAwesome: false
autofocus: true
tabSize: 4
indentWithTabs: true
toolbar: [
{
name: __("New"),
className: "fa fa-file",
action: (e) =>
@bloglist.unselect()
@clearEditor()
},
{
name: __("Save"),
className: "fa fa-save",
action: (e) =>
@saveBlog()
}
, "|", "bold", "italic", "heading", "|", "quote", "code",
"unordered-list", "ordered-list", "|", "link",
"image", "table", "horizontal-rule",
{
name: "image",
className: "fa fa-file-image-o",
action: (e) =>
@openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
}).then (d) =>
d.file.path.asFileHandle().publish()
.then (r) =>
doc = @editor.codemirror.getDoc()
doc.replaceSelection "![](#{@_api.handle.shared}/#{r.result})"
.catch (e) => @error __("Cannot export file for embedding to text"), e
},
{
name:"Youtube",
className: "fa fa-youtube",
action: (e) =>
doc = @editor.codemirror.getDoc()
doc.replaceSelection "[[youtube:]]"
}
"|",
{
name: __("Preview"),
className: "fa fa-eye no-disable",
action: (e) =>
@previewOn = !@previewOn
EasyMDE.togglePreview e
#/console.log @select ".editor-preview editor-preview-active"
renderMathInElement @find "editor-container"
},
"|",
{
name: __("Send mail"),
className: "fa fa-paper-plane",
action: (e) =>
sel = @bloglist.selectedItem
return @error __("No post selected") unless sel
data = sel.data
@openDialog(new BloggerSendmailDiaglog(@), {
title: __("Send mail"),
content: @editor.value(),
id: data.id
})
.then (d) ->
console.log "Email sent"
}
],
@bloglist.onlistselect = (e) =>
el = @bloglist.selectedItem
return unless el
sel = el.data
return unless sel
@blogdb.get Number(sel.id)
.then (r) =>
@editor.value r.content
@inputtags.value = r.tags
(@find "blog-publish").swon = if Number(r.publish) then true else false
.catch (e) =>
@error __("Cannot fetch the entry content"), e
@bloglist.onitemclose = (e) =>
return unless e
el = e.data.item
data = el.data
@openDialog("YesNoDialog", {
title: __("Delete a post"),
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete this post ?")
}).then (b) =>
return unless b
@blogdb.delete data.id
.then (r) =>
@bloglist.delete el
@bloglist.unselect()
@clearEditor()
return false
@bindKey "CTRL-S", () =>
sel = @tabcontainer.selectedTab
return unless sel and sel.container.aid is "blog-container"
@saveBlog()
@on "resize", () =>
@resizeContent()
@resizeContent()
@loadBlogs()
# @fetchData 0
# USER TAB
fetchData: (idx) ->
switch idx
when "user-container" #user info
@userdb.get null
.then (d) =>
@user = d[0]
inputs = @select "[input-class='user-input']"
($ v).val @user[v.name] for v in inputs
.catch (e) => @error __("Cannot fetch user data"), e
when "cv-container" # category
@refreshCVCat()
else
@loadBlogs()
saveUser:() ->
inputs = @select "[input-class='user-input']"
@user[v.name] = ($ v).val() for v in inputs
return @notify __("Full name must be entered") if not @user.fullname or @user.fullname is ""
#console.log @user
@userdb.save @user
.then (r) =>
return @notify __("User data updated")
.catch (e) => return @error __("Cannot save user data"), e
# PORFOLIO TAB
refreshCVCat: () ->
@fetchCVCat().then (data) =>
@cvlist.data = data
@cvlist.expandAll()
.catch (e) => @error __("Unable to load categories"), e
fetchCVCat: () ->
new Promise (resolve, reject) =>
data =
text: "Porfolio",
id:"0",
nodes: []
cnd =
order:
name: "ASC"
@cvcatdb.find cnd
.then (d) =>
@catListToTree d, data, "0"
resolve data
.catch (e) -> reject __e e
#it = (@cvlist.find "pid", "2")[0]
#@cvlist.set "selectedItem", it
catListToTree: (table, data, id) ->
result = (v for v in table when v.pid is id)
return data.nodes = null if result.length is 0
for v in result
v.nodes = []
v.text = v.name
@catListToTree table, v, v.id
#v.nodes = null if v.nodes.length is 0
data.nodes.push v
deleteCVCat: (cat) ->
me = @
ids = []
func = (c) ->
ids.push c.id
func(v) for v in c.nodes if c.nodes
func(cat)
cond = ({ "=": { cid: v } } for v in ids)
# delete all content
@cvsecdb.delete({ "or": cond }).then (r) =>
cond = ({ "=": { id: v } } for v in ids)
@cvcatdb.delete({ "or": cond }).then (re) =>
@refreshCVCat()
@seclist.data=[]
.catch (e) =>
@error __("Cannot delete the category: {0} [{1}]", cat.name, e.toString()), e
.catch (e) =>
@error __("Cannot delete all content of: {0} [{1}]", cat.name, e.toString()), e
CVSectionByCID: (cid) ->
cond =
exp:
"=":
cid: cid
order:
start: "DESC"
@cvsecdb.find(cond).then (d) =>
items = []
(@find "cv-sec-status").text = __("Found {0} sections", d.length)
for v in d
v.closable = true
v.tag = "afx-blogger-cvsection-item"
v.start = Number(v.start)
v.end = Number(v.end)
v.start = undefined if v.start < 1000
v.end = undefined if v.end < 1000
items.push v
@seclist.data = items
.catch (e) => @error e.toString(), e
# blog
saveBlog: () ->
sel = undefined
selel = @bloglist.selectedItem
sel = selel.data if selel
tags = @inputtags.value
content = @editor.value()
title = (new RegExp "^#+(.*)\n", "g").exec content
return @notify __("Please insert a title in the text: beginning with heading") unless title and title.length is 2
return @notify __("Please enter tags") if tags is ""
d = new Date()
data =
content: content
title: title[1].trim()
tags: tags
ctime: if sel then sel.ctime else d.timestamp()
ctimestr: if sel then sel.ctimestr else d.toString()
utime: d.timestamp()
utimestr: d.toString()
rendered: @process(@editor.options.previewRender(content))
publish: if (@find "blog-publish").swon then 1 else 0
data.id = sel.id if sel
#save the data
@blogdb.save data
.then (r) =>
@loadBlogs()
.catch (e) => @error __("Cannot save blog: {0}", e.toString()), e
process: (text) ->
# find video tag and rendered it
embed = (id) ->
return """
<iframe
class = "embeded-video"
width="560" height="315"
src="https://www.youtube.com/embed/#{id}"
frameborder="0" allow="encrypted-media" allowfullscreen
></iframe>
"""
re = /\[\[youtube:([^\]]*)\]\]/g
replace = []
while (found = re.exec text) isnt null
replace.push found
return text unless replace.length > 0
ret = ""
begin = 0
for it in 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
clearEditor:() ->
@.editor.value ""
@.inputtags.value = ""
(@.find "blog-publish").swon = false
# load blog
loadBlogs: () ->
selidx = -1
el = @bloglist.selectedItem
selidx = $(el).index()
cond =
order:
ctime: "DESC"
fields: [
"id",
"title",
"ctimestr",
"ctime",
"utime",
"utimestr"
]
@blogdb.find cond
.then (r) =>
v.tag = "afx-blogger-post-item" for v in r
@bloglist.data = r
if selidx isnt -1
@bloglist.selected = selidx
else
@clearEditor()
@bloglist.selected = -1
.catch (e) => @error __("No post found: {0}", e.toString()), e
resizeContent: () ->
container = @find "editor-container"
children = ($ ".EasyMDEContainer", container).children()
titlebar = (($ @scheme).find ".afx-window-top")[0]
toolbar = children[0]
statusbar = children[3]
cheight = ($ @scheme).height() - ($ titlebar).height() - ($ toolbar).height() - ($ statusbar).height() - 90
($ 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",
]
this.OS.register "Blogger", Blogger

893
Blogger/main.ts Normal file
View File

@ -0,0 +1,893 @@
// 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);
}
}
}
]
});
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",
];
}
}

View File

@ -6,7 +6,7 @@
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version": "0.2.9-a",
"version": "0.2.10-a",
"category": "Internet",
"iconclass": "fa fa-book",
"dependencies": [
@ -36,7 +36,6 @@
"No email selected": "No email selected",
"Unable to send mail to: {0}": "Unable to send mail to: {0}",
"Error sending mail: {0}": "Error sending mail: {0}",
"Cannot fetch subscribers data: {0}": "Cannot fetch subscribers data: {0}",
"Open/create new database": "Open/create new database",
"Unable to init database file: {0}": "Unable to init database file: {0}",
"Select image file": "Select image file",
@ -65,28 +64,20 @@
"No post selected": "No post selected",
"Emails sent": "Emails sent",
"Error sending mails: {0}": "Error sending mails: {0}",
"No record found for ID {}": "No record found for ID {}",
"Cannot fetch the entry content": "Cannot fetch the entry content",
"Delete a post": "Delete a post",
"Do you really want to delete this post ?": "Do you really want to delete this post ?",
"Cannot fetch user data": "Cannot fetch user data",
"Full name must be entered": "Full name must be entered",
"User data updated": "User data updated",
"Cannot save user data": "Cannot save user data",
"Cannot save user data: {0}": "Cannot save user data: {0}",
"Unable to load categories": "Unable to load categories",
"Found {0} sections": "Found {0} sections",
"Please insert a title in the text: beginning with heading": "Please insert a title in the text: beginning with heading",
"Please enter tags": "Please enter tags",
"Cannot save blog: {0}": "Cannot save blog: {0}",
"Cannot add new category": "Cannot add new category",
"Unable to fetch categories": "Unable to fetch categories",
"Cannot Edit category": "Cannot Edit category",
"Cannot save section: {0}": "Cannot save section: {0}",
"Cannot move section": "Cannot move section",
"Cannot delete the category: {0} [{1}]": "Cannot delete the category: {0} [{1}]",
"Cannot delete all content of: {0} [{1}]": "Cannot delete all content of: {0} [{1}]",
"No post found: {0}": "No post found: {0}",
"Created: {0}": "Created: {0}",
"Updated: {0}": "Updated: {0}",
"No more record to load": "No more record to load",
"Full name": "Full name",
"Address": "Address",
"Phone": "Phone",
@ -95,7 +86,10 @@
"Photo": "Photo",
"Short biblio": "Short biblio",
"Categories": "Categories",
"Tags": "Tags"
"Load more": "Load more",
"Tags": "Tags",
"Created: {0}": "Created: {0}",
"Updated: {0}": "Updated: {0}"
}
}
}

View File

@ -1,5 +1,5 @@
<afx-app-window data-id = "blogger-win" apptitle="Blogger" width="650" height="500">
<afx-hbox >
<afx-hbox padding="5">
<afx-tab-container data-id = "tabcontainer" dir = "row" tabbarwidth= "40">
<afx-hbox data-id="user-container" data-height="100%" iconclass="fa fa-user-circle">
@ -65,7 +65,10 @@
<afx-hbox data-id = "blog-container" data-height="100%" iconclass="fa fa-book">
<afx-list-view data-id = "blog-list" min-width="100" data-width="200"></afx-list-view>
<afx-vbox>
<afx-list-view data-id = "blog-list" min-width="100" data-width="200"></afx-list-view>
<afx-button data-id = "blog-load-more" text = "__(Load more)" iconclass_end = "bi bi-chevron-double-right" data-height="content"></afx-button>
</afx-vbox>
<afx-resizer data-width = "3"></afx-resizer>
<afx-vbox>
<div data-id = "editor-container">

View File

@ -1,20 +0,0 @@
<afx-app-window data-id = "blogger-send-mail-win" apptitle="Send mail" width="500" height="400" resizable = "false">
<afx-hbox>
<afx-menu data-width="150" data-id="email-list"></afx-menu>
<afx-resizer data-width="3"></afx-resizer>
<div data-width="5"></div>
<afx-vbox >
<div data-height="5"></div>
<afx-label data-height="20" text = "__(Title)"></afx-label>
<input type = "text" data-height="20" name="title" data-id = "mail-title"></input>
<afx-label data-height = "20" text = "Content" ></afx-label>
<textarea name="content" data-id = "contentarea" ></textarea>
<div data-height="5"></div>
<afx-hbox data-height = "30">
<div></div>
<afx-button iconclass = "fa fa-paper-plane" data-id = "bt-sendmail" data-width="60" text = "__(Send)"></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width="5"></div>
</afx-hbox>
</afx-app-window>

View File

@ -1,63 +0,0 @@
Ant = this
class CVSectionListItemTag extends this.OS.GUI.tag.ListViewItemTag
constructor: () ->
super()
ondatachange: () ->
return unless @data
v = @data
nativel = ["content", "start", "end" ]
@closable = v.closable
for k, el of @refs
if v[k] and v[k] isnt ""
if nativel.includes k
$(el).text v[k]
else
el.text = v[k]
reload: () ->
init:() ->
itemlayout: () ->
{ el: "div", children: [
{ el: "afx-label", ref: "title", class: "afx-cv-sec-title" },
{ el: "afx-label", ref: "subtitle", class: "afx-cv-sec-subtitle" },
{ el: "p", ref: "content", class: "afx-cv-sec-content" },
{ el: "p", class: "afx-cv-sec-period", children: [
{ el: "i", ref: "start" },
{ el: "i", ref: "end", class: "period-end" }
] },
{ el: "afx-label", ref: "location", class: "afx-cv-sec-loc" }
] }
this.OS.GUI.tag.define "afx-blogger-cvsection-item", CVSectionListItemTag
class BlogPostListItemTag extends this.OS.GUI.tag.ListViewItemTag
constructor: () ->
super()
ondatachange: (v) ->
return unless @data
v = @data
v.closable = true
@closable = v.closable
@refs.title.text = v.title
@refs.ctimestr.text = __("Created: {0}", v.ctimestr)
@refs.utimestr.text = __("Updated: {0}", v.utimestr)
reload: () ->
init:() ->
itemlayout: () ->
{ el: "div", children: [
{ el: "afx-label", ref: "title", class: "afx-blogpost-title" },
{ el: "afx-label", ref: "ctimestr", class: "blog-dates" },
{ el: "afx-label", ref: "utimestr", class: "blog-dates" },
] }
this.OS.GUI.tag.define "afx-blogger-post-item", BlogPostListItemTag

104
Blogger/tags.ts Normal file
View File

@ -0,0 +1,104 @@
interface Array<T> {
/**
* Check if the array includes an element
*
* @param {T} element to check
* @returns {boolean}
* @memberof Array
*/
includes(el: T): boolean;
}
namespace OS {
export namespace application {
export namespace blogger {
class CVSectionListItemTag extends OS.GUI.tag.ListViewItemTag {
constructor() {
super();
}
ondatachange() {
if (!this.data) { return; }
const v = this.data;
const nativel = ["content", "start", "end"];
this.closable = v.closable;
return (() => {
const result = [];
for (let k in this.refs) {
const el = this.refs[k];
if (v[k] && (v[k] !== "")) {
if (nativel.includes(k)) {
result.push($(el).text(v[k]));
} else {
result.push((el as OS.GUI.tag.LabelTag).text = v[k]);
}
} else {
result.push(undefined);
}
}
return result;
})();
}
reload() { }
init() { }
itemlayout() {
return {
el: "div", children: [
{ el: "afx-label", ref: "title", class: "afx-cv-sec-title" },
{ el: "afx-label", ref: "subtitle", class: "afx-cv-sec-subtitle" },
{ el: "p", ref: "content", class: "afx-cv-sec-content" },
{
el: "p", class: "afx-cv-sec-period", children: [
{ el: "i", ref: "start" },
{ el: "i", ref: "end", class: "period-end" }
]
},
{ el: "afx-label", ref: "location", class: "afx-cv-sec-loc" }
]
};
}
}
OS.GUI.tag.define("afx-blogger-cvsection-item", CVSectionListItemTag);
class BlogPostListItemTag extends OS.GUI.tag.ListViewItemTag {
constructor() {
super();
}
ondatachange() {
if (!this.data) { return; }
const v = this.data;
v.closable = true;
this.closable = v.closable;
(this.refs.title as OS.GUI.tag.LabelTag).text = v.title;
(this.refs.ctimestr as OS.GUI.tag.LabelTag).text = __("Created: {0}", v.ctimestr);
(this.refs.utimestr as OS.GUI.tag.LabelTag).text = __("Updated: {0}", v.utimestr);
}
reload() { }
init() { }
itemlayout() {
return {
el: "div", children: [
{ el: "afx-label", ref: "title", class: "afx-blogpost-title" },
{ el: "afx-label", ref: "ctimestr", class: "blog-dates" },
{ el: "afx-label", ref: "utimestr", class: "blog-dates" },
]
};
}
}
OS.GUI.tag.define("afx-blogger-post-item", BlogPostListItemTag);
}
}
}

View File

@ -406,15 +406,27 @@ namespace OS {
filter.table_name = this._table_name;
if(this._id)
{
filter.where = { id: this._id};
filter.where = {};
filter.where[this.info.schema.pk] = this._id;
}
let data = await this._handle.select(filter);
let data: GenericObject<any>[] = await this._handle.select(filter);
if(this._id)
{
this.cache = data[0];
}
else
{
for(let row of data)
{
if(row[this.info.schema.pk])
{
Object.defineProperty(row, '$vfs', {
value: `${this.path}@${row[this.info.schema.pk]}`.asFileHandle(),
enumerable: false,
configurable: false
})
}
}
this.cache = data;
}
resolve(this.cache)
@ -444,7 +456,7 @@ namespace OS {
await this.onready();
if(this._id && this._table_name)
{
this.cache.id = this._id;
this.cache[this.info.schema.pk] = this._id;
const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk);
resolve({result:ret, error: false});
return
@ -504,7 +516,8 @@ namespace OS {
filter.table_name = this._table_name;
if(this._id)
{
filter.where = { id: this._id};
filter.where = {};
filter.where[this.info.schema.pk] = this._id;
}
let ret = await this._handle.delete_records(filter);
resolve({result: ret, error: false});

View File

@ -1 +1 @@
var OS;!function(e){let t;!function(e){class t{constructor(e){t.REGISTY||(t.REGISTY={}),this.db_file=e.asFileHandle(),t.REGISTY[this.db_file.path]?this.db_file=t.REGISTY[this.db_file.path]:t.REGISTY[this.db_file.path]=this.db_file}pwd(){return"pkg://SQLiteDB/".asFileHandle()}fileinfo(){return this.db_file.info}init(){return new Promise(async(e,t)=>{try{if(this.db_file.ready)return e(!0);let t={action:"init",args:{db_source:this.db_file.path}},a=await this.call(t);if(a=await this.db_file.onready(),!this.db_file||!this.db_file.ready||"file"!==this.db_file.info.type)throw __("DB file meta-data is invalid: {0}",this.db_file.path).__();e(!0)}catch(e){t(__e(e))}})}call(t){return new Promise(async(a,i)=>{t.args.db_source=this.db_file.path;let s={path:this.pwd().path+"/api/api.lua",parameters:t},r=await e.apigateway(s,!1);r.error?i(e.throwe(__("SQLiteDB server call error: {0}",r.error))):a(r.result)})}request(e){return new Promise(async(t,a)=>{try{this.db_file.ready||await this.init(),t(await this.call(e))}catch(e){a(__e(e))}})}select(e){let t={action:"select",args:{filter:e}};return this.request(t)}delete_records(e){let t={action:"delete_records",args:{filter:e}};return this.request(t)}drop_table(e){let t={action:"drop_table",args:{table_name:e}};return this.request(t)}list_tables(){return this.request({action:"list_table",args:{}})}create_table(e,t){let a={action:"create_table",args:{table_name:e,scheme:t}};return this.request(a)}get_table_scheme(e){let t={action:"table_scheme",args:{table_name:e}};return this.request(t)}insert(e,t,a){let i={action:"insert",args:{table_name:e,record:t,pk:a}};return this.request(i)}update(e,t,a){let i={action:"update",args:{table_name:e,record:t,pk:a}};return this.request(i)}last_insert_id(){return this.request({action:"last_insert_id",args:{}})}}let a;!function(e){class a extends e.BaseFileHandle{setPath(e){let a=e.split("@");if(super.setPath(a[0]),a.length>3)throw new Error(__("Invalid file path").__());this.path=e,this._table_name=a[1],this._id=a[2]?parseInt(a[2]):void 0,this._handle=new t("home://"+this.genealogy.join("/"))}meta(){return new Promise(async(e,t)=>{try{await this._handle.init();let t={result:{file:this._handle.fileinfo(),schema:void 0},error:!1};if(this._table_name){const e=await this._handle.get_table_scheme(this._table_name);if(0==e.length)t.result.schema=void 0;else{t.result.schema={fields:[],types:{},pk:void 0},t.result.schema.fields=e.map(e=>e.name);for(let a of e)t.result.schema.types[a.name]=a.type,a.pk&&(t.result.schema.pk=a.name)}}return e(t)}catch(e){return t(__e(e))}})}_rd(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={id:this._id});let i=await this._handle.select(a);this._id?this.cache=i[0]:this.cache=i,t(this.cache)}else{let e=await this._handle.list_tables();const a={};for(let t of e)a[t.name]=`${this.path}@${t.name}`.asFileHandle();this.cache=a,t(a)}}catch(e){return a(__e(e))}})}_wr(e){return new Promise(async(t,a)=>{try{if(!this.cache)throw new Error(__("No data to submit to remote database, please check the `cache` field").__());if(await this.onready(),this._id&&this._table_name)return this.cache.id=this._id,void t({result:await this._handle.update(this._table_name,this.cache,this.info.schema.pk),error:!1});if(this._table_name)return void t({result:await this._handle.insert(this._table_name,this.cache,this.info.schema.pk),error:!1});t({result:await this._handle.create_table(e,this.cache),error:!1})}catch(e){return a(__e(e))}})}_rm(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={id:this._id}),t({result:await this._handle.delete_records(a),error:!1})}else{let a=e;if(!a)throw new Error(__("No table specified for dropping").__());t({result:await this._handle.drop_table(a),error:!1})}}catch(e){return a(__e(e))}})}}e.register("^sqlite$",a)}(a=e.VFS||(e.VFS={}))}(t=e.API||(e.API={}))}(OS||(OS={}));
var OS;!function(e){let t;!function(e){class t{constructor(e){t.REGISTY||(t.REGISTY={}),this.db_file=e.asFileHandle(),t.REGISTY[this.db_file.path]?this.db_file=t.REGISTY[this.db_file.path]:t.REGISTY[this.db_file.path]=this.db_file}pwd(){return"pkg://SQLiteDB/".asFileHandle()}fileinfo(){return this.db_file.info}init(){return new Promise(async(e,t)=>{try{if(this.db_file.ready)return e(!0);let t={action:"init",args:{db_source:this.db_file.path}},a=await this.call(t);if(a=await this.db_file.onready(),!this.db_file||!this.db_file.ready||"file"!==this.db_file.info.type)throw __("DB file meta-data is invalid: {0}",this.db_file.path).__();e(!0)}catch(e){t(__e(e))}})}call(t){return new Promise(async(a,i)=>{t.args.db_source=this.db_file.path;let s={path:this.pwd().path+"/api/api.lua",parameters:t},r=await e.apigateway(s,!1);r.error?i(e.throwe(__("SQLiteDB server call error: {0}",r.error))):a(r.result)})}request(e){return new Promise(async(t,a)=>{try{this.db_file.ready||await this.init(),t(await this.call(e))}catch(e){a(__e(e))}})}select(e){let t={action:"select",args:{filter:e}};return this.request(t)}delete_records(e){let t={action:"delete_records",args:{filter:e}};return this.request(t)}drop_table(e){let t={action:"drop_table",args:{table_name:e}};return this.request(t)}list_tables(){return this.request({action:"list_table",args:{}})}create_table(e,t){let a={action:"create_table",args:{table_name:e,scheme:t}};return this.request(a)}get_table_scheme(e){let t={action:"table_scheme",args:{table_name:e}};return this.request(t)}insert(e,t,a){let i={action:"insert",args:{table_name:e,record:t,pk:a}};return this.request(i)}update(e,t,a){let i={action:"update",args:{table_name:e,record:t,pk:a}};return this.request(i)}last_insert_id(){return this.request({action:"last_insert_id",args:{}})}}let a;!function(e){class a extends e.BaseFileHandle{setPath(e){let a=e.split("@");if(super.setPath(a[0]),a.length>3)throw new Error(__("Invalid file path").__());this.path=e,this._table_name=a[1],this._id=a[2]?parseInt(a[2]):void 0,this._handle=new t("home://"+this.genealogy.join("/"))}meta(){return new Promise(async(e,t)=>{try{await this._handle.init();let t={result:{file:this._handle.fileinfo(),schema:void 0},error:!1};if(this._table_name){const e=await this._handle.get_table_scheme(this._table_name);if(0==e.length)t.result.schema=void 0;else{t.result.schema={fields:[],types:{},pk:void 0},t.result.schema.fields=e.map(e=>e.name);for(let a of e)t.result.schema.types[a.name]=a.type,a.pk&&(t.result.schema.pk=a.name)}}return e(t)}catch(e){return t(__e(e))}})}_rd(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={},a.where[this.info.schema.pk]=this._id);let i=await this._handle.select(a);if(this._id)this.cache=i[0];else{for(let e of i)e[this.info.schema.pk]&&Object.defineProperty(e,"$vfs",{value:`${this.path}@${e[this.info.schema.pk]}`.asFileHandle(),enumerable:!1,configurable:!1});this.cache=i}t(this.cache)}else{let e=await this._handle.list_tables();const a={};for(let t of e)a[t.name]=`${this.path}@${t.name}`.asFileHandle();this.cache=a,t(a)}}catch(e){return a(__e(e))}})}_wr(e){return new Promise(async(t,a)=>{try{if(!this.cache)throw new Error(__("No data to submit to remote database, please check the `cache` field").__());if(await this.onready(),this._id&&this._table_name)return this.cache[this.info.schema.pk]=this._id,void t({result:await this._handle.update(this._table_name,this.cache,this.info.schema.pk),error:!1});if(this._table_name)return void t({result:await this._handle.insert(this._table_name,this.cache,this.info.schema.pk),error:!1});t({result:await this._handle.create_table(e,this.cache),error:!1})}catch(e){return a(__e(e))}})}_rm(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={},a.where[this.info.schema.pk]=this._id),t({result:await this._handle.delete_records(a),error:!1})}else{let a=e;if(!a)throw new Error(__("No table specified for dropping").__());t({result:await this._handle.drop_table(a),error:!1})}}catch(e){return a(__e(e))}})}}e.register("^sqlite$",a)}(a=e.VFS||(e.VFS={}))}(t=e.API||(e.API={}))}(OS||(OS={}));

File diff suppressed because one or more lines are too long

View File

@ -16,6 +16,7 @@
<afx-grid-view data-id="tb-browser"></afx-grid-view>
<div data-height="5"></div>
<afx-hbox data-height="35">
<afx-button iconclass_end="bi bi-arrow-clockwise" data-id="bt-refresh" data-width="content"></afx-button>
<afx-button iconclass_end="bi bi-chevron-double-right" data-id="bt-load-next" data-width="content"></afx-button>
<div></div>
<afx-button iconclass="bi bi-plus-lg" data-id="btn-add-record" data-width="content"></afx-button>

Binary file not shown.

View File

@ -235,6 +235,23 @@ namespace OS {
this.error(__("Error reading table: {0}", e.toString()),e);
}
}
(this.find("bt-refresh") as GUI.tag.ButtonTag).onbtclick = async (e) => {
try
{
this.last_max_id = 0;
const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle;
const records = await handle.read({fields:["COUNT(*)"]});
this.n_records = records[0]["COUNT(*)"];
this.grid_table.rows = [];
await this.load_table();
}
catch(e)
{
this.error(__("Error reload table: {0}", e.toString()),e);
}
}
this.tbl_list.onlistselect = async (_) => {
try
{
@ -406,7 +423,7 @@ namespace OS {
for(let v in headers)
{
let text:string = e[headers[v].text];
if(text.length > 100)
if(text && text.length > 100)
{
text = text.substring(0,100);
}

View File

@ -16,6 +16,7 @@
<afx-grid-view data-id="tb-browser"></afx-grid-view>
<div data-height="5"></div>
<afx-hbox data-height="35">
<afx-button iconclass_end="bi bi-arrow-clockwise" data-id="bt-refresh" data-width="content"></afx-button>
<afx-button iconclass_end="bi bi-chevron-double-right" data-id="bt-load-next" data-width="content"></afx-button>
<div></div>
<afx-button iconclass="bi bi-plus-lg" data-id="btn-add-record" data-width="content"></afx-button>

View File

@ -95,8 +95,8 @@
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/Blogger/README.md",
"category": "Internet",
"author": "Xuan Sang LE",
"version": "0.2.9-a",
"dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r"],"mimes":["none"],
"version": "0.2.10-a",
"dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r","SQLiteDB@0.1.0-a"],"mimes":["none"],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/Blogger/build/release/Blogger.zip"
},
{
@ -409,6 +409,16 @@
"dependencies": [],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/SimpleMDE/build/release/SimpleMDE.zip"
},
{
"pkgname": "SQLiteDB",
"name": "SQLite3 Browser",
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/SQLiteDB/README.md",
"category": "Library",
"author": "Dany LE",
"version": "0.1.0-a",
"dependencies": [],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/SQLiteDB/build/release/SQLiteDB.zip"
},
{
"pkgname": "SystemControl",
"name": "System monitoring",