diff --git a/Blogger/build.json b/Blogger/build.json index 62a5f5a..566d9f8 100644 --- a/Blogger/build.json +++ b/Blogger/build.json @@ -13,18 +13,22 @@ } ] }, - "coffee": { + "ts": { "require": [ - "coffee" + "ts" ], "jobs": [ { - "name": "coffee-compile", + "name": "ts-import", + "data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"] + }, + { + "name": "ts-compile", "data": { "src": [ - "main.coffee", - "dialogs.coffee", - "tags.coffee" + "main.ts", + "dialogs.ts", + "tags.ts" ], "dest": "build/debug/main.js" } @@ -51,9 +55,7 @@ "data": { "src": [ "scheme.html", - "cvsection.html", "api/sendmail.lua", - "sendmail.html", "package.json", "README.md", "main.css" @@ -62,6 +64,27 @@ } } ] + }, + "locale": { + "require": ["locale"], + "jobs": [ + { + "name":"locale-gen", + "data": { + "src": "", + "exclude": ["build/", "api/"], + "locale": "en_GB", + "dest": "package.json" + } + } + ] + }, + "debug": { + "depend": [ + "init", + "ts", + "copy" + ] }, "release": { "require": [ @@ -69,7 +92,7 @@ ], "depend": [ "init", - "coffee", + "ts", "uglify", "copy" ], diff --git a/Blogger/build/debug/cvsection.html b/Blogger/build/debug/cvsection.html deleted file mode 100644 index 51d6928..0000000 --- a/Blogger/build/debug/cvsection.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
-
-
\ No newline at end of file diff --git a/Blogger/build/debug/main.js b/Blogger/build/debug/main.js index 985e4f6..719622c 100644 --- a/Blogger/build/debug/main.js +++ b/Blogger/build/debug/main.js @@ -1 +1,1180 @@ -(function(){var t,e,i,s,a,n;(e=class extends this.OS.application.BaseApplication{constructor(t){super("Blogger",t)}main(){var t;return this.user={},this.cvlist=this.find("cv-list"),this.cvlist.ontreeselect=t=>{var e;if(t)return e=t.data.item.data,this.CVSectionByCID(Number(e.id))},this.inputtags=this.find("input-tags"),this.bloglist=this.find("blog-list"),this.seclist=this.find("cv-sec-list"),t=this.find("photo"),$(t).click(e=>this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]}).then(e=>t.value=e.file.path).catch(t=>this.error(__("Unable to get file"),t))),this.userdb=new this._api.DB("user"),this.cvcatdb=new this._api.DB("cv_cat"),this.cvsecdb=new this._api.DB("cv_sections"),this.blogdb=new this._api.DB("blogs"),this.tabcontainer=this.find("tabcontainer"),this.tabcontainer.ontabselect=t=>this.fetchData(t.data.container.aid),this.find("bt-user-save").onbtclick=t=>this.saveUser(),this.find("cv-cat-add").onbtclick=t=>{var e;return e=t=>this.openDialog(new s,{title:__("Add category"),tree:t}).then(t=>{var e;return e={name:t.value,pid:t.p.id,publish:1},this.cvcatdb.save(e).then(t=>this.refreshCVCat()).catch(t=>this.error(__("Cannot add new category"),t))}).catch(t=>this.error(t.toString(),t)),this.fetchCVCat().then(t=>e(t)).catch(t=>(e({text:"Porfolio",id:"0",nodes:[]}),this.error(__("Unable to fetch categories"),t)))},this.find("cv-cat-edit").onbtclick=t=>{var e,i;if((i=this.cvlist.selectedItem)&&(e=i.data))return this.fetchCVCat().then(t=>this.openDialog(new s,{title:__("Edit category"),tree:t,cat:e}).then(t=>{var i;return i={id:e.id,publish:e.publish,pid:t.p.id,name:t.value},this.cvcatdb.save(i).then(t=>this.refreshCVCat()).catch(t=>this.error(__("Cannot Edit category"),t))})).catch(t=>this.error(__("Unable to fetch categories"),t))},this.find("cv-cat-del").onbtclick=t=>{var e,i;if((i=this.cvlist.selectedItem)&&(e=i.data))return this.openDialog("YesNoDialog",{title:__("Delete category"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.name)}).then(t=>{if(t)return this.deleteCVCat(e)}).catch(t=>this.error(t.toString(),t))},this.find("cv-sec-add").onbtclick=t=>{var e,s;if(s=this.cvlist.selectedItem)return(e=s.data)&&"0"!==e.id?this.openDialog(new i(this),{title:__("New section entry for {0}",e.name)}).then(t=>(t.cid=Number(e.id),t.start=Number(t.start),t.end=Number(t.end),this.cvsecdb.save(t).then(t=>this.CVSectionByCID(Number(e.id))).catch(t=>this.error(__("Cannot save section: {0}",t.toString()),t)))):this.notify(__("Please select a category"))},this.find("cv-sec-move").onbtclick=t=>{var e,i;return(i=this.find("cv-sec-list").selectedItem)?(e=i.data,this.fetchCVCat().then(t=>this.openDialog(new s,{title:__("Move to"),tree:t,selonly:!0}).then(t=>{var i;return i={id:e.id,cid:t.p.id},this.cvsecdb.save(i).then(t=>(this.CVSectionByCID(e.cid),this.find("cv-sec-list").unselect())).catch(t=>this.error(__("Cannot move section"),t))}))):this.notify(__("Please select a section to move"))},this.find("cv-sec-edit").onbtclick=t=>{var e,s;return(s=this.find("cv-sec-list").selectedItem)?(e=s.data,this.openDialog(new i(this),{title:__("Modify section entry"),section:e}).then(t=>(t.cid=Number(e.cid),t.start=Number(t.start),t.end=Number(t.end),this.cvsecdb.save(t).then(t=>this.CVSectionByCID(Number(e.cid))).catch(t=>this.error(__("Cannot save section: {0}",t.toString()),t))))):this.notify(__("Please select a section to edit"))},this.seclist.onitemclose=t=>{var e;if(t)return e=t.data.item.data,this.openDialog("YesNoDialog",{iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.title)}).then(i=>{if(i)return this.cvsecdb.delete(e.id).then(e=>this.seclist.delete(t.data.item)).catch(t=>this.error(__("Cannot delete the section: {0}",t.toString()),t))}),!1},this.editor=new EasyMDE({element:this.find("markarea"),autoDownloadFontAwesome:!1,autofocus:!0,tabSize:4,indentWithTabs:!0,toolbar:[{name:__("New"),className:"fa fa-file",action:t=>(this.bloglist.unselect(),this.clearEditor())},{name:__("Save"),className:"fa fa-save",action:t=>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:t=>this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]}).then(t=>t.file.path.asFileHandle().publish().then(t=>this.editor.codemirror.getDoc().replaceSelection(`![](${this._api.handle.shared}/${t.result})`)).catch(t=>this.error(__("Cannot export file for embedding to text"),t)))},{name:"Youtube",className:"fa fa-youtube",action:t=>this.editor.codemirror.getDoc().replaceSelection("[[youtube:]]")},"|",{name:__("Preview"),className:"fa fa-eye no-disable",action:t=>(this.previewOn=!this.previewOn,EasyMDE.togglePreview(t),renderMathInElement(this.find("editor-container")))},"|",{name:__("Send mail"),className:"fa fa-paper-plane",action:t=>{var e,i;return(i=this.bloglist.selectedItem)?(e=i.data,this.openDialog(new a(this),{title:__("Send mail"),content:this.editor.value(),id:e.id}).then((function(t){return console.log("Email sent")}))):this.error(__("No post selected"))}}]}),this.bloglist.onlistselect=e=>{var i;if((t=this.bloglist.selectedItem)&&(i=t.data))return this.blogdb.get(Number(i.id)).then(t=>(this.editor.value(t.content),this.inputtags.value=t.tags,this.find("blog-publish").swon=!!Number(t.publish))).catch(t=>this.error(__("Cannot fetch the entry content"),t))},this.bloglist.onitemclose=e=>{var i;if(e)return t=e.data.item,i=t.data,this.openDialog("YesNoDialog",{title:__("Delete a post"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete this post ?")}).then(e=>{if(e)return this.blogdb.delete(i.id).then(e=>(this.bloglist.delete(t),this.bloglist.unselect(),this.clearEditor()))}),!1},this.bindKey("CTRL-S",()=>{var t;if((t=this.tabcontainer.selectedTab)&&"blog-container"===t.container.aid)return this.saveBlog()}),this.on("resize",()=>this.resizeContent()),this.resizeContent(),this.loadBlogs()}fetchData(t){switch(t){case"user-container":return this.userdb.get(null).then(t=>{var e,i,s,a,n;for(this.user=t[0],a=[],e=0,s=(i=this.select("[input-class='user-input']")).length;ethis.error(__("Cannot fetch user data"),t));case"cv-container":return this.refreshCVCat();default:return this.loadBlogs()}}saveUser(){var t,e,i,s;for(t=0,i=(e=this.select("[input-class='user-input']")).length;tthis.notify(__("User data updated"))).catch(t=>this.error(__("Cannot save user data"),t)):this.notify(__("Full name must be entered"))}refreshCVCat(){return this.fetchCVCat().then(t=>(this.cvlist.data=t,this.cvlist.expandAll())).catch(t=>this.error(__("Unable to load categories"),t))}fetchCVCat(){return new Promise((t,e)=>{var i,s;return s={text:"Porfolio",id:"0",nodes:[]},i={order:{name:"ASC"}},this.cvcatdb.find(i).then(e=>(this.catListToTree(e,s,"0"),t(s))).catch((function(t){return e(__e(t))}))})}catListToTree(t,e,i){var s,a,n,r,o;if(0===(n=function(){var e,s,a;for(a=[],e=0,s=t.length;e(e=function(){var t,e,i;for(i=[],t=0,e=s.length;t(this.refreshCVCat(),this.seclist.data=[])).catch(e=>this.error(__("Cannot delete the category: {0} [{1}]",t.name,e.toString()),e)))).catch(e=>this.error(__("Cannot delete all content of: {0} [{1}]",t.name,e.toString()),e))}CVSectionByCID(t){var e;return e={exp:{"=":{cid:t}},order:{start:"DESC"}},this.cvsecdb.find(e).then(t=>{var e,i,s,a;for(i=[],this.find("cv-sec-status").text=__("Found {0} sections",t.length),e=0,s=t.length;ethis.error(t.toString(),t))}saveBlog(){var t,e,i,s,a,n,r;return s=void 0,(a=this.bloglist.selectedItem)&&(s=a.data),n=this.inputtags.value,t=this.editor.value(),(r=new RegExp("^#+(.*)\n","g").exec(t))&&2===r.length?""===n?this.notify(__("Please enter tags")):(e=new Date,i={content:t,title:r[1].trim(),tags:n,ctime:s?s.ctime:e.timestamp(),ctimestr:s?s.ctimestr:e.toString(),utime:e.timestamp(),utimestr:e.toString(),rendered:this.process(this.editor.options.previewRender(t)),publish:this.find("blog-publish").swon?1:0},s&&(i.id=s.id),this.blogdb.save(i).then(t=>this.loadBlogs()).catch(t=>this.error(__("Cannot save blog: {0}",t.toString()),t))):this.notify(__("Please insert a title in the text: beginning with heading"))}process(t){var e,i,s,a,n,r,o,l,h;for(i=function(t){return``},o=/\[\[youtube:([^\]]*)\]\]/g,l=[];null!==(s=o.exec(t));)l.push(s);if(!(l.length>0))return t;for(h="",e=0,a=0,r=l.length;a{var e,s;for(e=0,s=t.length;ethis.error(__("No post found: {0}",t.toString()),t))}resizeContent(){var t,e,i,s,a,n;return i=this.find("editor-container"),e=$(".EasyMDEContainer",i).children(),a=$(this.scheme).find(".afx-window-top")[0],n=e[0],s=e[3],t=$(this.scheme).height()-$(a).height()-$(n).height()-$(s).height()-90,$(e[1]).css("height",t+"px")}}).singleton=!0,e.dependencies=["pkg://SimpleMDE/main.js","pkg://SimpleMDE/main.css","pkg://Katex/main.js","pkg://Katex/main.css"],this.OS.register("Blogger",e),(s=class t extends this.OS.GUI.BasicDialog{constructor(){super("BloggerCategoryDialog",t.scheme)}main(){var t;if(super.main(),this.tree=this.find("tree"),this.txtinput=this.find("txtinput"),this.find("bt-ok").onbtclick=t=>{var e,i,s;return(e=this.tree.selectedItem)?(i=e.data,""!==(s=this.txtinput.value)||this.data.selonly?this.data.cat&&this.data.cat.id===i.id?this.notify(__("Parent can not be the category itself")):(this.handle&&this.handle({p:i,value:s}),this.quit()):this.notify(__("Please enter category name"))):this.notify(__("Please select a parent category"))},this.find("bt-cancel").onbtclick=t=>this.quit(),this.data&&this.data.tree)return this.data&&this.data.cat&&(this.txtinput.value=this.data.cat.name,(t="0"===this.data.cat.pid?this.data.tree:this.findDataByID(this.data.cat.pid,this.data.tree.nodes))&&(t.selected=!0)),this.tree.data=this.data.tree,this.tree.expandAll()}findDataByID(t,e){var i,s,a;for(s=0,a=e.length;s\n \n \n \n \n \n \n
\n \n \n
\n
\n
\n',i=class extends this.OS.GUI.BasicDialog{constructor(t){super("BloggerCVSectionDiaglog",(t.meta().path+"/cvsection.html").asFileHandle())}main(){var t,e;return super.main(),this.editor=new EasyMDE({autoDownloadFontAwesome:!1,element:this.find("contentarea"),status:!1,toolbar:!1}),$(this.select('[class = "CodeMirror-scroll"]')[0]).css("min-height","50px"),$(this.select('[class="CodeMirror cm-s-paper CodeMirror-wrap"]')[0]).css("min-height","50px"),t=this.select("[input-class='user-input']"),this.data&&this.data.section&&function(){var i,s,a;for(a=[],i=0,s=t.length;i{var s,a,n,r;for(s={},a=0,n=t.length;athis.resizeContent()),this.resizeContent()}resizeContent(){var t,e,i;return i=this.find("editor-container"),e=$(".EasyMDEContainer",i).children(),t=$(i).height()-30,$(e[0]).css("height",t+"px")}},(a=class t extends this.OS.GUI.BasicDialog{constructor(t){super("BloggerSendmailDiaglog",(t.meta().path+"/sendmail.html").asFileHandle())}main(){var e,i;return super.main(),this.subdb=new this.parent._api.DB("subscribers"),this.maillinglist=this.find("email-list"),i=new RegExp("^#+(.*)\n","g").exec(this.data.content),this.find("mail-title").value=i[1],e=this.data.content.substring(0,500)+"...",this.find("contentarea").value=t.template.format(this.data.id,e),this.subdb.find({}).then(t=>{var e,i,s;for(e=0,i=t.length;ethis.error(__("Cannot fetch subscribers data: {0}",t.toString()),t)),this.find("bt-sendmail").onbtclick=t=>{var e,i,s,a,n,r;for(i=[],s=0,n=(a=this.maillinglist.items).length;st.error?this.notify(__("Unable to send mail to: {0}",t.result.join(", "))):this.quit()).catch(t=>(console.log(t),this.error(__("Error sending mail: {0}",t.toString()),t))))}}}).template="Hello,\n\nXuan Sang LE has just published a new post on his blog: https://blog.lxsang.me/post/id/{0}\n\n==========\n{1}\n==========\n\n\nRead the full article via:\nhttps://blog.lxsang.me/post/id/{0}\n\nYou receive this email because you have been subscribed to his blog.\n\nHave a nice day,\n\nSent from Blogger, an AntOS application",n=class extends this.OS.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){var t,e,i,s,a,n;if(this.data){for(e in n=this.data,i=["content","start","end"],this.closable=n.closable,a=[],s=this.refs)t=s[e],n[e]&&""!==n[e]?i.includes(e)?a.push($(t).text(n[e])):a.push(t.text=n[e]):a.push(void 0);return a}}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"}]}}},this.OS.GUI.tag.define("afx-blogger-cvsection-item",n),t=class extends this.OS.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(t){if(this.data)return(t=this.data).closable=!0,this.closable=t.closable,this.refs.title.text=t.title,this.refs.ctimestr.text=__("Created: {0}",t.ctimestr),this.refs.utimestr.text=__("Updated: {0}",t.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"}]}}},this.OS.GUI.tag.define("afx-blogger-post-item",t)}).call(this); \ No newline at end of file + +// Copyright 2017-2018 Xuan Sang LE +// 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/. +var OS; +(function (OS) { + let application; + (function (application) { + class Blogger extends application.BaseApplication { + constructor(args) { + super("Blogger", args); + this.previewOn = false; + } + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + 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); + } + } + 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(); + await this.loadBlogs(); + } + catch (e) { + this.error(__("Unable to init database file: {0}", e.toString()), e); + this.dbhandle = undefined; + } + } + main() { + this.user = {}; + this.cvlist = this.find("cv-list"); + this.cvlist.ontreeselect = (d) => { + if (!d) { + return; + } + const { data } = d.data.item; + return this.CVSectionByCID(Number(data.id)); + }; + this.inputtags = this.find("input-tags"); + this.bloglist = this.find("blog-list"); + this.seclist = this.find("cv-sec-list"); + let el = this.find("photo"); + $(el) + .on("click", async (e) => { + 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"); + this.tabcontainer.ontabselect = (e) => { + return this.fetchData(e.data.container.aid); + }; + this.find("bt-user-save").onbtclick = (e) => { + return this.saveUser(); + }; + this.find("cv-cat-add").onbtclick = async (e) => { + try { + const tree = await this.fetchCVCat(); + const d = await this.openDialog(new application.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); + } + await this.refreshCVCat(); + } + catch (e) { + this.error(__("cv-cat-add: {0}", e.toString()), e); + } + }; + this.find("cv-cat-edit").onbtclick = async (e) => { + 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 application.blogger.BloggerCategoryDialog(), { + title: __("Edit category"), + tree, cat + }); + this.cvcatdb.cache = { + id: cat.id, + publish: cat.publish, + pid: d.p.id, + name: d.value + }; + const r = await this.cvcatdb.write(undefined); + if (r.error) { + throw new Error(r.error); + } + await this.refreshCVCat(); + } + catch (e) { + this.error(__("cv-cat-edit: {0}", e.toString()), e); + } + }; + this.find("cv-cat-del").onbtclick = async (e) => { + 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").onbtclick = async (e) => { + 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 application.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); + } + await this.CVSectionByCID(Number(cat.id)); + } + catch (e) { + this.error(__("cv-sec-add: {0}", e.toString()), e); + } + }; + this.find("cv-sec-move").onbtclick = async (e) => { + try { + const sel = this.seclist.selectedItem; + if (!sel) { + return this.toast(__("Please select a section to move")); + } + const sec = sel.data; + const tree = await this.fetchCVCat(); + const d = await this.openDialog(new application.blogger.BloggerCategoryDialog(), { + title: __("Move to"), + tree, + selonly: true + }); + this.cvsecdb.cache = { + id: sec.id, + cid: d.p.id + }; + const r = await this.cvsecdb.write(undefined); + if (r.error) { + throw new Error(r.error); + } + await this.CVSectionByCID(sec.cid); + this.seclist.unselect(); + } + catch (e) { + this.error(__("cv-sec-move: {0}", e.toString()), e); + } + }; + this.find("cv-sec-edit").onbtclick = async (e) => { + 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 application.blogger.BloggerCVSectionDiaglog(), { + title: __("Modify section entry"), + section: sec + }); + d.cid = Number(sec.cid); + d.start = Number(d.start); + d.end = Number(d.end); + this.cvsecdb.cache = d; + //d.publish = Number sec.publish + const r = await this.cvsecdb.write(undefined); + if (r.error) { + throw new Error(r.error); + } + 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) => { + if (!b) { + return; + } + try { + const r = await this.cvsecdb.remove({ + where: { + id: data.id + } + }); + if (r.error) { + throw new Error(r.error); + } + 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) => { + this.bloglist.unselect(); + return this.clearEditor(); + } + }, + { + name: __("Save"), + className: "fa fa-save", + action: (e) => { + 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) => { + const doc = this.editor.codemirror.getDoc(); + return doc.replaceSelection(`![](${this._api.handle.shared}/${r.result})`); + }).catch((e) => this.error(__("Cannot export file for embedding to text"), e)); + }); + } + }, + { + name: "Youtube", + className: "fa fa-youtube", + action: (e) => { + const doc = this.editor.codemirror.getDoc(); + return doc.replaceSelection("[[youtube:]]"); + } + }, + "|", + { + name: __("Preview"), + className: "fa fa-eye no-disable", + action: (e) => { + 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) => { + 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 application.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 = (e) => { + const el = this.bloglist.selectedItem; + if (!el) { + return; + } + const sel = el.data; + if (!sel) { + return; + } + return this.blogdb.read({ + where: { + id: Number(sel.id) + } + }) + .then((r) => { + this.editor.value(r.content); + this.inputtags.value = r.tags; + return this.find("blog-publish").swon = Number(r.publish) ? true : false; + }).catch((e) => { + return this.error(__("Cannot fetch the entry content"), e); + }); + }; + 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) => { + if (!b) { + return; + } + const r = await this.blogdb.remove({ + where: { + id: Number(data.id) + } + }); + if (r.error) { + throw new Error(r.error); + } + 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.aid !== "blog-container")) { + return; + } + return this.saveBlog(); + }); + this.on("resize", () => { + return this.resizeContent(); + }); + this.resizeContent(); + return this.init_db(); + } + // @fetchData 0 + // USER TAB + fetchData(idx) { + 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.name])); + }).catch((e) => this.error(__("Cannot fetch user data"), e)); + case "cv-container": // category + return this.refreshCVCat(); + default: + return this.loadBlogs(); + } + } + async saveUser() { + try { + const inputs = this.select("[input-class='user-input']"); + for (let v of inputs) { + this.user[v.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); + } + 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 + refreshCVCat() { + return this.fetchCVCat().then((data) => { + this.cvlist.data = data; + return this.cvlist.expandAll(); + }).catch((e) => this.error(__("Unable to load categories"), e)); + } + 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 + catListToTree(table, data, id) { + let v; + const result = table.filter((e) => { + 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); + } + } + deleteCVCat(cat) { + return new Promise(async (resolve, reject) => { + try { + let v; + const ids = []; + var func = function (c) { + 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); + } + r = await this.cvcatdb.remove({ + where: { + $or: { + id: ids + } + } + }); + if (r.error) { + throw new Error(r.error); + } + await this.refreshCVCat(); + this.seclist.data = []; + } + catch (e) { + reject(__e(e)); + } + }); + } + CVSectionByCID(cid) { + 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").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 + 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 = { + 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").swon ? 1 : 0 + }; + if (sel) { + data.id = sel.id; + } + //save the data + this.blogdb.cache = data; + const r = await this.blogdb.write(undefined); + if (r.error) { + throw new Error(r.error); + } + await this.loadBlogs(); + } + catch (e) { + this.error(__("Cannot save blog: {0}", e.toString()), e); + } + } + process(text) { + // find video tag and rendered it + let found; + const embed = (id) => `\ +\ +`; + 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; + } + clearEditor() { + this.editor.value(""); + this.inputtags.value = ""; + return this.find("blog-publish").swon = false; + } + // load blog + loadBlogs() { + return new Promise(async (ok, reject) => { + try { + let selidx = -1; + const el = this.bloglist.selectedItem; + selidx = $(el).index(); + const filter = { + order: ["ctime$desc"], + fields: [ + "id", + "title", + "ctimestr", + "ctime", + "utime", + "utimestr" + ] + }; + const r = await this.blogdb.read(filter); + for (let v of r) { + v.tag = "afx-blogger-post-item"; + } + this.bloglist.data = r; + if (selidx !== -1) { + return this.bloglist.selected = selidx; + } + else { + this.clearEditor(); + return this.bloglist.selected = -1; + } + } + catch (e) { + reject(__e(e)); + } + }); + } + 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"); + } + } + application.Blogger = Blogger; + 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", + ]; + })(application = OS.application || (OS.application = {})); +})(OS || (OS = {})); + +// Copyright 2017-2018 Xuan Sang LE +// 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/. +var OS; +(function (OS) { + let application; + (function (application) { + let blogger; + (function (blogger) { + class BloggerCategoryDialog extends OS.GUI.BasicDialog { + constructor() { + super("BloggerCategoryDialog", BloggerCategoryDialog.scheme); + } + main() { + super.main(); + this.tree = this.find("tree"); + this.txtinput = this.find("txtinput"); + this.find("bt-ok").onbtclick = (e) => { + 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").onbtclick = (e) => { + return this.quit(); + }; + if (this.data && this.data.tree) { + if (this.data && this.data.cat) { + let seldata; + 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, list) { + for (let data of list) { + if (data.id === id) { + return data; + } + if (data.nodes) { + this.findDataByID(id, data.nodes); + } + } + return undefined; + } + } + blogger.BloggerCategoryDialog = BloggerCategoryDialog; + BloggerCategoryDialog.scheme = `\ + + + + + + + +
+ + +
+
+
+
\s + `; + // This dialog is use for cv section editing + class BloggerCVSectionDiaglog extends OS.GUI.BasicDialog { + 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.name]); + } + } + if (this.data && this.data.section) { + this.editor.value(this.data.section.content); + } + this.find("section-publish").swon = (this.data && this.data.section && Number(this.data.section.publish) ? true : false); + this.find("bt-cv-sec-save").onbtclick = (e) => { + const data = {}; + for (let v of inputs) { + data[v.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").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"); + } + } + blogger.BloggerCVSectionDiaglog = BloggerCVSectionDiaglog; + BloggerCVSectionDiaglog.scheme = `\ + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+
+
`; + // this dialog is for send mail + class BloggerSendmailDiaglog extends OS.GUI.BasicDialog { + constructor() { + super("BloggerSendmailDiaglog"); + } + main() { + super.main(); + this.maillinglist = this.find("email-list"); + const title = (new RegExp("^#+(.*)\n", "g")).exec(this.data.content); + this.find("mail-title").value = title[1]; + const content = (this.data.content.substring(0, 500)) + "..."; + this.find("contentarea").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").onbtclick = (e) => { + 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").value, + content: this.find("contentarea").value + } + }; + return this._api.apigateway(data, false) + .then((d) => { + 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); + }); + }; + } + } + blogger.BloggerSendmailDiaglog = BloggerSendmailDiaglog; + BloggerSendmailDiaglog.scheme = `\ + + + + +
+ +
+ + + + +
+ +
+ +
+
+
+
+
`; + 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\ +`; + })(blogger = application.blogger || (application.blogger = {})); + })(application = OS.application || (OS.application = {})); +})(OS || (OS = {})); + +var OS; +(function (OS) { + let application; + (function (application) { + let blogger; + (function (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.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.text = v.title; + this.refs.ctimestr.text = __("Created: {0}", v.ctimestr); + this.refs.utimestr.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); + })(blogger = application.blogger || (application.blogger = {})); + })(application = OS.application || (OS.application = {})); +})(OS || (OS = {})); diff --git a/Blogger/build/debug/package.json b/Blogger/build/debug/package.json index 29dc296..0fcd2be 100644 --- a/Blogger/build/debug/package.json +++ b/Blogger/build/debug/package.json @@ -1,14 +1,101 @@ { - "app":"Blogger", - "name":"Blogging application", - "description":"Backend manager for blogging", - "info":{ + "app": "Blogger", + "name": "Blogging application", + "description": "Backend manager for blogging", + "info": { "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version":"0.2.9-a", - "category":"Internet", - "iconclass":"fa fa-book", - "dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r"], - "mimes":["none"] + "version": "0.2.9-a", + "category": "Internet", + "iconclass": "fa fa-book", + "dependencies": [ + "SimpleMDE@2.18.0-r", + "Katex@0.11.1-r", + "SQLiteDB@0.1.0-a" + ], + "mimes": [ + "none" + ], + "locales": { + "en_GB": { + "Pick a parent": "Pick a parent", + "Category name": "Category name", + "Ok": "Ok", + "Cancel": "Cancel", + "Title": "Title", + "Subtitle": "Subtitle", + "Location": "Location", + "From": "From", + "Save": "Save", + "Send": "Send", + "Please select a parent category": "Please select a parent category", + "Please enter category name": "Please enter category name", + "Parent can not be the category itself": "Parent can not be the category itself", + "Title or content must not be blank": "Title or content must not be blank", + "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", + "Unable to get file": "Unable to get file", + "Add category": "Add category", + "cv-cat-add: {0}": "cv-cat-add: {0}", + "Edit category": "Edit category", + "cv-cat-edit: {0}": "cv-cat-edit: {0}", + "Delete category": "Delete category", + "Do you really want to delete: {0}?": "Do you really want to delete: {0}?", + "cv-cat-del: {0}": "cv-cat-del: {0}", + "Please select a category": "Please select a category", + "New section entry for {0}": "New section entry for {0}", + "cv-sec-add: {0}": "cv-sec-add: {0}", + "Please select a section to move": "Please select a section to move", + "Move to": "Move to", + "cv-sec-move: {0}": "cv-sec-move: {0}", + "Please select a section to edit": "Please select a section to edit", + "Modify section entry": "Modify section entry", + "cv-sec-edit: {0}": "cv-sec-edit: {0}", + "Cannot delete the section: {0}": "Cannot delete the section: {0}", + "New": "New", + "Cannot export file for embedding to text": "Cannot export file for embedding to text", + "Preview": "Preview", + "Send mail": "Send mail", + "No post selected": "No post selected", + "Emails sent": "Emails sent", + "Error sending mails: {0}": "Error sending mails: {0}", + "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", + "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}", + "Full name": "Full name", + "Address": "Address", + "Phone": "Phone", + "Email": "Email", + "Url": "Url", + "Photo": "Photo", + "Short biblio": "Short biblio", + "Categories": "Categories", + "Tags": "Tags" + } + } } \ No newline at end of file diff --git a/Blogger/build/debug/sendmail.html b/Blogger/build/debug/sendmail.html deleted file mode 100644 index 799709a..0000000 --- a/Blogger/build/debug/sendmail.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - -
- -
- - - - -
- -
- -
-
-
-
-
\ No newline at end of file diff --git a/Blogger/build/release/Blogger.zip b/Blogger/build/release/Blogger.zip index a1c6026..4ab5068 100644 Binary files a/Blogger/build/release/Blogger.zip and b/Blogger/build/release/Blogger.zip differ diff --git a/Blogger/dialogs.coffee b/Blogger/dialogs.coffee index c38b9a0..630d9de 100644 --- a/Blogger/dialogs.coffee +++ b/Blogger/dialogs.coffee @@ -160,7 +160,7 @@ class BloggerSendmailDiaglog extends this.OS.GUI.BasicDialog content: (@find "contentarea").value @_api.apigateway data, false .then (d) => - return @notify __("Unable to send mail to: {0}", d.result.join(", ")) if d.error + return @notify __("Unable to send mail to: {0}", d.result.join(",")) if d.error @quit() .catch (e) => console.log e diff --git a/Blogger/package.json b/Blogger/package.json index 29dc296..0fcd2be 100644 --- a/Blogger/package.json +++ b/Blogger/package.json @@ -1,14 +1,101 @@ { - "app":"Blogger", - "name":"Blogging application", - "description":"Backend manager for blogging", - "info":{ + "app": "Blogger", + "name": "Blogging application", + "description": "Backend manager for blogging", + "info": { "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version":"0.2.9-a", - "category":"Internet", - "iconclass":"fa fa-book", - "dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r"], - "mimes":["none"] + "version": "0.2.9-a", + "category": "Internet", + "iconclass": "fa fa-book", + "dependencies": [ + "SimpleMDE@2.18.0-r", + "Katex@0.11.1-r", + "SQLiteDB@0.1.0-a" + ], + "mimes": [ + "none" + ], + "locales": { + "en_GB": { + "Pick a parent": "Pick a parent", + "Category name": "Category name", + "Ok": "Ok", + "Cancel": "Cancel", + "Title": "Title", + "Subtitle": "Subtitle", + "Location": "Location", + "From": "From", + "Save": "Save", + "Send": "Send", + "Please select a parent category": "Please select a parent category", + "Please enter category name": "Please enter category name", + "Parent can not be the category itself": "Parent can not be the category itself", + "Title or content must not be blank": "Title or content must not be blank", + "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", + "Unable to get file": "Unable to get file", + "Add category": "Add category", + "cv-cat-add: {0}": "cv-cat-add: {0}", + "Edit category": "Edit category", + "cv-cat-edit: {0}": "cv-cat-edit: {0}", + "Delete category": "Delete category", + "Do you really want to delete: {0}?": "Do you really want to delete: {0}?", + "cv-cat-del: {0}": "cv-cat-del: {0}", + "Please select a category": "Please select a category", + "New section entry for {0}": "New section entry for {0}", + "cv-sec-add: {0}": "cv-sec-add: {0}", + "Please select a section to move": "Please select a section to move", + "Move to": "Move to", + "cv-sec-move: {0}": "cv-sec-move: {0}", + "Please select a section to edit": "Please select a section to edit", + "Modify section entry": "Modify section entry", + "cv-sec-edit: {0}": "cv-sec-edit: {0}", + "Cannot delete the section: {0}": "Cannot delete the section: {0}", + "New": "New", + "Cannot export file for embedding to text": "Cannot export file for embedding to text", + "Preview": "Preview", + "Send mail": "Send mail", + "No post selected": "No post selected", + "Emails sent": "Emails sent", + "Error sending mails: {0}": "Error sending mails: {0}", + "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", + "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}", + "Full name": "Full name", + "Address": "Address", + "Phone": "Phone", + "Email": "Email", + "Url": "Url", + "Photo": "Photo", + "Short biblio": "Short biblio", + "Categories": "Categories", + "Tags": "Tags" + } + } } \ No newline at end of file diff --git a/SQLiteDB/LibSQLite.ts b/SQLiteDB/LibSQLite.ts index 79e0ae9..c2d4ed5 100644 --- a/SQLiteDB/LibSQLite.ts +++ b/SQLiteDB/LibSQLite.ts @@ -232,6 +232,7 @@ namespace OS { * user: "dany'", * $or: { * 'user.email': "test@mail.com", + * age: [15, 20, 25], * age$lte: 30, * $and: { * 'user.birth$ne': 1986, @@ -258,6 +259,9 @@ namespace OS { * ( contacts.user = 'dany''' ) AND * ( * ( user.email = 'test@mail.com' ) OR + * ( contacts.age = 15 ) OR + * ( contacts.age = 20 ) OR + * ( contacts.age = 25 ) OR * ( contacts.age <= 30 ) OR * ( * ( user.birth != 1986 ) AND diff --git a/SQLiteDB/api/api.lua b/SQLiteDB/api/api.lua index 4c038fe..26f7809 100644 --- a/SQLiteDB/api/api.lua +++ b/SQLiteDB/api/api.lua @@ -99,12 +99,14 @@ handle.insert = function(data) end local sql = string.format("INSERT INTO %s (%s) VALUES(%s)", data.table_name, table.concat(keys,","), table.concat(vals,",")) LOG_DEBUG("Execute query: [%s]", sql) - local ret, err = sqlite.exec(db, sql); + local ret, err = sqlite.exec(db, sql) + local id = sqlite.last_insert_id(db) sqlite.dbclose(db) if not ret then return error("insert: Unable to insert to %s: %s", data.table_name, err) else - return result(ret) + + return result(id) end end diff --git a/SQLiteDB/build/debug/api/api.lua b/SQLiteDB/build/debug/api/api.lua index 4c038fe..26f7809 100644 --- a/SQLiteDB/build/debug/api/api.lua +++ b/SQLiteDB/build/debug/api/api.lua @@ -99,12 +99,14 @@ handle.insert = function(data) end local sql = string.format("INSERT INTO %s (%s) VALUES(%s)", data.table_name, table.concat(keys,","), table.concat(vals,",")) LOG_DEBUG("Execute query: [%s]", sql) - local ret, err = sqlite.exec(db, sql); + local ret, err = sqlite.exec(db, sql) + local id = sqlite.last_insert_id(db) sqlite.dbclose(db) if not ret then return error("insert: Unable to insert to %s: %s", data.table_name, err) else - return result(ret) + + return result(id) end end diff --git a/SQLiteDB/build/debug/libsqlite.js b/SQLiteDB/build/debug/libsqlite.js index 4f32bd8..7cc3853 100644 --- a/SQLiteDB/build/debug/libsqlite.js +++ b/SQLiteDB/build/debug/libsqlite.js @@ -1,446 +1 @@ - -var OS; -(function (OS) { - let API; - (function (API) { - class SQLiteDBCore { - constructor(path) { - if (!SQLiteDBCore.REGISTY) { - SQLiteDBCore.REGISTY = {}; - } - this.db_file = path.asFileHandle(); - if (SQLiteDBCore.REGISTY[this.db_file.path]) { - this.db_file = SQLiteDBCore.REGISTY[this.db_file.path]; - } - else { - SQLiteDBCore.REGISTY[this.db_file.path] = this.db_file; - } - } - pwd() { - return "pkg://SQLiteDB/".asFileHandle(); - } - fileinfo() { - return this.db_file.info; - } - /** - * init and create the db file if it does not exist - */ - init() { - return new Promise(async (ok, reject) => { - try { - if (this.db_file.ready) { - return ok(true); - } - let request = { - action: 'init', - args: { - db_source: this.db_file.path, - } - }; - let _result = await this.call(request); - _result = await this.db_file.onready(); - if (!this.db_file || !this.db_file.ready || this.db_file.info.type !== "file") { - throw __("DB file meta-data is invalid: {0}", this.db_file.path).__(); - } - ok(true); - } - catch (e) { - reject(__e(e)); - } - }); - } - call(request) { - return new Promise(async (ok, reject) => { - request.args.db_source = this.db_file.path; - let cmd = { - path: this.pwd().path + "/api/api.lua", - parameters: request - }; - let data = await API.apigateway(cmd, false); - if (!data.error) { - ok(data.result); - } - else { - reject(API.throwe(__("SQLiteDB server call error: {0}", data.error))); - } - }); - } - request(rq) { - return new Promise(async (ok, reject) => { - try { - if (!this.db_file.ready) { - let _ = await this.init(); - } - let result = await this.call(rq); - ok(result); - } - catch (e) { - reject(__e(e)); - } - }); - } - select(filter) { - let rq = { - action: 'select', - args: { - filter - } - }; - return this.request(rq); - } - delete_records(filter) { - let rq = { - action: 'delete_records', - args: { - filter - } - }; - return this.request(rq); - } - drop_table(table_name) { - let rq = { - action: 'drop_table', - args: { table_name } - }; - return this.request(rq); - } - list_tables() { - let rq = { - action: 'list_table', - args: {} - }; - return this.request(rq); - } - create_table(table, scheme) { - let rq = { - action: 'create_table', - args: { - table_name: table, - scheme - } - }; - return this.request(rq); - } - get_table_scheme(table_name) { - let rq = { - action: 'table_scheme', - args: { - table_name - } - }; - return this.request(rq); - } - insert(table_name, record, pk) { - let rq = { - action: 'insert', - args: { - table_name, - record, - pk - } - }; - return this.request(rq); - } - update(table_name, record, pk) { - let rq = { - action: 'update', - args: { - table_name, - record, - pk - } - }; - return this.request(rq); - } - last_insert_id() { - let rq = { - action: 'last_insert_id', - args: {} - }; - return this.request(rq); - } - } - let VFS; - (function (VFS) { - /** - * SQLite VFS handle for database accessing - * - * A Sqlite file handle shall be in the following formats: - * * `sqlite://remote/path/to/file.db` refers to the entire databale (`remote/path/to/file.db` is relative to the home folder) - * - read operation, will list all available tables - * - write operations will create table - * - rm operation will delete table - * - meta operation will return file info - * - other operations are not supported - * * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database - * - meta operation will return fileinfo with table scheme information - * - read operation will read all records by filter defined by the filter as parameters - * - write operations will insert a new record - * - rm operation will delete records by filter as parameters - * - other operations are not supported - * - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id` - * - read operation will read the current record - * - write operation will update current record - * - rm operation will delete current record - * - other operations are not supported - * - * Example of filter: - * ```ts - * { - * table_name:'contacts'; - * where: { - * id$gte: 10, - * user: "dany'", - * $or: { - * 'user.email': "test@mail.com", - * age$lte: 30, - * $and: { - * 'user.birth$ne': 1986, - * age$not_between: [20,30], - * name$not_like: "%LE" - * } - * } - * }, - * fields: ['name as n', 'id', 'email'], - * order: ['user.name$asc', "id$desc"], - * joins: { - * cid: 'Category.id', - * did: 'Country.id', - * uid: "User.id" - * } - *} - * ``` - * This will generate the followings expressions: - * - `( self.name as n,self.id,self.email )` for fields - * - condition: - * ``` - * ( - * ( contacts.id >= 10 ) AND - * ( contacts.user = 'dany''' ) AND - * ( - * ( user.email = 'test@mail.com' ) OR - * ( contacts.age <= 30 ) OR - * ( - * ( user.birth != 1986 ) AND - * ( contacts.age NOT BETWEEN 20 AND 30 ) AND - * ( contacts.name NOT LIKE '%LE' ) - * ) - * ) - * ) - * ``` - * - order: `user.name ASC,contacts.id DESC` - * - joining: - * ``` - * INNER JOIN Category ON contacts.cid = Category.id - * INNER JOIN Country ON contacts.did = Country.id - * INNER JOIN Country ON contacts.did = Country.id - * ``` - * - * @class SqliteFileHandle - * @extends {BaseFileHandle} - */ - class SqliteFileHandle extends VFS.BaseFileHandle { - /** - * Set a file path to the current file handle - * - * - * @param {string} p - * @returns {void} - * @memberof SqliteFileHandle - */ - setPath(p) { - let arr = p.split("@"); - super.setPath(arr[0]); - if (arr.length > 3) { - throw new Error(__("Invalid file path").__()); - } - this.path = p; - this._table_name = arr[1]; - this._id = arr[2] ? parseInt(arr[2]) : undefined; - this._handle = new SQLiteDBCore(`home://${this.genealogy.join("/")}`); - } - /** - * Read database file meta-data - * - * Return file info on the target database file, if the table_name is specified - * return also the table scheme - * - * @returns {Promise} - * @memberof SqliteFileHandle - */ - meta() { - return new Promise(async (resolve, reject) => { - try { - await this._handle.init(); - let d = { - result: { - file: this._handle.fileinfo(), - schema: undefined - }, error: false - }; - if (this._table_name) { - const data = await this._handle.get_table_scheme(this._table_name); - if (data.length == 0) { - d.result.schema = undefined; - } - else { - d.result.schema = { - fields: [], - types: {}, - pk: undefined - }; - d.result.schema.fields = data.map(e => e.name); - for (let v of data) { - d.result.schema.types[v.name] = v.type; - if (v.pk) { - d.result.schema.pk = v.name; - } - } - } - } - return resolve(d); - } - catch (e) { - return reject(__e(e)); - } - }); - } - /** - * Query the database based on the provided info - * - * If no table is provided, return list of tables in the - * data base. - * If the current table is specified: - * - if the record id is specfied return the record - * - otherwise, return the records in the table using the specified filter - * - * @protected - * @param {any} t filter type - * @returns {Promise} - * @memberof SqliteFileHandle - */ - _rd(user_data) { - return new Promise(async (resolve, reject) => { - 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) { - // return list of tables in form of data base file handles in ready mode - let list = await this._handle.list_tables(); - const map = {}; - for (let v of list) { - map[v.name] = `${this.path}@${v.name}`.asFileHandle(); - } - this.cache = map; - resolve(map); - } - else { - // return all the data in the table set by the filter - // if this is a table, return the filtered records - // otherwise, it is a record, fetch only that record - let filter = user_data; - if (!filter || this._id) { - filter = {}; - } - filter.table_name = this._table_name; - if (this._id) { - filter.where = { id: this._id }; - } - let data = await this._handle.select(filter); - if (this._id) { - this.cache = data[0]; - } - else { - this.cache = data; - } - resolve(this.cache); - } - } - catch (e) { - return reject(__e(e)); - } - }); - } - /** - * Write commit file cache to the remote database - * - * @protected - * @param {string} t is table name, used only when create table - * @returns {Promise} - * @memberof SqliteFileHandle - */ - _wr(t) { - return new Promise(async (resolve, reject) => { - try { - if (!this.cache) { - throw new Error(__("No data to submit to remote database, please check the `cache` field").__()); - } - await this.onready(); - if (this._id && this._table_name) { - this.cache.id = this._id; - const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk); - resolve({ result: ret, error: false }); - return; - } - if (this._table_name) { - const ret = await this._handle.insert(this._table_name, this.cache, this.info.schema.pk); - resolve({ result: ret, error: false }); - return; - } - // create a new table with the scheme provided in the cache - let r = await this._handle.create_table(t, this.cache); - resolve({ result: r, error: false }); - } - catch (e) { - return reject(__e(e)); - } - }); - } - /** - * Delete data from remote database - * - * @protected - * @param {any} user_data is table name, for delete table, otherwise, filter object for deleting records - * @returns {Promise} - * @memberof SqliteFileHandle - */ - _rm(user_data) { - return new Promise(async (resolve, reject) => { - 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 table_name = user_data; - if (!table_name) { - throw new Error(__("No table specified for dropping").__()); - } - let ret = await this._handle.drop_table(table_name); - resolve({ result: ret, error: false }); - // delete the table - } - else { - let filter = user_data; - // delete the records in the table using the filter - if (!filter || this._id) { - filter = {}; - } - filter.table_name = this._table_name; - if (this._id) { - filter.where = { id: this._id }; - } - let ret = await this._handle.delete_records(filter); - resolve({ result: ret, error: false }); - } - } - catch (e) { - return reject(__e(e)); - } - }); - } - } - VFS.register("^sqlite$", SqliteFileHandle); - })(VFS = API.VFS || (API.VFS = {})); - })(API = OS.API || (OS.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={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={})); \ No newline at end of file diff --git a/SQLiteDB/build/debug/main.js b/SQLiteDB/build/debug/main.js index 74186ee..f9a242b 100644 --- a/SQLiteDB/build/debug/main.js +++ b/SQLiteDB/build/debug/main.js @@ -1,963 +1 @@ - - -var OS; -(function (OS) { - let application; - (function (application) { - /** - * - * @class SQLiteDBBrowser - * @extends {BaseApplication} - */ - class SQLiteDBBrowser extends application.BaseApplication { - constructor(args) { - super("SQLiteDBBrowser", args); - } - menu() { - return [ - { - text: "__(File)", - nodes: [ - { - text: "__(New database)", - dataid: "new", - shortcut: 'A-N' - }, - { - text: "__(Open database)", - dataid: "open", - shortcut: 'A-O' - }, - ], - onchildselect: (e) => { - switch (e.data.item.data.dataid) { - case "new": - return this.newFile(); - case "open": - return this.openFile(); - } - } - } - ]; - } - list_tables() { - this.filehandle.read() - .then((data) => { - const list = []; - for (let k in data) { - list.push({ - text: k, - name: k, - handle: data[k] - }); - } - this.tbl_list.data = list; - if (list.length > 0) { - this.tbl_list.selected = list.length - 1; - } - }); - } - async openFile() { - try { - const d_1 = await this.openDialog("FileDialog", { - title: __("Open file"), - mimes: this.meta().mimes - }); - this.filehandle = `sqlite://${d_1.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle(); - await this.filehandle.onready(); - this.list_tables(); - } - catch (e) { - this.error(__("Unable to open database file: {0}", e.toString()), e); - } - ; - } - async newFile() { - try { - const f = await this.openDialog("FileDialog", { - title: __("Save as"), - 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.filehandle = `sqlite://${target.genealogy.join("/")}`.asFileHandle(); - await this.filehandle.onready(); - this.list_tables(); - } - catch (e) { - this.error(__("Unable to init database file: {0}", e.toString()), e); - } - } - main() { - this.filehandle = undefined; - this.tbl_list = this.find("tbl-list"); - this.grid_table = this.find("tb-browser"); - this.grid_scheme = this.find("sch-browser"); - this.grid_table.resizable = true; - this.grid_scheme.resizable = true; - this.grid_scheme.header = [ - { text: __("Field name") }, - { text: __("Field type") }, - ]; - this.btn_loadmore = this.find("bt-load-next"); - this.container = this.find("container"); - this.bindKey("ALT-N", () => { - return this.newFile(); - }); - this.bindKey("ALT-O", () => { - return this.openFile(); - }); - this.container.ontabselect = (e) => { - if (this.container.selectedIndex == 0) { - if (!this.tbl_list.selectedItem) - return; - const schema = this.tbl_list.selectedItem.data.handle.info.schema; - if (!schema) - return; - const data = []; - for (let k in schema.types) { - data.push([ - { text: k }, - { text: schema.types[k] } - ]); - } - this.grid_scheme.rows = data; - } - }; - this.find("bt-rm-table").onbtclick = async (e) => { - try { - if (!this.filehandle) { - return this.notify(__("Please open a database file")); - } - if (this.tbl_list.selectedItem == undefined) { - return; - } - const table = this.tbl_list.selectedItem.data.name; - const ret = await this.openDialog("YesNoDialog", { - title: __("Confirm delete?"), - text: __("Do you realy want to delete table: {0}", table) - }); - if (ret) { - await this.filehandle.remove(table); - this.list_tables(); - } - } - catch (e) { - this.error(__("Unable to execute action table delete: {0}", e.toString()), e); - } - }; - this.find("bt-add-table").onbtclick = async (e) => { - try { - if (!this.filehandle) { - return this.notify(__("Please open a database file")); - } - const data = await this.openDialog(new NewTableDialog(), { - title: __("Create new table") - }); - this.filehandle.cache = data.schema; - await this.filehandle.write(data.name); - this.list_tables(); - } - catch (e) { - this.error(__("Unable to create table: {0}", e.toString()), e); - } - }; - this.find("btn-edit-record").onbtclick = async (e) => { - this.edit_record(); - }; - this.find("btn-add-record").onbtclick = async (e) => { - this.add_record(); - }; - this.find("btn-delete-record").onbtclick = async (e) => { - this.remove_record(); - }; - this.btn_loadmore.onbtclick = async (e) => { - try { - await this.load_table(); - } - catch (e) { - this.error(__("Error reading table: {0}", e.toString()), e); - } - }; - this.tbl_list.onlistselect = async (_) => { - try { - if (!this.tbl_list.selectedItem) - return; - const handle = this.tbl_list.selectedItem.data.handle; - await handle.onready(); - this.last_max_id = 0; - const headers = handle.info.schema.fields.map((e) => { - return { text: e }; - }); - this.grid_table.header = headers; - this.grid_table.rows = []; - const records = await handle.read({ fields: ["COUNT(*)"] }); - this.n_records = records[0]["(COUNT(*))"]; - this.btn_loadmore.text = `0/${this.n_records}`; - await this.load_table(); - this.container.selectedIndex = 1; - } - catch (e) { - this.error(__("Error reading table: {0}", e.toString()), e); - } - }; - this.grid_table.oncelldbclick = async (e) => { - this.edit_record(); - }; - this.openFile(); - } - async add_record() { - try { - const table_handle = this.tbl_list.selectedItem; - if (!table_handle) { - return; - } - const file_hd = table_handle.data.handle; - const schema = table_handle.data.handle.info.schema; - const model = {}; - for (let k in schema.types) { - if (["INTEGER", "REAL", "NUMERIC"].includes(schema.types[k])) { - model[k] = 0; - } - else { - model[k] = ""; - } - } - console.log(model); - const data = await this.openDialog(new RecordEditDialog(), { - title: __("New record"), - schema: schema, - record: model - }); - file_hd.cache = data; - await file_hd.write(undefined); - this.n_records += 1; - await this.load_table(); - } - catch (e) { - this.error(__("Error edit/view record: {0}", e.toString()), e); - } - } - async remove_record() { - try { - const cell = this.grid_table.selectedCell; - const row = this.grid_table.selectedRow; - const table_handle = this.tbl_list.selectedItem; - if (!cell || !table_handle) { - return; - } - const pk_id = cell.data.record[table_handle.data.handle.info.schema.pk]; - const ret = await this.openDialog("YesNoDialog", { - title: __("Delete record"), - text: __("Do you realy want to delete record {0}", pk_id) - }); - if (!ret) { - return; - } - const file_hd = `${table_handle.data.handle.path}@${pk_id}`.asFileHandle(); - await file_hd.remove(); - this.n_records--; - // remove the target row - this.grid_table.delete(row); - this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`; - } - catch (e) { - this.error(__("Error deleting record: {0}", e.toString()), e); - } - } - async edit_record() { - try { - const cell = this.grid_table.selectedCell; - const row = this.grid_table.selectedRow; - const table_handle = this.tbl_list.selectedItem; - if (!cell || !table_handle) { - return; - } - const data = await this.openDialog(new RecordEditDialog(), { - title: __("View/edit record"), - schema: table_handle.data.handle.info.schema, - record: cell.data.record - }); - const pk_id = cell.data.record[table_handle.data.handle.info.schema.pk]; - const file_hd = `${table_handle.data.handle.path}@${pk_id}`.asFileHandle(); - file_hd.cache = data; - await file_hd.write(undefined); - const row_data = []; - for (let k of file_hd.info.schema.fields) { - let text = data[k]; - if (text.length > 100) { - text = text.substring(0, 100); - } - row_data.push({ - text: text, - record: data - }); - } - row.data = row_data; - } - catch (e) { - this.error(__("Error edit/view record: {0}", e.toString()), e); - } - } - async load_table() { - if (this.grid_table.rows && this.grid_table.rows.length >= this.n_records) { - return; - } - if (!this.tbl_list.selectedItem) - return; - const handle = this.tbl_list.selectedItem.data.handle; - await handle.onready(); - const headers = handle.info.schema.fields.map((e) => { - return { text: e }; - }); - // read all records - const filter = { where: {}, limit: 10 }; - filter.where[`${handle.info.schema.pk}\$gt`] = this.last_max_id; - const records = await handle.read(filter); - if (records && records.length > 0) { - for (let e of records) { - const row = []; - if (e.id && e.id > this.last_max_id) { - this.last_max_id = e.id; - } - for (let v in headers) { - let text = e[headers[v].text]; - if (text.length > 100) { - text = text.substring(0, 100); - } - row.push({ - text: text, - record: e - }); - } - this.grid_table.push(row, false); - } - this.grid_table.scroll_to_bottom(); - } - this.btn_loadmore.text = `${this.grid_table.rows.length}/${this.n_records}`; - } - } - application.SQLiteDBBrowser = SQLiteDBBrowser; - SQLiteDBBrowser.dependencies = [ - "pkg://SQLiteDB/libsqlite.js" - ]; - class NewTableDialog extends OS.GUI.BasicDialog { - /** - * Creates an instance of NewTableDialog. - * @memberof NewTableDialog - */ - constructor() { - super("NewTableDialog"); - } - /** - * Main entry point - * - * @memberof NewTableDialog - */ - main() { - super.main(); - this.container = this.find("container"); - this.find("btnCancel").onbtclick = (e) => this.quit(); - this.find("btnAdd").onbtclick = (e) => this.addField(); - $(this.container) - .css("overflow-y", "auto"); - this.addField(); - this.find("btnOk").onbtclick = (e) => { - const input = this.find("txt-tblname"); - if (!input.value || input.value == "") { - return this.notify(__("Please enter table name")); - } - const tblname = input.value; - const inputs = $("input", this.container); - const lists = $("afx-list-view", this.container); - if (inputs.length == 0) { - return this.notify(__("Please define table fields")); - } - let cdata = {}; - for (let i = 0; i < inputs.length; i++) { - const key = inputs[i].value.trim(); - if (key === "") { - return this.notify(__("Field name cannot be empty")); - } - if (cdata[key]) { - return this.notify(__("Duplicate field: {0}", key)); - } - cdata[key] = lists[i].selectedItem.data.text; - } - if (this.handle) - this.handle({ name: tblname, schema: cdata }); - this.quit(); - }; - } - /** - * Add new input key-value field to the dialog - * - * @private - * @memberof NewTableDialog - */ - addField() { - const div = $("
") - .css("display", "flex") - .css("flex-direction", "row") - .appendTo(this.container); - $("") - .attr("type", "text") - .css("flex", "1") - .appendTo(div); - let list = $("") - .css("flex", "1") - .appendTo(div)[0]; - list.uify(this.observable); - list.dropdown = true; - list.data = [ - { text: "TEXT" }, - { text: "INTEGER" }, - { text: "REAL" }, - { text: "NUMERIC" }, - ]; - list.selected = 0; - const btn = $(""); - btn[0].uify(undefined); - btn[0].iconclass = "fa fa-minus"; - btn - .on("click", () => { - div.remove(); - }) - .appendTo(div); - } - } - /** - * Scheme definition - */ - NewTableDialog.scheme = `\ - - - - -
- - -
- - -
-
-
-
`; - class RecordEditDialog extends OS.GUI.BasicDialog { - /** - * Creates an instance of RecordEditDialog. - * @memberof RecordEditDialog - */ - constructor() { - super("RecordEditDialog"); - } - /** - * Main entry point - * - * @memberof RecordEditDialog - */ - main() { - super.main(); - this.container = this.find("container"); - this.find("btnCancel").onbtclick = (e) => this.quit(); - $(this.container) - .css("overflow-y", "auto"); - if (!this.data || !this.data.schema) { - throw new Error(__("No data provided for dialog").__()); - } - for (let k in this.data.schema.types) { - const input = $("").appendTo(this.container)[0]; - input.uify(this.observable); - input.label = k; - if (k == this.data.schema.pk) { - input.disable = true; - } - if (this.data.schema.types[k] == "TEXT") { - input.verbose = true; - $(input).css("height", "100px"); - } - if (this.data.record[k] != undefined) { - input.value = this.data.record[k]; - } - } - this.find("btnOk").onbtclick = (e) => { - const inputs = $("afx-input", this.container); - const data = {}; - for (let input of inputs) { - data[input.label.__()] = input.value; - } - if (this.handle) - this.handle(data); - this.quit(); - }; - } - } - /** - * Scheme definition - */ - RecordEditDialog.scheme = `\ - - -
- -
-
- - -
-
-
-
`; - })(application = OS.application || (OS.application = {})); -})(OS || (OS = {})); - - -var OS; -(function (OS) { - let API; - (function (API) { - class SQLiteDBCore { - constructor(path) { - if (!SQLiteDBCore.REGISTY) { - SQLiteDBCore.REGISTY = {}; - } - this.db_file = path.asFileHandle(); - if (SQLiteDBCore.REGISTY[this.db_file.path]) { - this.db_file = SQLiteDBCore.REGISTY[this.db_file.path]; - } - else { - SQLiteDBCore.REGISTY[this.db_file.path] = this.db_file; - } - } - pwd() { - return "pkg://SQLiteDB/".asFileHandle(); - } - fileinfo() { - return this.db_file.info; - } - /** - * init and create the db file if it does not exist - */ - init() { - return new Promise(async (ok, reject) => { - try { - if (this.db_file.ready) { - return ok(true); - } - let request = { - action: 'init', - args: { - db_source: this.db_file.path, - } - }; - let _result = await this.call(request); - _result = await this.db_file.onready(); - if (!this.db_file || !this.db_file.ready || this.db_file.info.type !== "file") { - throw __("DB file meta-data is invalid: {0}", this.db_file.path).__(); - } - ok(true); - } - catch (e) { - reject(__e(e)); - } - }); - } - call(request) { - return new Promise(async (ok, reject) => { - request.args.db_source = this.db_file.path; - let cmd = { - path: this.pwd().path + "/api/api.lua", - parameters: request - }; - let data = await API.apigateway(cmd, false); - if (!data.error) { - ok(data.result); - } - else { - reject(API.throwe(__("SQLiteDB server call error: {0}", data.error))); - } - }); - } - request(rq) { - return new Promise(async (ok, reject) => { - try { - if (!this.db_file.ready) { - let _ = await this.init(); - } - let result = await this.call(rq); - ok(result); - } - catch (e) { - reject(__e(e)); - } - }); - } - select(filter) { - let rq = { - action: 'select', - args: { - filter - } - }; - return this.request(rq); - } - delete_records(filter) { - let rq = { - action: 'delete_records', - args: { - filter - } - }; - return this.request(rq); - } - drop_table(table_name) { - let rq = { - action: 'drop_table', - args: { table_name } - }; - return this.request(rq); - } - list_tables() { - let rq = { - action: 'list_table', - args: {} - }; - return this.request(rq); - } - create_table(table, scheme) { - let rq = { - action: 'create_table', - args: { - table_name: table, - scheme - } - }; - return this.request(rq); - } - get_table_scheme(table_name) { - let rq = { - action: 'table_scheme', - args: { - table_name - } - }; - return this.request(rq); - } - insert(table_name, record, pk) { - let rq = { - action: 'insert', - args: { - table_name, - record, - pk - } - }; - return this.request(rq); - } - update(table_name, record, pk) { - let rq = { - action: 'update', - args: { - table_name, - record, - pk - } - }; - return this.request(rq); - } - last_insert_id() { - let rq = { - action: 'last_insert_id', - args: {} - }; - return this.request(rq); - } - } - let VFS; - (function (VFS) { - /** - * SQLite VFS handle for database accessing - * - * A Sqlite file handle shall be in the following formats: - * * `sqlite://remote/path/to/file.db` refers to the entire databale (`remote/path/to/file.db` is relative to the home folder) - * - read operation, will list all available tables - * - write operations will create table - * - rm operation will delete table - * - meta operation will return file info - * - other operations are not supported - * * `sqlite://remote/path/to/file.db@table_name` refers to the table `table_name` in the database - * - meta operation will return fileinfo with table scheme information - * - read operation will read all records by filter defined by the filter as parameters - * - write operations will insert a new record - * - rm operation will delete records by filter as parameters - * - other operations are not supported - * - `sqlite://remote/path/to/file.db@table_name@id` refers to a records in `table_name` with ID `id` - * - read operation will read the current record - * - write operation will update current record - * - rm operation will delete current record - * - other operations are not supported - * - * Example of filter: - * ```ts - * { - * table_name:'contacts'; - * where: { - * id$gte: 10, - * user: "dany'", - * $or: { - * 'user.email': "test@mail.com", - * age$lte: 30, - * $and: { - * 'user.birth$ne': 1986, - * age$not_between: [20,30], - * name$not_like: "%LE" - * } - * } - * }, - * fields: ['name as n', 'id', 'email'], - * order: ['user.name$asc', "id$desc"], - * joins: { - * cid: 'Category.id', - * did: 'Country.id', - * uid: "User.id" - * } - *} - * ``` - * This will generate the followings expressions: - * - `( self.name as n,self.id,self.email )` for fields - * - condition: - * ``` - * ( - * ( contacts.id >= 10 ) AND - * ( contacts.user = 'dany''' ) AND - * ( - * ( user.email = 'test@mail.com' ) OR - * ( contacts.age <= 30 ) OR - * ( - * ( user.birth != 1986 ) AND - * ( contacts.age NOT BETWEEN 20 AND 30 ) AND - * ( contacts.name NOT LIKE '%LE' ) - * ) - * ) - * ) - * ``` - * - order: `user.name ASC,contacts.id DESC` - * - joining: - * ``` - * INNER JOIN Category ON contacts.cid = Category.id - * INNER JOIN Country ON contacts.did = Country.id - * INNER JOIN Country ON contacts.did = Country.id - * ``` - * - * @class SqliteFileHandle - * @extends {BaseFileHandle} - */ - class SqliteFileHandle extends VFS.BaseFileHandle { - /** - * Set a file path to the current file handle - * - * - * @param {string} p - * @returns {void} - * @memberof SqliteFileHandle - */ - setPath(p) { - let arr = p.split("@"); - super.setPath(arr[0]); - if (arr.length > 3) { - throw new Error(__("Invalid file path").__()); - } - this.path = p; - this._table_name = arr[1]; - this._id = arr[2] ? parseInt(arr[2]) : undefined; - this._handle = new SQLiteDBCore(`home://${this.genealogy.join("/")}`); - } - /** - * Read database file meta-data - * - * Return file info on the target database file, if the table_name is specified - * return also the table scheme - * - * @returns {Promise} - * @memberof SqliteFileHandle - */ - meta() { - return new Promise(async (resolve, reject) => { - try { - await this._handle.init(); - let d = { - result: { - file: this._handle.fileinfo(), - schema: undefined - }, error: false - }; - if (this._table_name) { - const data = await this._handle.get_table_scheme(this._table_name); - if (data.length == 0) { - d.result.schema = undefined; - } - else { - d.result.schema = { - fields: [], - types: {}, - pk: undefined - }; - d.result.schema.fields = data.map(e => e.name); - for (let v of data) { - d.result.schema.types[v.name] = v.type; - if (v.pk) { - d.result.schema.pk = v.name; - } - } - } - } - return resolve(d); - } - catch (e) { - return reject(__e(e)); - } - }); - } - /** - * Query the database based on the provided info - * - * If no table is provided, return list of tables in the - * data base. - * If the current table is specified: - * - if the record id is specfied return the record - * - otherwise, return the records in the table using the specified filter - * - * @protected - * @param {any} t filter type - * @returns {Promise} - * @memberof SqliteFileHandle - */ - _rd(user_data) { - return new Promise(async (resolve, reject) => { - 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) { - // return list of tables in form of data base file handles in ready mode - let list = await this._handle.list_tables(); - const map = {}; - for (let v of list) { - map[v.name] = `${this.path}@${v.name}`.asFileHandle(); - } - this.cache = map; - resolve(map); - } - else { - // return all the data in the table set by the filter - // if this is a table, return the filtered records - // otherwise, it is a record, fetch only that record - let filter = user_data; - if (!filter || this._id) { - filter = {}; - } - filter.table_name = this._table_name; - if (this._id) { - filter.where = { id: this._id }; - } - let data = await this._handle.select(filter); - if (this._id) { - this.cache = data[0]; - } - else { - this.cache = data; - } - resolve(this.cache); - } - } - catch (e) { - return reject(__e(e)); - } - }); - } - /** - * Write commit file cache to the remote database - * - * @protected - * @param {string} t is table name, used only when create table - * @returns {Promise} - * @memberof SqliteFileHandle - */ - _wr(t) { - return new Promise(async (resolve, reject) => { - try { - if (!this.cache) { - throw new Error(__("No data to submit to remote database, please check the `cache` field").__()); - } - await this.onready(); - if (this._id && this._table_name) { - this.cache.id = this._id; - const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk); - resolve({ result: ret, error: false }); - return; - } - if (this._table_name) { - const ret = await this._handle.insert(this._table_name, this.cache, this.info.schema.pk); - resolve({ result: ret, error: false }); - return; - } - // create a new table with the scheme provided in the cache - let r = await this._handle.create_table(t, this.cache); - resolve({ result: r, error: false }); - } - catch (e) { - return reject(__e(e)); - } - }); - } - /** - * Delete data from remote database - * - * @protected - * @param {any} user_data is table name, for delete table, otherwise, filter object for deleting records - * @returns {Promise} - * @memberof SqliteFileHandle - */ - _rm(user_data) { - return new Promise(async (resolve, reject) => { - 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 table_name = user_data; - if (!table_name) { - throw new Error(__("No table specified for dropping").__()); - } - let ret = await this._handle.drop_table(table_name); - resolve({ result: ret, error: false }); - // delete the table - } - else { - let filter = user_data; - // delete the records in the table using the filter - if (!filter || this._id) { - filter = {}; - } - filter.table_name = this._table_name; - if (this._id) { - filter.where = { id: this._id }; - } - let ret = await this._handle.delete_records(filter); - resolve({ result: ret, error: false }); - } - } - catch (e) { - return reject(__e(e)); - } - }); - } - } - VFS.register("^sqlite$", SqliteFileHandle); - })(VFS = API.VFS || (API.VFS = {})); - })(API = OS.API || (OS.API = {})); -})(OS || (OS = {})); +var OS;!function(t){let e;!function(e){class i extends e.BaseApplication{constructor(t){super("SQLiteDBBrowser",t)}menu(){return[{text:"__(File)",nodes:[{text:"__(New database)",dataid:"new",shortcut:"A-N"},{text:"__(Open database)",dataid:"open",shortcut:"A-O"}],onchildselect:t=>{switch(t.data.item.data.dataid){case"new":return this.newFile();case"open":return this.openFile()}}}]}list_tables(){this.filehandle.read().then(t=>{const e=[];for(let i in t)e.push({text:i,name:i,handle:t[i]});this.tbl_list.data=e,e.length>0&&(this.tbl_list.selected=e.length-1)})}async openFile(){try{let t;t=this.args&&this.args.length>0?this.args[0].path.asFileHandle():(await this.openDialog("FileDialog",{title:__("Open file"),mimes:this.meta().mimes})).file.path.asFileHandle(),this.filehandle=("sqlite://"+t.genealogy.join("/")).asFileHandle(),await this.filehandle.onready(),this.list_tables()}catch(t){this.error(__("Unable to open database file: {0}",t.toString()),t)}}async newFile(){try{const e=await this.openDialog("FileDialog",{title:__("Save as"),file:"Untitled.db"});var t=e.file.path.asFileHandle();"file"===e.file.type&&(t=t.parent());const i=`${t.path}/${e.name}`.asFileHandle();this.filehandle=("sqlite://"+i.genealogy.join("/")).asFileHandle(),await this.filehandle.onready(),this.list_tables()}catch(t){this.error(__("Unable to init database file: {0}",t.toString()),t)}}main(){this.filehandle=void 0,this.tbl_list=this.find("tbl-list"),this.grid_table=this.find("tb-browser"),this.grid_scheme=this.find("sch-browser"),this.grid_table.resizable=!0,this.grid_scheme.resizable=!0,this.grid_scheme.header=[{text:__("Field name")},{text:__("Field type")}],this.btn_loadmore=this.find("bt-load-next"),this.container=this.find("container"),this.bindKey("ALT-N",()=>this.newFile()),this.bindKey("ALT-O",()=>this.openFile()),this.container.ontabselect=t=>{if(0==this.container.selectedIndex){if(!this.tbl_list.selectedItem)return;const t=this.tbl_list.selectedItem.data.handle.info.schema;if(!t)return;const e=[];for(let i in t.types)e.push([{text:i},{text:t.types[i]}]);this.grid_scheme.rows=e}},this.find("bt-rm-table").onbtclick=async t=>{try{if(!this.filehandle)return this.notify(__("Please open a database file"));if(null==this.tbl_list.selectedItem)return;const t=this.tbl_list.selectedItem.data.name;await this.openDialog("YesNoDialog",{title:__("Confirm delete?"),text:__("Do you realy want to delete table: {0}",t)})&&(await this.filehandle.remove(t),this.list_tables())}catch(t){this.error(__("Unable to execute action table delete: {0}",t.toString()),t)}},this.find("bt-add-table").onbtclick=async t=>{try{if(!this.filehandle)return this.notify(__("Please open a database file"));const t=await this.openDialog(new a,{title:__("Create new table")});this.filehandle.cache=t.schema,await this.filehandle.write(t.name),this.list_tables()}catch(t){this.error(__("Unable to create table: {0}",t.toString()),t)}},this.find("btn-edit-record").onbtclick=async t=>{this.edit_record()},this.find("btn-add-record").onbtclick=async t=>{this.add_record()},this.find("btn-delete-record").onbtclick=async t=>{this.remove_record()},this.btn_loadmore.onbtclick=async t=>{try{await this.load_table()}catch(t){this.error(__("Error reading table: {0}",t.toString()),t)}},this.tbl_list.onlistselect=async t=>{try{if(!this.tbl_list.selectedItem)return;const t=this.tbl_list.selectedItem.data.handle;await t.onready(),this.last_max_id=0;const e=t.info.schema.fields.map(t=>({text:t}));this.grid_table.header=e,this.grid_table.rows=[];const i=await t.read({fields:["COUNT(*)"]});this.n_records=i[0]["COUNT(*)"],this.btn_loadmore.text="0/"+this.n_records,await this.load_table(),this.container.selectedIndex=1}catch(t){this.error(__("Error reading table: {0}",t.toString()),t)}},this.grid_table.oncelldbclick=async t=>{this.edit_record()},this.openFile()}async add_record(){try{const t=this.tbl_list.selectedItem;if(!t)return;const e=t.data.handle,i=t.data.handle.info.schema,a={};for(let t in i.types)["INTEGER","REAL","NUMERIC"].includes(i.types[t])?a[t]=0:a[t]="";console.log(a);const n=await this.openDialog(new s,{title:__("New record"),schema:i,record:a});e.cache=n,await e.write(void 0),this.n_records+=1,await this.load_table()}catch(t){this.error(__("Error edit/view record: {0}",t.toString()),t)}}async remove_record(){try{const t=this.grid_table.selectedCell,e=this.grid_table.selectedRow,i=this.tbl_list.selectedItem;if(!t||!i)return;const a=t.data.record[i.data.handle.info.schema.pk];if(!await this.openDialog("YesNoDialog",{title:__("Delete record"),text:__("Do you realy want to delete record {0}",a)}))return;const s=`${i.data.handle.path}@${a}`.asFileHandle();await s.remove(),this.n_records--,this.grid_table.delete(e),this.btn_loadmore.text=`${this.grid_table.rows.length}/${this.n_records}`}catch(t){this.error(__("Error deleting record: {0}",t.toString()),t)}}async edit_record(){try{const t=this.grid_table.selectedCell,e=this.grid_table.selectedRow,i=this.tbl_list.selectedItem;if(!t||!i)return;const a=await this.openDialog(new s,{title:__("View/edit record"),schema:i.data.handle.info.schema,record:t.data.record}),n=t.data.record[i.data.handle.info.schema.pk],l=`${i.data.handle.path}@${n}`.asFileHandle();l.cache=a,await l.write(void 0);const d=[];for(let t of l.info.schema.fields){let e=a[t];e.length>100&&(e=e.substring(0,100)),d.push({text:e,record:a})}e.data=d}catch(t){this.error(__("Error edit/view record: {0}",t.toString()),t)}}async load_table(){if(this.grid_table.rows&&this.grid_table.rows.length>=this.n_records)return;if(!this.tbl_list.selectedItem)return;const t=this.tbl_list.selectedItem.data.handle;await t.onready();const e=t.info.schema.fields.map(t=>({text:t})),i={where:{},limit:10};i.where[t.info.schema.pk+"$gt"]=this.last_max_id;const a=await t.read(i);if(a&&a.length>0){for(let t of a){const i=[];t.id&&t.id>this.last_max_id&&(this.last_max_id=t.id);for(let a in e){let s=t[e[a].text];s.length>100&&(s=s.substring(0,100)),i.push({text:s,record:t})}this.grid_table.push(i,!1)}this.grid_table.scroll_to_bottom()}this.btn_loadmore.text=`${this.grid_table.rows.length}/${this.n_records}`}}e.SQLiteDBBrowser=i,i.dependencies=["pkg://SQLiteDB/libsqlite.js"];class a extends t.GUI.BasicDialog{constructor(){super("NewTableDialog")}main(){super.main(),this.container=this.find("container"),this.find("btnCancel").onbtclick=t=>this.quit(),this.find("btnAdd").onbtclick=t=>this.addField(),$(this.container).css("overflow-y","auto"),this.addField(),this.find("btnOk").onbtclick=t=>{const e=this.find("txt-tblname");if(!e.value||""==e.value)return this.notify(__("Please enter table name"));const i=e.value,a=$("input",this.container),s=$("afx-list-view",this.container);if(0==a.length)return this.notify(__("Please define table fields"));let n={};for(let t=0;t").css("display","flex").css("flex-direction","row").appendTo(this.container);$("").attr("type","text").css("flex","1").appendTo(t);let e=$("").css("flex","1").appendTo(t)[0];e.uify(this.observable),e.dropdown=!0,e.data=[{text:"TEXT"},{text:"INTEGER"},{text:"REAL"},{text:"NUMERIC"}],e.selected=0;const i=$("");i[0].uify(void 0),i[0].iconclass="fa fa-minus",i.on("click",()=>{t.remove()}).appendTo(t)}}a.scheme='\n \n \n \n
\n \n \n
\n \n \n
\n
\n
\n
';class s extends t.GUI.BasicDialog{constructor(){super("RecordEditDialog")}main(){if(super.main(),this.container=this.find("container"),this.find("btnCancel").onbtclick=t=>this.quit(),$(this.container).css("overflow-y","auto"),!this.data||!this.data.schema)throw new Error(__("No data provided for dialog").__());for(let t in this.data.schema.types){const e=$("").appendTo(this.container)[0];e.uify(this.observable),e.label=t,t==this.data.schema.pk&&(e.disable=!0),"TEXT"==this.data.schema.types[t]&&(e.verbose=!0,$(e).css("height","100px")),null!=this.data.record[t]&&(e.value=this.data.record[t])}this.find("btnOk").onbtclick=t=>{const e=$("afx-input",this.container),i={};for(let t of e)i[t.label.__()]=t.value;this.handle&&this.handle(i),this.quit()}}}s.scheme='\n \n
\n \n
\n
\n \n \n
\n
\n
\n
'}(e=t.application||(t.application={}))}(OS||(OS={})); \ No newline at end of file diff --git a/SQLiteDB/build/release/SQLiteDB.zip b/SQLiteDB/build/release/SQLiteDB.zip new file mode 100644 index 0000000..5a3e81e Binary files /dev/null and b/SQLiteDB/build/release/SQLiteDB.zip differ diff --git a/SQLiteDB/main.ts b/SQLiteDB/main.ts index 1a792b5..74b53d0 100644 --- a/SQLiteDB/main.ts +++ b/SQLiteDB/main.ts @@ -86,11 +86,21 @@ namespace OS { } private async openFile() { try { - const d_1 = await this.openDialog("FileDialog",{ - title: __("Open file"), - mimes: this.meta().mimes - }); - this.filehandle=`sqlite://${d_1.file.path.asFileHandle().genealogy.join("/")}`.asFileHandle(); + let file: API.VFS.BaseFileHandle; + if (this.args && this.args.length > 0) { + file = this.args[0].path.asFileHandle(); + } + else + { + const d_1 = await this.openDialog("FileDialog",{ + title: __("Open file"), + mimes: this.meta().mimes + }); + file = d_1.file.path.asFileHandle(); + } + + + this.filehandle=`sqlite://${file.genealogy.join("/")}`.asFileHandle(); await this.filehandle.onready(); this.list_tables(); } @@ -239,7 +249,7 @@ namespace OS { this.grid_table.header = headers; this.grid_table.rows = []; const records = await handle.read({fields:["COUNT(*)"]}); - this.n_records = records[0]["(COUNT(*))"]; + this.n_records = records[0]["COUNT(*)"]; this.btn_loadmore.text = `0/${this.n_records}`; await this.load_table(); this.container.selectedIndex = 1; @@ -252,6 +262,7 @@ namespace OS { this.grid_table.oncelldbclick = async (e) => { this.edit_record(); } + this.openFile(); } private async add_record()