diff --git a/Blogger/README.md b/Blogger/README.md index 8b57be7..b6f783d 100644 --- a/Blogger/README.md +++ b/Blogger/README.md @@ -6,6 +6,7 @@ Blackend for my blog at https://blog.iohub.dev ## Change logs ### v0.2.x-a +* Patch 10: Migrate code to typescript, use SQLiteDB lib for database access * Patch 9: Update to use the new MDE library * Patch 8: Support for antOS 2.0.x * Patch 7: Fix sendmail API security bug diff --git a/Blogger/build/debug/README.md b/Blogger/build/debug/README.md index 8b57be7..b6f783d 100644 --- a/Blogger/build/debug/README.md +++ b/Blogger/build/debug/README.md @@ -6,6 +6,7 @@ Blackend for my blog at https://blog.iohub.dev ## Change logs ### v0.2.x-a +* Patch 10: Migrate code to typescript, use SQLiteDB lib for database access * Patch 9: Update to use the new MDE library * Patch 8: Support for antOS 2.0.x * Patch 7: Fix sendmail API security bug diff --git a/Blogger/build/debug/main.js b/Blogger/build/debug/main.js index 719622c..59d4cd8 100644 --- a/Blogger/build/debug/main.js +++ b/Blogger/build/debug/main.js @@ -1,1180 +1 @@ - -// 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 = {})); +var OS;!function(t){let e;!function(t){class e extends t.BaseApplication{constructor(t){super("Blogger",t),this.previewOn=!1}async init_db(){try{const e=await this.openDialog("FileDialog",{title:__("Open/create new database"),file:"Untitled.db"});var t=e.file.path.asFileHandle();"file"===e.file.type&&(t=t.parent());const i=`${t.path}/${e.name}`.asFileHandle();this.dbhandle=("sqlite://"+i.genealogy.join("/")).asFileHandle();const a=await this.dbhandle.read();if(!a.user){this.dbhandle.cache={address:"TEXT",Phone:"TEXT",shortbiblio:"TEXT",fullname:"TEXT",email:"TEXT",url:"TEXT",photo:"TEXT"};const t=await this.dbhandle.write("user");if(t.error)throw new Error(t.error)}if(!a.cv_cat){this.dbhandle.cache={publish:"NUMERIC",name:"TEXT",pid:"NUMERIC"};const t=await this.dbhandle.write("cv_cat");if(t.error)throw new Error(t.error)}if(!a.cv_sections){this.dbhandle.cache={title:"TEXT",start:"NUMERIC",location:"TEXT",end:"NUMERIC",content:"TEXT",subtitle:"TEXT",publish:"NUMERIC",cid:"NUMERIC"};const t=await this.dbhandle.write("cv_sections");if(t.error)throw new Error(t.error)}if(!a.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 t=await this.dbhandle.write("blogs");if(t.error)throw new Error(t.error)}if(!a.st_similarity){this.dbhandle.cache={pid:"NUMERIC",sid:"NUMERIC",score:"NUMERIC"};const t=await this.dbhandle.write("st_similarity");if(t.error)throw new Error(t.error)}if(!a.subscribers){this.dbhandle.cache={name:"TEXT",email:"TEXT"};const t=await this.dbhandle.write("subscribers");if(t.error)throw new Error(t.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(),this.last_ctime=0,this.bloglist.data=[],this.loadBlogs()}catch(t){this.error(__("Unable to init database file: {0}",t.toString()),t),this.dbhandle=void 0}}menu(){return[{text:"__(Open/Create database)",onmenuselect:t=>{this.init_db()}}]}main(){this.user={},this.cvlist=this.find("cv-list"),this.cvlist.ontreeselect=t=>{if(!t)return;const{data:e}=t.data.item;return this.CVSectionByCID(Number(e.id))},this.inputtags=this.find("input-tags"),this.bloglist=this.find("blog-list"),this.seclist=this.find("cv-sec-list");let e=this.find("photo");return $(e).on("click",async t=>{try{const t=await this.openDialog("FileDialog",{title:__("Select image file"),mimes:["image/.*"]});return e.value=t.file.path}catch(t){return this.error(__("Unable to get file"),t)}}),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("blog-load-more").onbtclick=t=>{this.loadBlogs()},this.find("cv-cat-add").onbtclick=async e=>{try{const e=await this.fetchCVCat(),i=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Add category"),tree:e});this.cvcatdb.cache={name:i.value,pid:i.p.id,publish:1};const a=await this.cvcatdb.write(void 0);if(a.error)throw new Error(a.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 e=this.cvlist.selectedItem;if(!e)return;const i=e.data;if(!i)return;const a=await this.fetchCVCat(),s=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Edit category"),tree:a,cat:i}),n=i.$vfs;n.cache={id:i.id,publish:i.publish,pid:s.p.id,name:s.value};const r=await n.write(void 0);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 t=>{try{const t=this.cvlist.selectedItem;if(!t)return;const e=t.data;if(!e)return;if(!await this.openDialog("YesNoDialog",{title:__("Delete category"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.name)}))return;await this.deleteCVCat(e)}catch(t){this.error(__("cv-cat-del: {0}",t.toString()),t)}},this.find("cv-sec-add").onbtclick=async e=>{try{const e=this.cvlist.selectedItem;if(!e)return;const i=e.data;if(!i||"0"===i.id)return this.toast(__("Please select a category"));const a=await this.openDialog(new t.blogger.BloggerCVSectionDiaglog,{title:__("New section entry for {0}",i.name)});a.cid=Number(i.id),a.start=Number(a.start),a.end=Number(a.end),this.cvsecdb.cache=a;const s=await this.cvsecdb.write(void 0);if(s.error)throw new Error(s.error);await this.CVSectionByCID(Number(i.id))}catch(e){this.error(__("cv-sec-add: {0}",e.toString()),e)}},this.find("cv-sec-move").onbtclick=async e=>{try{const e=this.seclist.selectedItem;if(!e)return this.toast(__("Please select a section to move"));const i=e.data,a=i.$vfs;console.log(a);const s=await this.fetchCVCat(),n=await this.openDialog(new t.blogger.BloggerCategoryDialog,{title:__("Move to"),tree:s,selonly:!0});a.cache={id:i.id,cid:n.p.id};const r=await a.write(void 0);if(r.error)throw new Error(r.error);await this.CVSectionByCID(i.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 e=this.seclist.selectedItem;if(!e)return this.toast(__("Please select a section to edit"));const i=e.data,a=await this.openDialog(new t.blogger.BloggerCVSectionDiaglog,{title:__("Modify section entry"),section:i});a.cid=Number(i.cid),a.start=Number(a.start),a.end=Number(a.end);const s=i.$vfs;s.cache=a;const n=await s.write(void 0);if(n.error)throw new Error(n.error);await this.CVSectionByCID(Number(i.cid))}catch(e){this.error(__("cv-sec-edit: {0}",e.toString()),e)}},this.seclist.onitemclose=t=>{if(!t)return;const e=t.data.item.data;return this.openDialog("YesNoDialog",{iconclass:"fa fa-question-circle",text:__("Do you really want to delete: {0}?",e.title)}).then(async i=>{if(i)try{const i=await this.cvsecdb.remove({where:{id:e.id}});if(i.error)throw new Error(i.error);return this.seclist.delete(t.data.item)}catch(t){return 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:async e=>{try{const e=await this.subdb.read(),i=this.bloglist.selectedItem;if(!i)return this.error(__("No post selected"));const a=i.data;await this.openDialog(new t.blogger.BloggerSendmailDiaglog,{title:__("Send mail"),content:this.editor.value(),mails:e,id:a.id}),this.toast(__("Emails sent"))}catch(e){this.error(__("Error sending mails: {0}",e.toString()),e)}}}]}),this.bloglist.onlistselect=async t=>{const e=this.bloglist.selectedItem;if(!e)return;const i=e.data;if(i)try{const t=await this.blogdb.read({where:{id:Number(i.id)}});if(!t||0==t.length)throw new Error(__("No record found for ID {}",i.id).__());const e=t[0];return this.editor.value(e.content),this.inputtags.value=e.tags,this.find("blog-publish").swon=!!Number(e.publish)}catch(t){return this.error(__("Cannot fetch the entry content"),t)}},this.bloglist.onitemclose=t=>{if(!t)return;const e=t.data.item,i=e.data;return this.openDialog("YesNoDialog",{title:__("Delete a post"),iconclass:"fa fa-question-circle",text:__("Do you really want to delete this post ?")}).then(async t=>{if(!t)return;const a=await this.blogdb.remove({where:{id:Number(i.id)}});if(a.error)throw new Error(a.error);return this.bloglist.delete(e),this.bloglist.unselect(),this.clearEditor()}),!1},this.bindKey("CTRL-S",()=>{const t=this.tabcontainer.selectedTab;if(t&&"blog-container"===t.container.aid)return this.saveBlog()}),this.on("resize",()=>this.resizeContent()),this.resizeContent(),this.init_db()}fetchData(t){switch(t){case"user-container":return this.userdb.read().then(t=>{if(t&&0!=t.length)return this.user=t[0],this.select("[input-class='user-input']").map((t,e)=>$(e).val(this.user[e.name]))}).catch(t=>this.error(__("Cannot fetch user data"),t));case"cv-container":return this.refreshCVCat();default:return this.last_ctime=0,this.bloglist.data=[],this.loadBlogs()}}async saveUser(){try{const t=this.select("[input-class='user-input']");for(let e of t)this.user[e.name]=$(e).val();if(!this.user.fullname||""===this.user.fullname)return this.toast(__("Full name must be entered"));let e=this.userdb;this.user&&this.user.id&&(e=`${this.userdb.path}@${this.user.id}`.asFileHandle()),e.cache=this.user;const i=await e.write(void 0);if(i.error)throw new Error(i.error);this.user.id||(this.user.id=i.result),this.toast(__("User data updated"))}catch(t){this.error(__("Cannot save user data: {0}",t.toString()),t)}}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(async(t,e)=>{try{const e={text:"Porfolio",id:0,nodes:[]},i={order:["name$asc"]},a=await this.cvcatdb.read(i);this.catListToTree(a,e,0),t(e)}catch(t){e(__e(t))}})}catListToTree(t,e,i){const a=t.filter(t=>t.pid==i);if(0===a.length)return e.nodes=null;for(let i of a)i.nodes=[],i.text=i.name,this.catListToTree(t,i,i.id),e.nodes.push(i)}deleteCVCat(t){return new Promise(async(e,i)=>{try{const e=[];var a=function(t){e.push(t.id),t.nodes&&t.nodes.map(t=>a(t))};a(t);let i=await this.cvsecdb.remove({where:{$or:{cid:e}}});if(i.error)throw new Error(i.error);if(i=await this.cvcatdb.remove({where:{$or:{id:e}}}),i.error)throw new Error(i.error);await this.refreshCVCat(),this.seclist.data=[]}catch(t){i(__e(t))}})}CVSectionByCID(t){return new Promise(async(e,i)=>{try{const e=await this.cvsecdb.read({where:{cid:t},order:["start$desc"]}),i=[];this.find("cv-sec-status").text=__("Found {0} sections",e.length);for(let t of e)t.closable=!0,t.tag="afx-blogger-cvsection-item",t.start=Number(t.start),t.end=Number(t.end),t.start<1e3&&(t.start=void 0),t.end<1e3&&(t.end=void 0),i.push(t);this.seclist.data=i}catch(t){i(__e(t))}})}async saveBlog(){try{let t=void 0;const e=this.bloglist.selectedItem;e&&(t=e.data);const i=this.inputtags.value,a=this.editor.value(),s=new RegExp("^#+(.*)\n","g").exec(a);if(!s||2!==s.length)return this.toast(__("Please insert a title in the text: beginning with heading"));if(""===i)return this.toast(__("Please enter tags"));const n=new Date,r={content:a,title:s[1].trim(),tags:i,ctime:t?t.ctime:n.timestamp(),ctimestr:t?t.ctimestr:n.toString(),utime:n.timestamp(),utimestr:n.toString(),rendered:this.process(this.editor.options.previewRender(a)),publish:this.find("blog-publish").swon?1:0};let o=this.blogdb;t&&(r.id=t.id,o=t.$vfs),o.cache=r;const l=await o.write(void 0);if(l.error)throw new Error(l.error);t?e.data=r:(this.last_ctime=0,this.bloglist.data=[],await this.loadBlogs())}catch(t){this.error(__("Cannot save blog: {0}",t.toString()),t)}}process(t){let e;const i=/\[\[youtube:([^\]]*)\]\]/g,a=[];for(;null!==(e=i.exec(t));)a.push(e);if(!(a.length>0))return t;let s="",n=0;for(let e of a)s+=t.substring(n,e.index),s+=``,n=e.index+e[0].length;return s+=t.substring(n,t.length),s}clearEditor(){return this.editor.value(""),this.inputtags.value="",this.find("blog-publish").swon=!1}loadBlogs(){return new Promise(async(t,e)=>{try{const t={order:["ctime$desc"],fields:["id","title","ctimestr","ctime","utime","utimestr"],limit:10};this.last_ctime&&(t.where={ctime$lt:this.last_ctime});const e=await this.blogdb.read(t);if(0==e.length)return void this.toast(__("No more record to load"));this.last_ctime=e[e.length-1].ctime;for(let t of e)t.tag="afx-blogger-post-item",this.bloglist.push(t);return this.clearEditor(),this.bloglist.selected=-1}catch(t){e(__e(t))}})}resizeContent(){const t=this.find("editor-container"),e=$(".EasyMDEContainer",t).children(),i=$(this.scheme).find(".afx-window-top")[0],a=e[0],s=e[3],n=$(this.scheme).height()-$(i).height()-$(a).height()-$(s).height()-90;return $(e[1]).css("height",n+"px")}}t.Blogger=e,e.singleton=!0,e.dependencies=["pkg://SimpleMDE/main.js","pkg://SimpleMDE/main.css","pkg://Katex/main.js","pkg://Katex/main.css","pkg://SQLiteDB/libsqlite.js"]}(e=t.application||(t.application={}))}(OS||(OS={})),function(t){let e;!function(e){let i;!function(e){class i extends t.GUI.BasicDialog{constructor(){super("BloggerCategoryDialog",i.scheme)}main(){if(super.main(),this.tree=this.find("tree"),this.txtinput=this.find("txtinput"),this.find("bt-ok").onbtclick=t=>{const e=this.tree.selectedItem;if(!e)return this.notify(__("Please select a parent category"));const i=e.data,a=this.txtinput.value;return""!==a||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:a}),this.quit()):this.notify(__("Please enter category name"))},this.find("bt-cancel").onbtclick=t=>this.quit(),this.data&&this.data.tree){if(this.data&&this.data.cat){let t;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&&(t.selected=!0)}return this.tree.data=this.data.tree,this.tree.expandAll()}}findDataByID(t,e){for(let i of e){if(i.id===t)return i;i.nodes&&this.findDataByID(t,i.nodes)}}}e.BloggerCategoryDialog=i,i.scheme='\n \n \n \n \n \n \n
\n \n \n
\n
\n
\n
s\n ';class a extends t.GUI.BasicDialog{constructor(){super("BloggerCVSectionDiaglog")}main(){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");const t=this.select("[input-class='user-input']");if(this.data&&this.data.section)for(let e of t)$(e).val(this.data.section[e.name]);return 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)),this.find("bt-cv-sec-save").onbtclick=e=>{const i={};for(let e of t)i[e.name]=$(e).val();if(i.content=this.editor.value(),""===i.title&&""===i.content)return this.notify(__("Title or content must not be blank"));this.data&&this.data.section&&(i.id=this.data.section.id);const a=this.find("section-publish").swon;return i.publish=!0===a?1:0,this.handle&&this.handle(i),this.quit()},this.on("resize",()=>this.resizeContent()),this.resizeContent()}resizeContent(){const t=this.find("editor-container"),e=$(".EasyMDEContainer",t).children(),i=$(t).height()-30;return $(e[0]).css("height",i+"px")}}e.BloggerCVSectionDiaglog=a,a.scheme='\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n
\n
\n \n \n
\n
\n
';class s extends t.GUI.BasicDialog{constructor(){super("BloggerSendmailDiaglog")}main(){super.main(),this.maillinglist=this.find("email-list");const t=new RegExp("^#+(.*)\n","g").exec(this.data.content);this.find("mail-title").value=t[1];const e=this.data.content.substring(0,500)+"...";this.find("contentarea").value=s.template.format(this.data.id,e);const i=this.data.mails.map(t=>({text:t.name,email:t.email,switch:!0,checked:!0}));return this.maillinglist.data=i,this.find("bt-sendmail").onbtclick=t=>{const e=this.maillinglist.data,i=[];for(let t of e)!0===t.checked&&(console.log(t.email),i.push(t.email));if(0===i.length)return this.notify(__("No email selected"));const a={path:this.meta().path+"/sendmail.lua",parameters:{to:i,title:this.find("mail-title").value,content:this.find("contentarea").value}};return this._api.apigateway(a,!1).then(t=>t.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)))}}}e.BloggerSendmailDiaglog=s,s.scheme='\n \n \n \n
\n \n
\n \n \n \n \n
\n \n
\n \n
\n
\n
\n
\n
',s.template="Hello,\n\nXuan Sang LE has just published a new post on his blog: https://blog.iohub.dev/post/id/{0}\n\n==========\n{1}\n==========\n\n\nRead the full article via:\nhttps://blog.iohub.dev/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"}(i=e.blogger||(e.blogger={}))}(e=t.application||(t.application={}))}(OS||(OS={})),function(t){let e;!function(e){let i;!function(e){class i extends t.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){if(!this.data)return;const t=this.data,e=["content","start","end"];return this.closable=t.closable,(()=>{const i=[];for(let a in this.refs){const s=this.refs[a];t[a]&&""!==t[a]?e.includes(a)?i.push($(s).text(t[a])):i.push(s.text=t[a]):i.push(void 0)}return i})()}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"}]}}}t.GUI.tag.define("afx-blogger-cvsection-item",i);class a extends t.GUI.tag.ListViewItemTag{constructor(){super()}ondatachange(){if(!this.data)return;const t=this.data;t.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"}]}}}t.GUI.tag.define("afx-blogger-post-item",a)}(i=e.blogger||(e.blogger={}))}(e=t.application||(t.application={}))}(OS||(OS={})); \ No newline at end of file diff --git a/Blogger/build/debug/package.json b/Blogger/build/debug/package.json index 0fcd2be..67e3f7b 100644 --- a/Blogger/build/debug/package.json +++ b/Blogger/build/debug/package.json @@ -6,7 +6,7 @@ "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version": "0.2.9-a", + "version": "0.2.10-a", "category": "Internet", "iconclass": "fa fa-book", "dependencies": [ @@ -36,7 +36,6 @@ "No email selected": "No email selected", "Unable to send mail to: {0}": "Unable to send mail to: {0}", "Error sending mail: {0}": "Error sending mail: {0}", - "Cannot fetch subscribers data: {0}": "Cannot fetch subscribers data: {0}", "Open/create new database": "Open/create new database", "Unable to init database file: {0}": "Unable to init database file: {0}", "Select image file": "Select image file", @@ -65,28 +64,20 @@ "No post selected": "No post selected", "Emails sent": "Emails sent", "Error sending mails: {0}": "Error sending mails: {0}", + "No record found for ID {}": "No record found for ID {}", "Cannot fetch the entry content": "Cannot fetch the entry content", "Delete a post": "Delete a post", "Do you really want to delete this post ?": "Do you really want to delete this post ?", "Cannot fetch user data": "Cannot fetch user data", "Full name must be entered": "Full name must be entered", "User data updated": "User data updated", - "Cannot save user data": "Cannot save user data", + "Cannot save user data: {0}": "Cannot save user data: {0}", "Unable to load categories": "Unable to load categories", "Found {0} sections": "Found {0} sections", "Please insert a title in the text: beginning with heading": "Please insert a title in the text: beginning with heading", "Please enter tags": "Please enter tags", "Cannot save blog: {0}": "Cannot save blog: {0}", - "Cannot add new category": "Cannot add new category", - "Unable to fetch categories": "Unable to fetch categories", - "Cannot Edit category": "Cannot Edit category", - "Cannot save section: {0}": "Cannot save section: {0}", - "Cannot move section": "Cannot move section", - "Cannot delete the category: {0} [{1}]": "Cannot delete the category: {0} [{1}]", - "Cannot delete all content of: {0} [{1}]": "Cannot delete all content of: {0} [{1}]", - "No post found: {0}": "No post found: {0}", - "Created: {0}": "Created: {0}", - "Updated: {0}": "Updated: {0}", + "No more record to load": "No more record to load", "Full name": "Full name", "Address": "Address", "Phone": "Phone", @@ -95,7 +86,10 @@ "Photo": "Photo", "Short biblio": "Short biblio", "Categories": "Categories", - "Tags": "Tags" + "Load more": "Load more", + "Tags": "Tags", + "Created: {0}": "Created: {0}", + "Updated: {0}": "Updated: {0}" } } } \ No newline at end of file diff --git a/Blogger/build/debug/scheme.html b/Blogger/build/debug/scheme.html index cf8ed01..e10b98d 100644 --- a/Blogger/build/debug/scheme.html +++ b/Blogger/build/debug/scheme.html @@ -1,5 +1,5 @@ - + @@ -65,7 +65,10 @@ - + + + +
diff --git a/Blogger/build/release/Blogger.zip b/Blogger/build/release/Blogger.zip index 4ab5068..ea7d5ea 100644 Binary files a/Blogger/build/release/Blogger.zip and b/Blogger/build/release/Blogger.zip differ diff --git a/Blogger/cvsection.html b/Blogger/cvsection.html deleted file mode 100644 index 51d6928..0000000 --- a/Blogger/cvsection.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -
- -
-
- - -
-
-
\ No newline at end of file diff --git a/Blogger/dialogs.coffee b/Blogger/dialogs.coffee deleted file mode 100644 index 630d9de..0000000 --- a/Blogger/dialogs.coffee +++ /dev/null @@ -1,189 +0,0 @@ -# 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/. -class BloggerCategoryDialog extends this.OS.GUI.BasicDialog - constructor: () -> - super "BloggerCategoryDialog", BloggerCategoryDialog.scheme - - main: () -> - super.main() - @tree = @find "tree" - @txtinput = @find "txtinput" - - (@find "bt-ok").onbtclick = (e) => - sel = @tree.selectedItem - return @notify __("Please select a parent category") unless sel - seldata = sel.data - val = @txtinput.value - return @notify __("Please enter category name") if val is "" and not @data.selonly - return @notify __("Parent can not be the category itself") if @data.cat and @data.cat.id is seldata.id - @handle { p: seldata, value: val } if @handle - @quit() - - (@find "bt-cancel").onbtclick = (e) => - @quit() - if @data and @data.tree - if @data and @data.cat - @txtinput.value = @data.cat.name - if @data.cat.pid is "0" - seldata = @data.tree - else - seldata = @findDataByID @data.cat.pid, @data.tree.nodes - seldata.selected = true if seldata - @tree.data = @data.tree - @tree.expandAll() - # TODO set selected category name - - findDataByID: (id, list) -> - for data in list - return data if data.id is id - if data.nodes - @findDataByID id, data.nodes - return undefined - -BloggerCategoryDialog.scheme = """ - - - - - - - -
- - -
-
-
-
-""" - -# This dialog is use for cv section editing - -class BloggerCVSectionDiaglog extends this.OS.GUI.BasicDialog - constructor: (parent) -> - file = "#{parent.meta().path}/cvsection.html".asFileHandle() - super "BloggerCVSectionDiaglog", file - - main: () -> - super.main() - @editor = new EasyMDE - autoDownloadFontAwesome: false - element: @find "contentarea" - status: false - toolbar: false - ($ (@select '[class = "CodeMirror-scroll"]')[0]).css "min-height", "50px" - ($ (@select '[class="CodeMirror cm-s-paper CodeMirror-wrap"]')[0]).css "min-height", "50px" - inputs = @select "[input-class='user-input']" - (($ v).val @data.section[v.name] for v in inputs ) if @data and @data.section - @editor.value @data.section.content if @data and @data.section - (@find "section-publish").swon = (if @data and @data.section and Number(@data.section.publish) then true else false) - (@find "bt-cv-sec-save").onbtclick = (e) => - data = {} - data[v.name] = ($ v).val() for v in inputs - data.content = @editor.value() - return @notify __("Title or content must not be blank") if data.title is "" and data.content is "" - #return @notify "Content must not be blank" if data.content is "" - data.id = @data.section.id if @data and @data.section - val = (@find "section-publish").swon - if val is true - data.publish = 1 - else - data.publish = 0 - @handle data if @handle - @quit() - - @on "resize", () => @resizeContent() - @resizeContent() - - resizeContent: () -> - container = @find "editor-container" - children = ($ ".EasyMDEContainer", container).children() - cheight = ($ container).height() - 30 - ($ children[0]).css("height", cheight + "px") - - -# this dialog is for send mail -class BloggerSendmailDiaglog extends this.OS.GUI.BasicDialog - constructor: (parent) -> - file = "#{parent.meta().path}/sendmail.html".asFileHandle() - super "BloggerSendmailDiaglog", file - - main: () -> - super.main() - @subdb = new @.parent._api.DB("subscribers") - @maillinglist = @find "email-list" - title = (new RegExp "^#+(.*)\n", "g").exec @data.content - (@find "mail-title").value = title[1] - content = (@data.content.substring 0, 500) + "..." - (@find "contentarea").value = BloggerSendmailDiaglog.template.format @data.id, content - - @subdb.find {} - .then (d) => - for v in d - v.text = v.name - v.switch = true - v.checked = true - @maillinglist. items = d - .catch (e) => - @error __("Cannot fetch subscribers data: {0}", e.toString()), e - - (@find "bt-sendmail").onbtclick = (e) => - items = @maillinglist.items - emails = [] - for v in items - if v.checked is true - console.log v.email - emails.push v.email - - return @notify __("No email selected") if emails.length is 0 - # send the email - data = - path: "#{@parent.path()}/sendmail.lua", - parameters: - to: emails, - title: (@find "mail-title").value, - content: (@find "contentarea").value - @_api.apigateway data, false - .then (d) => - return @notify __("Unable to send mail to: {0}", d.result.join(",")) if d.error - @quit() - .catch (e) => - console.log e - @error __("Error sending mail: {0}", e.toString()), e - - - -BloggerSendmailDiaglog.template = """ -Hello, - -Xuan Sang LE has just published a new post on his blog: https://blog.lxsang.me/post/id/{0} - -========== -{1} -========== - - -Read the full article via: -https://blog.lxsang.me/post/id/{0} - -You receive this email because you have been subscribed to his blog. - -Have a nice day, - -Sent from Blogger, an AntOS application -""" \ No newline at end of file diff --git a/Blogger/dialogs.ts b/Blogger/dialogs.ts new file mode 100644 index 0000000..8453425 --- /dev/null +++ b/Blogger/dialogs.ts @@ -0,0 +1,278 @@ +// 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/. +namespace OS { + export namespace application { + export namespace blogger { + declare var EasyMDE; + export class BloggerCategoryDialog extends OS.GUI.BasicDialog { + private tree: OS.GUI.tag.TreeViewTag; + private txtinput: HTMLInputElement; + constructor() { + super("BloggerCategoryDialog", BloggerCategoryDialog.scheme); + } + + main() { + super.main(); + this.tree = this.find("tree") as OS.GUI.tag.TreeViewTag; + this.txtinput = this.find("txtinput") as HTMLInputElement; + + (this.find("bt-ok") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => { + const sel = this.tree.selectedItem; + if (!sel) { return this.notify(__("Please select a parent category")); } + const seldata = sel.data; + const val = this.txtinput.value; + if ((val === "") && !this.data.selonly) { return this.notify(__("Please enter category name")); } + if (this.data.cat && (this.data.cat.id === seldata.id)) { return this.notify(__("Parent can not be the category itself")); } + if (this.handle) { this.handle({ p: seldata, value: val }); } + return this.quit(); + }; + + (this.find("bt-cancel") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => { + return this.quit(); + }; + if (this.data && this.data.tree) { + if (this.data && this.data.cat) { + let seldata: GenericObject; + this.txtinput.value = this.data.cat.name; + if (this.data.cat.pid === "0") { + seldata = this.data.tree; + } else { + seldata = this.findDataByID(this.data.cat.pid, this.data.tree.nodes); + } + if (seldata) { seldata.selected = true; } + } + this.tree.data = this.data.tree; + return this.tree.expandAll(); + } + } + // TODO set selected category name + + findDataByID(id: number, list: GenericObject[]) { + for (let data of list) { + if (data.id === id) { return data; } + if (data.nodes) { + this.findDataByID(id, data.nodes); + } + } + return undefined; + } + } + + BloggerCategoryDialog.scheme = `\ + + + + + + + +
+ + +
+
+
+
\s + `; + + // This dialog is use for cv section editing + + export class BloggerCVSectionDiaglog extends OS.GUI.BasicDialog { + private editor: GenericObject; + constructor() { + super("BloggerCVSectionDiaglog"); + } + + main() { + super.main(); + this.editor = new EasyMDE({ + autoDownloadFontAwesome: false, + element: this.find("contentarea"), + status: false, + toolbar: false + }); + ($((this.select('[class = "CodeMirror-scroll"]'))[0])).css("min-height", "50px"); + ($((this.select('[class="CodeMirror cm-s-paper CodeMirror-wrap"]'))[0])).css("min-height", "50px"); + const inputs = this.select("[input-class='user-input']"); + if (this.data && this.data.section) { for (let v of inputs) { ($(v)).val(this.data.section[(v as HTMLInputElement).name]); } } + if (this.data && this.data.section) { this.editor.value(this.data.section.content); } + (this.find("section-publish") as OS.GUI.tag.SwitchTag).swon = (this.data && this.data.section && Number(this.data.section.publish) ? true : false); + (this.find("bt-cv-sec-save") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => { + const data: GenericObject = {}; + for (let v of inputs) { data[(v as HTMLInputElement).name] = ($(v)).val(); } + data.content = this.editor.value(); + if ((data.title === "") && (data.content === "")) { return this.notify(__("Title or content must not be blank")); } + //return @notify "Content must not be blank" if data.content is "" + if (this.data && this.data.section) { data.id = this.data.section.id; } + const val = (this.find("section-publish") as OS.GUI.tag.SwitchTag).swon; + if (val === true) { + data.publish = 1; + } else { + data.publish = 0; + } + if (this.handle) { this.handle(data); } + return this.quit(); + }; + + this.on("resize", () => this.resizeContent()); + return this.resizeContent(); + } + + resizeContent() { + const container = this.find("editor-container"); + const children = ($(".EasyMDEContainer", container)).children(); + const cheight = ($(container)).height() - 30; + return ($(children[0])).css("height", cheight + "px"); + } + } + BloggerCVSectionDiaglog.scheme = `\ + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+
+
`; + + // this dialog is for send mail + export class BloggerSendmailDiaglog extends OS.GUI.BasicDialog { + static template: string; + private maillinglist: OS.GUI.tag.ListViewTag; + // TODO: convert to SQLite handle + private subdb: API.VFS.BaseFileHandle; + constructor() { + super("BloggerSendmailDiaglog"); + } + + main() { + super.main(); + this.maillinglist = this.find("email-list") as OS.GUI.tag.ListViewTag; + const title = (new RegExp("^#+(.*)\n", "g")).exec(this.data.content); + (this.find("mail-title") as HTMLInputElement).value = title[1]; + const content = (this.data.content.substring(0, 500)) + "..."; + (this.find("contentarea") as HTMLTextAreaElement).value = BloggerSendmailDiaglog.template.format(this.data.id, content); + const mlist = this.data.mails.map((el)=> { + return { + text: el.name, + email: el.email, + switch: true, + checked: true + } + }) + this.maillinglist.data = mlist; + + return (this.find("bt-sendmail") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => { + const items = this.maillinglist.data; + const emails = []; + for (let v of items) { + if (v.checked === true) { + console.log(v.email); + emails.push(v.email); + } + } + + if (emails.length === 0) { return this.notify(__("No email selected")); } + // send the email + const data = { + path: `${this.meta().path}/sendmail.lua`, + parameters: { + to: emails, + title: (this.find("mail-title") as HTMLInputElement).value, + content: (this.find("contentarea") as HTMLTextAreaElement).value + } + }; + return this._api.apigateway(data, false) + .then((d: { error: any; result: { join: (arg0: string) => any; }; }) => { + if (d.error) { return this.notify(__("Unable to send mail to: {0}", d.result.join(","))); } + return this.quit(); + }).catch((e) => { + console.log(e); + return this.error(__("Error sending mail: {0}", e.toString()), e); + }); + }; + } + } + + BloggerSendmailDiaglog.scheme = `\ + + + + +
+ +
+ + + + +
+ +
+ +
+
+
+
+
`; + + + 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\ +`; + } + } +} \ No newline at end of file diff --git a/Blogger/main.coffee b/Blogger/main.coffee deleted file mode 100644 index 9925b49..0000000 --- a/Blogger/main.coffee +++ /dev/null @@ -1,506 +0,0 @@ -# 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/. -class Blogger extends this.OS.application.BaseApplication - constructor: (args) -> - super "Blogger", args - - - main: () -> - @user = {} - @cvlist = @find "cv-list" - @cvlist.ontreeselect = (d) => - return unless d - data = d.data.item.data - @CVSectionByCID Number(data.id) - - @inputtags = @.find "input-tags" - @bloglist = @find "blog-list" - @seclist = @find "cv-sec-list" - - el = @find("photo") - $(el) - .click (e) => - @openDialog("FileDialog", { - title: __("Select image file"), - mimes: ["image/.*"] - }) - .then (d) => - el.value = d.file.path - .catch (e) => @error __("Unable to get file"), e - - @userdb = new @_api.DB("user") - @cvcatdb = new @_api.DB("cv_cat") - @cvsecdb = new @_api.DB("cv_sections") - @blogdb = new @_api.DB("blogs") - - - @tabcontainer = @find "tabcontainer" - @tabcontainer.ontabselect = (e) => - @fetchData e.data.container.aid - - (@find "bt-user-save").onbtclick = (e) => - @saveUser() - - (@find "cv-cat-add").onbtclick = (e) => - fn = (tree) => - @openDialog(new BloggerCategoryDialog(), { - title: __("Add category"), - tree: tree - }).then (d) => - c = - name: d.value, - pid: d.p.id, - publish: 1 - @cvcatdb.save c - .then (r) => - @refreshCVCat() - .catch (e) => @error __("Cannot add new category"), e - .catch (e) => @error e.toString(), e - @fetchCVCat() - .then (tree) => fn(tree) - .catch (e) => - data = - text: "Porfolio", - id:"0", - nodes: [] - fn(data) - @error __("Unable to fetch categories"), e - - (@find "cv-cat-edit").onbtclick = (e) => - sel = @cvlist.selectedItem - return unless sel - cat = sel.data - return unless cat - @fetchCVCat().then (tree) => - @openDialog(new BloggerCategoryDialog(), { - title: __("Edit category"), - tree: tree, cat: cat - }).then (d) => - c = - id: cat.id, - publish: cat.publish, - pid: d.p.id, - name: d.value - - @cvcatdb.save c - .then (r) => - @refreshCVCat() - .catch (e) => - @error __("Cannot Edit category"), e - .catch (e) => @error __("Unable to fetch categories"), e - - (@find "cv-cat-del").onbtclick = (e) => - sel = @cvlist.selectedItem - return unless sel - cat = sel.data - return unless cat - @openDialog("YesNoDialog", { - title: __("Delete category") , - iconclass: "fa fa-question-circle", - text: __("Do you really want to delete: {0}?", cat.name) - }).then (d) => - return unless d - @deleteCVCat cat - .catch (e) => @error e.toString(), e - - (@find "cv-sec-add").onbtclick = (e) => - sel = @cvlist.selectedItem - return unless sel - cat = sel.data - return @notify __("Please select a category") unless cat and cat.id isnt "0" - @openDialog(new BloggerCVSectionDiaglog(@), { - title: __("New section entry for {0}", cat.name) - }).then (d) => - d.cid = Number cat.id - d.start = Number d.start - d.end = Number d.end - # d.publish = 1 - @cvsecdb.save d - .then (r) => - @CVSectionByCID Number(cat.id) - .catch (e) => @error __("Cannot save section: {0}", e.toString()), e - - (@find "cv-sec-move").onbtclick = (e) => - sel = (@find "cv-sec-list").selectedItem - return @notify __("Please select a section to move") unless sel - sec = sel.data - - @fetchCVCat().then (tree) => - @openDialog(new BloggerCategoryDialog(),{ - title: __("Move to"), - tree: tree, - selonly: true - }).then (d) => - c = - id: sec.id, - cid: d.p.id - - @cvsecdb.save c - .then (r) => - @CVSectionByCID(sec.cid) - (@find "cv-sec-list").unselect() - .catch (e) => @error __("Cannot move section"), e - - (@find "cv-sec-edit").onbtclick = (e) => - sel = (@find "cv-sec-list").selectedItem - return @notify __("Please select a section to edit") unless sel - sec = sel.data - @openDialog(new BloggerCVSectionDiaglog(@), { - title: __("Modify section entry"), - section: sec - }).then (d) => - d.cid = Number sec.cid - d.start = Number d.start - d.end = Number d.end - #d.publish = Number sec.publish - @cvsecdb.save d - .then (r) => - @CVSectionByCID Number(sec.cid) - .catch (e) => return @error __("Cannot save section: {0}", e.toString()), e - - @seclist.onitemclose = (e) => - return unless e - data = e.data.item.data - @openDialog("YesNoDialog", { - iconclass: "fa fa-question-circle", - text: __("Do you really want to delete: {0}?", data.title) - }).then (b) => - return unless b - @cvsecdb.delete data.id - .then (r) => - @seclist.delete e.data.item - .catch (e) => @error __("Cannot delete the section: {0}", e.toString()), e - return false - - @editor = new EasyMDE - element: @find "markarea" - autoDownloadFontAwesome: false - autofocus: true - tabSize: 4 - indentWithTabs: true - toolbar: [ - { - name: __("New"), - className: "fa fa-file", - action: (e) => - @bloglist.unselect() - @clearEditor() - }, - { - name: __("Save"), - className: "fa fa-save", - action: (e) => - @saveBlog() - } - , "|", "bold", "italic", "heading", "|", "quote", "code", - "unordered-list", "ordered-list", "|", "link", - "image", "table", "horizontal-rule", - { - name: "image", - className: "fa fa-file-image-o", - action: (e) => - @openDialog("FileDialog", { - title: __("Select image file"), - mimes: ["image/.*"] - }).then (d) => - d.file.path.asFileHandle().publish() - .then (r) => - doc = @editor.codemirror.getDoc() - doc.replaceSelection "![](#{@_api.handle.shared}/#{r.result})" - .catch (e) => @error __("Cannot export file for embedding to text"), e - }, - { - name:"Youtube", - className: "fa fa-youtube", - action: (e) => - doc = @editor.codemirror.getDoc() - doc.replaceSelection "[[youtube:]]" - } - "|", - { - name: __("Preview"), - className: "fa fa-eye no-disable", - action: (e) => - @previewOn = !@previewOn - EasyMDE.togglePreview e - #/console.log @select ".editor-preview editor-preview-active" - renderMathInElement @find "editor-container" - }, - "|", - { - name: __("Send mail"), - className: "fa fa-paper-plane", - action: (e) => - sel = @bloglist.selectedItem - return @error __("No post selected") unless sel - data = sel.data - @openDialog(new BloggerSendmailDiaglog(@), { - title: __("Send mail"), - content: @editor.value(), - id: data.id - }) - .then (d) -> - console.log "Email sent" - } - ], - - @bloglist.onlistselect = (e) => - el = @bloglist.selectedItem - return unless el - sel = el.data - return unless sel - @blogdb.get Number(sel.id) - .then (r) => - @editor.value r.content - @inputtags.value = r.tags - (@find "blog-publish").swon = if Number(r.publish) then true else false - .catch (e) => - @error __("Cannot fetch the entry content"), e - - @bloglist.onitemclose = (e) => - return unless e - el = e.data.item - data = el.data - @openDialog("YesNoDialog", { - title: __("Delete a post"), - iconclass: "fa fa-question-circle", - text: __("Do you really want to delete this post ?") - }).then (b) => - return unless b - @blogdb.delete data.id - .then (r) => - @bloglist.delete el - @bloglist.unselect() - @clearEditor() - return false - - - @bindKey "CTRL-S", () => - sel = @tabcontainer.selectedTab - return unless sel and sel.container.aid is "blog-container" - @saveBlog() - @on "resize", () => - @resizeContent() - - @resizeContent() - @loadBlogs() - # @fetchData 0 - # USER TAB - fetchData: (idx) -> - switch idx - when "user-container" #user info - - @userdb.get null - .then (d) => - @user = d[0] - inputs = @select "[input-class='user-input']" - ($ v).val @user[v.name] for v in inputs - .catch (e) => @error __("Cannot fetch user data"), e - when "cv-container" # category - @refreshCVCat() - else - @loadBlogs() - - saveUser:() -> - inputs = @select "[input-class='user-input']" - @user[v.name] = ($ v).val() for v in inputs - return @notify __("Full name must be entered") if not @user.fullname or @user.fullname is "" - #console.log @user - @userdb.save @user - .then (r) => - return @notify __("User data updated") - .catch (e) => return @error __("Cannot save user data"), e - - - # PORFOLIO TAB - refreshCVCat: () -> - @fetchCVCat().then (data) => - @cvlist.data = data - @cvlist.expandAll() - .catch (e) => @error __("Unable to load categories"), e - - fetchCVCat: () -> - new Promise (resolve, reject) => - data = - text: "Porfolio", - id:"0", - nodes: [] - cnd = - order: - name: "ASC" - @cvcatdb.find cnd - .then (d) => - @catListToTree d, data, "0" - resolve data - .catch (e) -> reject __e e - #it = (@cvlist.find "pid", "2")[0] - #@cvlist.set "selectedItem", it - - catListToTree: (table, data, id) -> - result = (v for v in table when v.pid is id) - return data.nodes = null if result.length is 0 - for v in result - v.nodes = [] - v.text = v.name - @catListToTree table, v, v.id - #v.nodes = null if v.nodes.length is 0 - data.nodes.push v - - deleteCVCat: (cat) -> - me = @ - ids = [] - func = (c) -> - ids.push c.id - func(v) for v in c.nodes if c.nodes - func(cat) - - cond = ({ "=": { cid: v } } for v in ids) - # delete all content - @cvsecdb.delete({ "or": cond }).then (r) => - cond = ({ "=": { id: v } } for v in ids) - @cvcatdb.delete({ "or": cond }).then (re) => - @refreshCVCat() - @seclist.data=[] - .catch (e) => - @error __("Cannot delete the category: {0} [{1}]", cat.name, e.toString()), e - .catch (e) => - @error __("Cannot delete all content of: {0} [{1}]", cat.name, e.toString()), e - - CVSectionByCID: (cid) -> - cond = - exp: - "=": - cid: cid - order: - start: "DESC" - @cvsecdb.find(cond).then (d) => - items = [] - (@find "cv-sec-status").text = __("Found {0} sections", d.length) - for v in d - v.closable = true - v.tag = "afx-blogger-cvsection-item" - v.start = Number(v.start) - v.end = Number(v.end) - v.start = undefined if v.start < 1000 - v.end = undefined if v.end < 1000 - items.push v - @seclist.data = items - .catch (e) => @error e.toString(), e - - # blog - saveBlog: () -> - sel = undefined - selel = @bloglist.selectedItem - sel = selel.data if selel - tags = @inputtags.value - content = @editor.value() - title = (new RegExp "^#+(.*)\n", "g").exec content - return @notify __("Please insert a title in the text: beginning with heading") unless title and title.length is 2 - return @notify __("Please enter tags") if tags is "" - d = new Date() - data = - content: content - title: title[1].trim() - tags: tags - ctime: if sel then sel.ctime else d.timestamp() - ctimestr: if sel then sel.ctimestr else d.toString() - utime: d.timestamp() - utimestr: d.toString() - rendered: @process(@editor.options.previewRender(content)) - publish: if (@find "blog-publish").swon then 1 else 0 - data.id = sel.id if sel - #save the data - @blogdb.save data - .then (r) => - @loadBlogs() - .catch (e) => @error __("Cannot save blog: {0}", e.toString()), e - - process: (text) -> - # find video tag and rendered it - embed = (id) -> - return """ - - """ - re = /\[\[youtube:([^\]]*)\]\]/g - replace = [] - while (found = re.exec text) isnt null - replace.push found - return text unless replace.length > 0 - ret = "" - begin = 0 - for it in replace - ret += text.substring begin, it.index - ret += embed(it[1]) - begin = it.index + it[0].length - ret += text.substring begin, text.length - #console.log ret - return ret - - clearEditor:() -> - @.editor.value "" - @.inputtags.value = "" - (@.find "blog-publish").swon = false - # load blog - loadBlogs: () -> - selidx = -1 - el = @bloglist.selectedItem - selidx = $(el).index() - cond = - order: - ctime: "DESC" - fields: [ - "id", - "title", - "ctimestr", - "ctime", - "utime", - "utimestr" - ] - @blogdb.find cond - .then (r) => - v.tag = "afx-blogger-post-item" for v in r - @bloglist.data = r - if selidx isnt -1 - @bloglist.selected = selidx - else - @clearEditor() - @bloglist.selected = -1 - .catch (e) => @error __("No post found: {0}", e.toString()), e - - resizeContent: () -> - container = @find "editor-container" - children = ($ ".EasyMDEContainer", container).children() - titlebar = (($ @scheme).find ".afx-window-top")[0] - toolbar = children[0] - statusbar = children[3] - cheight = ($ @scheme).height() - ($ titlebar).height() - ($ toolbar).height() - ($ statusbar).height() - 90 - ($ children[1]).css("height", cheight + "px") - -Blogger.singleton = true -Blogger.dependencies = [ - "pkg://SimpleMDE/main.js", - "pkg://SimpleMDE/main.css" - "pkg://Katex/main.js", - "pkg://Katex/main.css", -] -this.OS.register "Blogger", Blogger \ No newline at end of file diff --git a/Blogger/main.ts b/Blogger/main.ts new file mode 100644 index 0000000..955e477 --- /dev/null +++ b/Blogger/main.ts @@ -0,0 +1,893 @@ +// 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/. +namespace OS { + export namespace application { + declare function renderMathInElement(el: HTMLElement):void; + declare var EasyMDE; + export class Blogger extends BaseApplication { + + private user: GenericObject; + private cvlist: GUI.tag.TreeViewTag; + private inputtags: HTMLInputElement; + private bloglist: GUI.tag.ListViewTag; + private seclist: GUI.tag.ListViewTag; + private tabcontainer: GUI.tag.TabContainerTag; + private editor: GenericObject; + private previewOn: boolean; + // datatbase objects + private dbhandle: API.VFS.BaseFileHandle; + // database handles + private userdb: API.VFS.BaseFileHandle; + private cvcatdb: API.VFS.BaseFileHandle; + private cvsecdb: API.VFS.BaseFileHandle; + private blogdb: API.VFS.BaseFileHandle; + private subdb: API.VFS.BaseFileHandle; + + private last_ctime: number; + + constructor(args: any) { + super("Blogger", args); + this.previewOn = false; + } + + private async init_db() { + try { + const f = await this.openDialog("FileDialog",{ + title: __("Open/create new database"), + file: "Untitled.db" + }); + var d_1 = f.file.path.asFileHandle(); + if(f.file.type==="file") { + d_1=d_1.parent(); + } + const target=`${d_1.path}/${f.name}`.asFileHandle(); + this.dbhandle=`sqlite://${target.genealogy.join("/")}`.asFileHandle(); + const tables = await this.dbhandle.read(); + /** + * Init following tables if not exist: + * - user + * - cvcat + * - cvsec + * - blogdb + */ + if(!tables.user) + { + this.dbhandle.cache = { + address: "TEXT", + Phone: "TEXT", + shortbiblio: "TEXT", + fullname: "TEXT", + email: "TEXT",url: "TEXT", + photo: "TEXT" + } + const r = await this.dbhandle.write("user"); + if(r.error) + { + throw new Error(r.error as string); + } + } + if(!tables.cv_cat) + { + this.dbhandle.cache = { + publish: "NUMERIC", + name: "TEXT", + pid: "NUMERIC" + } + const r = await this.dbhandle.write("cv_cat"); + if(r.error) + { + throw new Error(r.error as string); + } + } + if(!tables.cv_sections) + { + this.dbhandle.cache = { + title: "TEXT", + start: "NUMERIC", + location: "TEXT", + end: "NUMERIC", + content: "TEXT", + subtitle: "TEXT", + publish: "NUMERIC", + cid: "NUMERIC" + } + const r = await this.dbhandle.write("cv_sections"); + if(r.error) + { + throw new Error(r.error as string); + } + } + if(!tables.blogs) + { + this.dbhandle.cache = { + tags: "TEXT", + content: "TEXT", + utime: "NUMERIC", + rendered: "TEXT", + title: "TEXT", + utimestr: "TEXT", + ctime: "NUMERIC", + ctimestr: "TEXT", + publish: "INTEGER DEFAULT 0", + } + const r = await this.dbhandle.write("blogs"); + if(r.error) + { + throw new Error(r.error as string); + } + } + if(!tables.st_similarity) + { + this.dbhandle.cache = { + pid: "NUMERIC", + sid: "NUMERIC", + score: "NUMERIC" + } + const r = await this.dbhandle.write("st_similarity"); + if(r.error) + { + throw new Error(r.error as string); + } + } + if(!tables.subscribers) + { + this.dbhandle.cache = { + name: "TEXT", + email: "TEXT" + } + const r = await this.dbhandle.write("subscribers"); + if(r.error) + { + throw new Error(r.error as string); + } + } + this.userdb = `${this.dbhandle.path}@user`.asFileHandle(); + this.cvcatdb = `${this.dbhandle.path}@cv_cat`.asFileHandle(); + this.cvsecdb = `${this.dbhandle.path}@cv_sections`.asFileHandle(); + this.blogdb = `${this.dbhandle.path}@blogs`.asFileHandle(); + this.subdb = `${this.dbhandle.path}@subscribers`.asFileHandle(); + + this.last_ctime = 0; + this.bloglist.data = []; + this.loadBlogs(); + } + catch(e) { + this.error(__("Unable to init database file: {0}",e.toString()),e); + this.dbhandle = undefined; + } + } + + menu() { + return [ + { + text: "__(Open/Create database)", + onmenuselect: (e) => { + this.init_db(); + } + } + ]; + } + + main() { + this.user = {}; + this.cvlist = this.find("cv-list") as GUI.tag.TreeViewTag; + this.cvlist.ontreeselect = (d) => { + if (!d) { return; } + const { + data + } = d.data.item; + return this.CVSectionByCID(Number(data.id)); + }; + + this.inputtags = this.find("input-tags") as HTMLInputElement; + this.bloglist = this.find("blog-list") as GUI.tag.ListViewTag; + this.seclist = this.find("cv-sec-list") as GUI.tag.ListViewTag; + + let el = this.find("photo") as HTMLInputElement; + $(el) + .on("click", async (e: any) => { + try { + const ret = await this.openDialog("FileDialog", { + title: __("Select image file"), + mimes: ["image/.*"] + }); + return el.value = ret.file.path; + } catch (e) { + return this.error(__("Unable to get file"), e); + } + }); + + this.tabcontainer = this.find("tabcontainer") as GUI.tag.TabContainerTag; + this.tabcontainer.ontabselect = (e) => { + return this.fetchData((e.data.container as GUI.tag.TileLayoutTag).aid); + }; + + (this.find("bt-user-save") as GUI.tag.ButtonTag).onbtclick = (e: any) => { + return this.saveUser(); + }; + + (this.find("blog-load-more") as GUI.tag.ButtonTag).onbtclick = (e) => { + this.loadBlogs(); + } + + (this.find("cv-cat-add") as GUI.tag.ButtonTag).onbtclick = async (e: any) => { + try { + const tree = await this.fetchCVCat(); + const d = await this.openDialog(new blogger.BloggerCategoryDialog(), { + title: __("Add category"), + tree + }); + this.cvcatdb.cache = { + name: d.value, + pid: d.p.id, + publish: 1 + }; + const r = await this.cvcatdb.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + await this.refreshCVCat(); + } + catch(e) + { + this.error(__("cv-cat-add: {0}", e.toString()), e); + } + }; + + (this.find("cv-cat-edit") as GUI.tag.ButtonTag).onbtclick = async (e: any) => { + try { + const sel = this.cvlist.selectedItem; + if (!sel) { return; } + const cat = sel.data; + if (!cat) { return; } + const tree = await this.fetchCVCat(); + const d = await this.openDialog(new blogger.BloggerCategoryDialog(), { + title: __("Edit category"), + tree, cat + }); + const handle = cat.$vfs; + handle.cache = { + id: cat.id, + publish: cat.publish, + pid: d.p.id, + name: d.value + }; + + const r = await handle.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + await this.refreshCVCat(); + } + catch(e) + { + this.error(__("cv-cat-edit: {0}", e.toString()), e); + } + }; + + (this.find("cv-cat-del") as GUI.tag.ButtonTag).onbtclick = async (e: any) => { + try { + const sel = this.cvlist.selectedItem; + if (!sel) { return; } + const cat = sel.data; + if (!cat) { return; } + const d = await this.openDialog("YesNoDialog", { + title: __("Delete category"), + iconclass: "fa fa-question-circle", + text: __("Do you really want to delete: {0}?", cat.name) + }); + if (!d) { return; } + await this.deleteCVCat(cat); + } + catch(e) + { + this.error(__("cv-cat-del: {0}", e.toString()), e); + } + }; + + (this.find("cv-sec-add") as GUI.tag.ButtonTag).onbtclick = async (e: any) => { + try + { + const sel = this.cvlist.selectedItem; + if (!sel) { return; } + const cat = sel.data; + if (!cat || (cat.id === "0")) { return this.toast(__("Please select a category")); } + const d = await this.openDialog(new blogger.BloggerCVSectionDiaglog(), { + title: __("New section entry for {0}", cat.name) + }); + d.cid = Number(cat.id); + d.start = Number(d.start); + d.end = Number(d.end); + this.cvsecdb.cache = d; + // d.publish = 1 + const r = await this.cvsecdb.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + await this.CVSectionByCID(Number(cat.id)); + } + catch(e) + { + this.error(__("cv-sec-add: {0}", e.toString()), e); + } + }; + + (this.find("cv-sec-move") as GUI.tag.ButtonTag).onbtclick = async (e: any) => { + try { + const sel = this.seclist.selectedItem; + if (!sel) { return this.toast(__("Please select a section to move")); } + const sec = sel.data; + const handle = sec.$vfs; + console.log(handle); + const tree = await this.fetchCVCat(); + const d = await this.openDialog(new blogger.BloggerCategoryDialog(), { + title: __("Move to"), + tree, + selonly: true + }); + handle.cache = { + id: sec.id, + cid: d.p.id + }; + const r = await handle.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + await this.CVSectionByCID(sec.cid); + this.seclist.unselect(); + } + catch(e) + { + this.error(__("cv-sec-move: {0}", e.toString()), e); + } + }; + + (this.find("cv-sec-edit") as GUI.tag.ButtonTag).onbtclick = async (e: any) => { + try { + const sel = this.seclist.selectedItem; + if (!sel) { return this.toast(__("Please select a section to edit")); } + const sec = sel.data; + const d = await this.openDialog(new blogger.BloggerCVSectionDiaglog(), { + title: __("Modify section entry"), + section: sec + }); + d.cid = Number(sec.cid); + d.start = Number(d.start); + d.end = Number(d.end); + + const handle = sec.$vfs; + handle.cache = d; + //d.publish = Number sec.publish + const r = await handle.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + await this.CVSectionByCID(Number(sec.cid)); + } + catch(e) + { + this.error(__("cv-sec-edit: {0}", e.toString()), e); + } + }; + + this.seclist.onitemclose = (evt) => { + if (!evt) { return; } + const data = evt.data.item.data; + this.openDialog("YesNoDialog", { + iconclass: "fa fa-question-circle", + text: __("Do you really want to delete: {0}?", data.title) + }).then(async (b: any) => { + if (!b) { return; } + try { + const r = await this.cvsecdb.remove({ + where: { + id: data.id + } + }); + if(r.error) + { + throw new Error(r.error as string); + } + return this.seclist.delete(evt.data.item); + } catch(e) { + return this.error(__("Cannot delete the section: {0}",e.toString()),e); + } + }); + return false; + }; + + this.editor = new EasyMDE({ + element: this.find("markarea"), + autoDownloadFontAwesome: false, + autofocus: true, + tabSize: 4, + indentWithTabs: true, + toolbar: [ + { + name: __("New"), + className: "fa fa-file", + action: (e: any) => { + this.bloglist.unselect(); + return this.clearEditor(); + } + }, + { + name: __("Save"), + className: "fa fa-save", + action: (e: any) => { + return this.saveBlog(); + } + } + , "|", "bold", "italic", "heading", "|", "quote", "code", + "unordered-list", "ordered-list", "|", "link", + "image", "table", "horizontal-rule", + { + name: "image", + className: "fa fa-file-image-o", + action: (_) => { + return this.openDialog("FileDialog", { + title: __("Select image file"), + mimes: ["image/.*"] + }).then((d) => { + return d.file.path.asFileHandle().publish() + .then((r: { result: any; }) => { + const doc = this.editor.codemirror.getDoc(); + return doc.replaceSelection(`![](${this._api.handle.shared}/${r.result})`); + }).catch((e: any) => this.error(__("Cannot export file for embedding to text"), e)); + }); + } + }, + { + name: "Youtube", + className: "fa fa-youtube", + action: (e: any) => { + const doc = this.editor.codemirror.getDoc(); + return doc.replaceSelection("[[youtube:]]"); + } + }, + "|", + { + name: __("Preview"), + className: "fa fa-eye no-disable", + action: (e: any) => { + this.previewOn = !this.previewOn; + EasyMDE.togglePreview(e); + ///console.log @select ".editor-preview editor-preview-active" + renderMathInElement(this.find("editor-container")); + } + }, + "|", + { + name: __("Send mail"), + className: "fa fa-paper-plane", + action: async (e: any) => { + try { + const d = await this.subdb.read(); + const sel = this.bloglist.selectedItem; + if (!sel) { return this.error(__("No post selected")); } + const data = sel.data; + await this.openDialog(new blogger.BloggerSendmailDiaglog(), { + title: __("Send mail"), + content: this.editor.value(), + mails: d, + id: data.id + }); + this.toast(__("Emails sent")); + } + catch(e) + { + this.error(__("Error sending mails: {0}", e.toString()), e); + } + } + } + ] + }); + + this.bloglist.onlistselect = async (e: any) => { + const el = this.bloglist.selectedItem; + if (!el) { return; } + const sel = el.data; + if (!sel) { return; } + try { + const result=await this.blogdb.read({ + where: { + id: Number(sel.id) + } + }); + if(!result || result.length == 0) + { + throw new Error(__("No record found for ID {}", sel.id).__()); + } + const r = result[0]; + this.editor.value(r.content); + this.inputtags.value=r.tags; + return (this.find("blog-publish") as GUI.tag.SwitchTag).swon=Number(r.publish)? true:false; + } + catch(e_1) { + return this.error(__("Cannot fetch the entry content"),e_1); + } + }; + + this.bloglist.onitemclose = (e) => { + if (!e) { return; } + const el = e.data.item; + const data = el.data; + this.openDialog("YesNoDialog", { + title: __("Delete a post"), + iconclass: "fa fa-question-circle", + text: __("Do you really want to delete this post ?") + }).then(async (b: any) => { + if (!b) { return; } + const r = await this.blogdb.remove({ + where: { + id: Number(data.id) + } + }); + if(r.error) + { + throw new Error(r.error as string); + } + this.bloglist.delete(el); + this.bloglist.unselect(); + return this.clearEditor(); + }); + return false; + }; + + + this.bindKey("CTRL-S", () => { + const sel = this.tabcontainer.selectedTab; + if (!sel || ((sel.container as GUI.tag.TileLayoutTag).aid !== "blog-container")) { return; } + return this.saveBlog(); + }); + this.on("resize", () => { + return this.resizeContent(); + }); + + this.resizeContent(); + return this.init_db(); + } + // @fetchData 0 + // USER TAB + private fetchData(idx: any) { + switch (idx) { + case "user-container": //user info + return this.userdb.read() + .then((d) => { + if(!d || d.length == 0) + { + return; + } + this.user = d[0]; + const inputs = this.select("[input-class='user-input']"); + return inputs.map((i,v) => ($(v)).val(this.user[(v as HTMLInputElement).name])); + }).catch((e: any) => this.error(__("Cannot fetch user data"), e)); + case "cv-container": // category + return this.refreshCVCat(); + default: + this.last_ctime = 0; + this.bloglist.data = []; + return this.loadBlogs(); + } + } + + private async saveUser() { + try { + const inputs = this.select("[input-class='user-input']"); + for (let v of inputs) { this.user[(v as HTMLInputElement).name] = ($(v)).val(); } + if (!this.user.fullname || (this.user.fullname === "")) { return this.toast(__("Full name must be entered")); } + //console.log @user + let fp = this.userdb; + if(this.user && this.user.id) + { + fp = `${this.userdb.path}@${this.user.id}`.asFileHandle(); + } + fp.cache = this.user + const r = await fp.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + if(!this.user.id) + { + this.user.id = r.result; + } + this.toast(__("User data updated")); + } + catch(e) + { + this.error(__("Cannot save user data: {0}", e.toString()), e); + } + } + + + // PORFOLIO TAB + private refreshCVCat() { + return this.fetchCVCat().then((data: any) => { + this.cvlist.data = data; + return this.cvlist.expandAll(); + }).catch((e: any) => this.error(__("Unable to load categories"), e)); + } + + private fetchCVCat() { + return new Promise(async (resolve, reject) => { + try { + const data = { + text: "Porfolio", + id: 0, + nodes: [] + }; + const filter = { + order: ["name$asc"] + }; + const d = await this.cvcatdb.read(filter); + this.catListToTree(d, data, 0); + resolve(data); + } + catch(e) + { + reject(__e(e)); + } + }); + } + //it = (@cvlist.find "pid", "2")[0] + //@cvlist.set "selectedItem", it + + private catListToTree(table: GenericObject[], data: GenericObject, id: number) { + const result = table.filter((e) => { + return e.pid == id + }); + if (result.length === 0) { + return data.nodes = null; + } + for(let v of result) + { + v.nodes = []; + v.text = v.name; + this.catListToTree(table, v, v.id); + data.nodes.push(v); + } + } + + private deleteCVCat(cat: GenericObject): Promise { + return new Promise(async (resolve, reject) => { + try { + let v: any; + const ids = []; + var func = function (c: GenericObject) { + ids.push(c.id); + if (c.nodes) { + c.nodes.map((v) => func(v)); + } + }; + func(cat); + // delete all content + let r = await this.cvsecdb.remove({ + where: { + $or: { + cid: ids + } + } + }); + if(r.error) + { + throw new Error(r.error as string); + } + r = await this.cvcatdb.remove({ + where: { + $or: { + id: ids + } + } + }); + if(r.error) + { + throw new Error(r.error as string); + } + await this.refreshCVCat(); + this.seclist.data = []; + } + catch(e) + { + reject(__e(e)); + } + }); + } + + private CVSectionByCID(cid: number): Promise { + return new Promise( async (resolve, reject)=> { + try { + const d = await this.cvsecdb.read({ + where: {cid}, + order: [ "start$desc" ] + }); + const items = []; + (this.find("cv-sec-status") as GUI.tag.LabelTag).text = __("Found {0} sections", d.length); + for (let v of d) { + v.closable = true; + v.tag = "afx-blogger-cvsection-item"; + v.start = Number(v.start); + v.end = Number(v.end); + if (v.start < 1000) { v.start = undefined; } + if (v.end < 1000) { v.end = undefined; } + items.push(v); + } + this.seclist.data = items; + } + catch(e) + { + reject(__e(e)); + } + }); + } + + // blog + private async saveBlog() { + try { + let sel = undefined; + const selel = this.bloglist.selectedItem; + if (selel) { sel = selel.data; } + const tags = this.inputtags.value; + const content = this.editor.value(); + const title = (new RegExp("^#+(.*)\n", "g")).exec(content); + if (!title || (title.length !== 2)) { return this.toast(__("Please insert a title in the text: beginning with heading")); } + if (tags === "") { return this.toast(__("Please enter tags")); } + const d = new Date(); + const data: GenericObject = { + content, + title: title[1].trim(), + tags, + ctime: sel ? sel.ctime : d.timestamp(), + ctimestr: sel ? sel.ctimestr : d.toString(), + utime: d.timestamp(), + utimestr: d.toString(), + rendered: this.process(this.editor.options.previewRender(content)), + publish: (this.find("blog-publish") as GUI.tag.SwitchTag).swon ? 1 : 0 + }; + let handle = this.blogdb; + if (sel) { + data.id = sel.id; + handle = sel.$vfs; + } + //save the data + handle.cache = data; + const r = await handle.write(undefined); + if(r.error) + { + throw new Error(r.error as string); + } + if(!sel) + { + this.last_ctime = 0; + this.bloglist.data = []; + await this.loadBlogs(); + } + else + { + //data.text = data.title; + selel.data = data; + } + } + catch(e) + { + this.error(__("Cannot save blog: {0}", e.toString()), e); + } + } + + private process(text: string) { + // find video tag and rendered it + let found: any; + const embed = (id: any) => `\ +\ +`; + const re = /\[\[youtube:([^\]]*)\]\]/g; + const replace = []; + while ((found = re.exec(text)) !== null) { + replace.push(found); + } + if (!(replace.length > 0)) { return text; } + let ret = ""; + let begin = 0; + for (let it of replace) { + ret += text.substring(begin, it.index); + ret += embed(it[1]); + begin = it.index + it[0].length; + } + ret += text.substring(begin, text.length); + //console.log ret + return ret; + } + + private clearEditor() { + this.editor.value(""); + this.inputtags.value = ""; + return (this.find("blog-publish") as GUI.tag.SwitchTag).swon = false; + } + // load blog + private loadBlogs(): Promise { + return new Promise( async (ok, reject)=> { + try { + const filter: GenericObject = { + order: ["ctime$desc"], + fields: [ + "id", + "title", + "ctimestr", + "ctime", + "utime", + "utimestr" + ], + limit: 10, + }; + if(this.last_ctime) + { + filter.where = { ctime$lt: this.last_ctime}; + } + const r = await this.blogdb.read(filter); + if(r.length == 0) + { + this.toast(__("No more record to load")); + return; + } + this.last_ctime = r[r.length - 1].ctime; + for (let v of r) { + v.tag = "afx-blogger-post-item"; + this.bloglist.push(v); + } + this.clearEditor(); + return this.bloglist.selected = -1; + } + catch(e) + { + reject(__e(e)); + } + }); + + } + + private resizeContent() { + const container = this.find("editor-container"); + const children = ($(".EasyMDEContainer", container)).children(); + const titlebar = (($(this.scheme)).find(".afx-window-top"))[0]; + const toolbar = children[0]; + const statusbar = children[3]; + const cheight = ($(this.scheme)).height() - ($(titlebar)).height() - ($(toolbar)).height() - ($(statusbar)).height() - 90; + return ($(children[1])).css("height", cheight + "px"); + } + } + + Blogger.singleton = true; + Blogger.dependencies = [ + "pkg://SimpleMDE/main.js", + "pkg://SimpleMDE/main.css", + "pkg://Katex/main.js", + "pkg://Katex/main.css", + "pkg://SQLiteDB/libsqlite.js", + ]; + } +} diff --git a/Blogger/package.json b/Blogger/package.json index 0fcd2be..67e3f7b 100644 --- a/Blogger/package.json +++ b/Blogger/package.json @@ -6,7 +6,7 @@ "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version": "0.2.9-a", + "version": "0.2.10-a", "category": "Internet", "iconclass": "fa fa-book", "dependencies": [ @@ -36,7 +36,6 @@ "No email selected": "No email selected", "Unable to send mail to: {0}": "Unable to send mail to: {0}", "Error sending mail: {0}": "Error sending mail: {0}", - "Cannot fetch subscribers data: {0}": "Cannot fetch subscribers data: {0}", "Open/create new database": "Open/create new database", "Unable to init database file: {0}": "Unable to init database file: {0}", "Select image file": "Select image file", @@ -65,28 +64,20 @@ "No post selected": "No post selected", "Emails sent": "Emails sent", "Error sending mails: {0}": "Error sending mails: {0}", + "No record found for ID {}": "No record found for ID {}", "Cannot fetch the entry content": "Cannot fetch the entry content", "Delete a post": "Delete a post", "Do you really want to delete this post ?": "Do you really want to delete this post ?", "Cannot fetch user data": "Cannot fetch user data", "Full name must be entered": "Full name must be entered", "User data updated": "User data updated", - "Cannot save user data": "Cannot save user data", + "Cannot save user data: {0}": "Cannot save user data: {0}", "Unable to load categories": "Unable to load categories", "Found {0} sections": "Found {0} sections", "Please insert a title in the text: beginning with heading": "Please insert a title in the text: beginning with heading", "Please enter tags": "Please enter tags", "Cannot save blog: {0}": "Cannot save blog: {0}", - "Cannot add new category": "Cannot add new category", - "Unable to fetch categories": "Unable to fetch categories", - "Cannot Edit category": "Cannot Edit category", - "Cannot save section: {0}": "Cannot save section: {0}", - "Cannot move section": "Cannot move section", - "Cannot delete the category: {0} [{1}]": "Cannot delete the category: {0} [{1}]", - "Cannot delete all content of: {0} [{1}]": "Cannot delete all content of: {0} [{1}]", - "No post found: {0}": "No post found: {0}", - "Created: {0}": "Created: {0}", - "Updated: {0}": "Updated: {0}", + "No more record to load": "No more record to load", "Full name": "Full name", "Address": "Address", "Phone": "Phone", @@ -95,7 +86,10 @@ "Photo": "Photo", "Short biblio": "Short biblio", "Categories": "Categories", - "Tags": "Tags" + "Load more": "Load more", + "Tags": "Tags", + "Created: {0}": "Created: {0}", + "Updated: {0}": "Updated: {0}" } } } \ No newline at end of file diff --git a/Blogger/scheme.html b/Blogger/scheme.html index cf8ed01..e10b98d 100644 --- a/Blogger/scheme.html +++ b/Blogger/scheme.html @@ -1,5 +1,5 @@ - + @@ -65,7 +65,10 @@ - + + + +
diff --git a/Blogger/sendmail.html b/Blogger/sendmail.html deleted file mode 100644 index 799709a..0000000 --- a/Blogger/sendmail.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - -
- -
- - - - -
- -
- -
-
-
-
-
\ No newline at end of file diff --git a/Blogger/tags.coffee b/Blogger/tags.coffee deleted file mode 100644 index b751498..0000000 --- a/Blogger/tags.coffee +++ /dev/null @@ -1,63 +0,0 @@ -Ant = this - -class CVSectionListItemTag extends this.OS.GUI.tag.ListViewItemTag - constructor: () -> - super() - - ondatachange: () -> - return unless @data - v = @data - nativel = ["content", "start", "end" ] - @closable = v.closable - for k, el of @refs - if v[k] and v[k] isnt "" - if nativel.includes k - $(el).text v[k] - else - el.text = v[k] - - reload: () -> - - init:() -> - - - itemlayout: () -> - { el: "div", children: [ - { el: "afx-label", ref: "title", class: "afx-cv-sec-title" }, - { el: "afx-label", ref: "subtitle", class: "afx-cv-sec-subtitle" }, - { el: "p", ref: "content", class: "afx-cv-sec-content" }, - { el: "p", class: "afx-cv-sec-period", children: [ - { el: "i", ref: "start" }, - { el: "i", ref: "end", class: "period-end" } - ] }, - { el: "afx-label", ref: "location", class: "afx-cv-sec-loc" } - ] } - -this.OS.GUI.tag.define "afx-blogger-cvsection-item", CVSectionListItemTag - - -class BlogPostListItemTag extends this.OS.GUI.tag.ListViewItemTag - constructor: () -> - super() - - ondatachange: (v) -> - return unless @data - v = @data - v.closable = true - @closable = v.closable - @refs.title.text = v.title - @refs.ctimestr.text = __("Created: {0}", v.ctimestr) - @refs.utimestr.text = __("Updated: {0}", v.utimestr) - - reload: () -> - - init:() -> - - itemlayout: () -> - { el: "div", children: [ - { el: "afx-label", ref: "title", class: "afx-blogpost-title" }, - { el: "afx-label", ref: "ctimestr", class: "blog-dates" }, - { el: "afx-label", ref: "utimestr", class: "blog-dates" }, - ] } - -this.OS.GUI.tag.define "afx-blogger-post-item", BlogPostListItemTag \ No newline at end of file diff --git a/Blogger/tags.ts b/Blogger/tags.ts new file mode 100644 index 0000000..f4a7a06 --- /dev/null +++ b/Blogger/tags.ts @@ -0,0 +1,104 @@ + +interface Array { + /** + * Check if the array includes an element + * + * @param {T} element to check + * @returns {boolean} + * @memberof Array + */ + includes(el: T): boolean; +} + +namespace OS { + export namespace application { + export namespace blogger { + + class CVSectionListItemTag extends OS.GUI.tag.ListViewItemTag { + constructor() { + super(); + } + + ondatachange() { + if (!this.data) { return; } + const v = this.data; + const nativel = ["content", "start", "end"]; + this.closable = v.closable; + return (() => { + const result = []; + for (let k in this.refs) { + const el = this.refs[k]; + if (v[k] && (v[k] !== "")) { + if (nativel.includes(k)) { + result.push($(el).text(v[k])); + } else { + result.push((el as OS.GUI.tag.LabelTag).text = v[k]); + } + } else { + result.push(undefined); + } + } + return result; + })(); + } + + reload() { } + + init() { } + + + itemlayout() { + return { + el: "div", children: [ + { el: "afx-label", ref: "title", class: "afx-cv-sec-title" }, + { el: "afx-label", ref: "subtitle", class: "afx-cv-sec-subtitle" }, + { el: "p", ref: "content", class: "afx-cv-sec-content" }, + { + el: "p", class: "afx-cv-sec-period", children: [ + { el: "i", ref: "start" }, + { el: "i", ref: "end", class: "period-end" } + ] + }, + { el: "afx-label", ref: "location", class: "afx-cv-sec-loc" } + ] + }; + } + } + + OS.GUI.tag.define("afx-blogger-cvsection-item", CVSectionListItemTag); + + + class BlogPostListItemTag extends OS.GUI.tag.ListViewItemTag { + constructor() { + super(); + } + + ondatachange() { + if (!this.data) { return; } + const v = this.data; + v.closable = true; + this.closable = v.closable; + (this.refs.title as OS.GUI.tag.LabelTag).text = v.title; + (this.refs.ctimestr as OS.GUI.tag.LabelTag).text = __("Created: {0}", v.ctimestr); + (this.refs.utimestr as OS.GUI.tag.LabelTag).text = __("Updated: {0}", v.utimestr); + } + + reload() { } + + init() { } + + itemlayout() { + return { + el: "div", children: [ + { el: "afx-label", ref: "title", class: "afx-blogpost-title" }, + { el: "afx-label", ref: "ctimestr", class: "blog-dates" }, + { el: "afx-label", ref: "utimestr", class: "blog-dates" }, + ] + }; + } + } + + OS.GUI.tag.define("afx-blogger-post-item", BlogPostListItemTag); + } + } +} \ No newline at end of file diff --git a/SQLiteDB/LibSQLite.ts b/SQLiteDB/LibSQLite.ts index c2d4ed5..aea192c 100644 --- a/SQLiteDB/LibSQLite.ts +++ b/SQLiteDB/LibSQLite.ts @@ -406,15 +406,27 @@ namespace OS { filter.table_name = this._table_name; if(this._id) { - filter.where = { id: this._id}; + filter.where = {}; + filter.where[this.info.schema.pk] = this._id; } - let data = await this._handle.select(filter); + let data: GenericObject[] = await this._handle.select(filter); if(this._id) { this.cache = data[0]; } else { + for(let row of data) + { + if(row[this.info.schema.pk]) + { + Object.defineProperty(row, '$vfs', { + value: `${this.path}@${row[this.info.schema.pk]}`.asFileHandle(), + enumerable: false, + configurable: false + }) + } + } this.cache = data; } resolve(this.cache) @@ -444,7 +456,7 @@ namespace OS { await this.onready(); if(this._id && this._table_name) { - this.cache.id = this._id; + this.cache[this.info.schema.pk] = this._id; const ret = await this._handle.update(this._table_name, this.cache, this.info.schema.pk); resolve({result:ret, error: false}); return @@ -504,7 +516,8 @@ namespace OS { filter.table_name = this._table_name; if(this._id) { - filter.where = { id: this._id}; + filter.where = {}; + filter.where[this.info.schema.pk] = this._id; } let ret = await this._handle.delete_records(filter); resolve({result: ret, error: false}); diff --git a/SQLiteDB/build/debug/libsqlite.js b/SQLiteDB/build/debug/libsqlite.js index 7cc3853..c3fc375 100644 --- a/SQLiteDB/build/debug/libsqlite.js +++ b/SQLiteDB/build/debug/libsqlite.js @@ -1 +1 @@ -var OS;!function(e){let t;!function(e){class t{constructor(e){t.REGISTY||(t.REGISTY={}),this.db_file=e.asFileHandle(),t.REGISTY[this.db_file.path]?this.db_file=t.REGISTY[this.db_file.path]:t.REGISTY[this.db_file.path]=this.db_file}pwd(){return"pkg://SQLiteDB/".asFileHandle()}fileinfo(){return this.db_file.info}init(){return new Promise(async(e,t)=>{try{if(this.db_file.ready)return e(!0);let t={action:"init",args:{db_source:this.db_file.path}},a=await this.call(t);if(a=await this.db_file.onready(),!this.db_file||!this.db_file.ready||"file"!==this.db_file.info.type)throw __("DB file meta-data is invalid: {0}",this.db_file.path).__();e(!0)}catch(e){t(__e(e))}})}call(t){return new Promise(async(a,i)=>{t.args.db_source=this.db_file.path;let s={path:this.pwd().path+"/api/api.lua",parameters:t},r=await e.apigateway(s,!1);r.error?i(e.throwe(__("SQLiteDB server call error: {0}",r.error))):a(r.result)})}request(e){return new Promise(async(t,a)=>{try{this.db_file.ready||await this.init(),t(await this.call(e))}catch(e){a(__e(e))}})}select(e){let t={action:"select",args:{filter:e}};return this.request(t)}delete_records(e){let t={action:"delete_records",args:{filter:e}};return this.request(t)}drop_table(e){let t={action:"drop_table",args:{table_name:e}};return this.request(t)}list_tables(){return this.request({action:"list_table",args:{}})}create_table(e,t){let a={action:"create_table",args:{table_name:e,scheme:t}};return this.request(a)}get_table_scheme(e){let t={action:"table_scheme",args:{table_name:e}};return this.request(t)}insert(e,t,a){let i={action:"insert",args:{table_name:e,record:t,pk:a}};return this.request(i)}update(e,t,a){let i={action:"update",args:{table_name:e,record:t,pk:a}};return this.request(i)}last_insert_id(){return this.request({action:"last_insert_id",args:{}})}}let a;!function(e){class a extends e.BaseFileHandle{setPath(e){let a=e.split("@");if(super.setPath(a[0]),a.length>3)throw new Error(__("Invalid file path").__());this.path=e,this._table_name=a[1],this._id=a[2]?parseInt(a[2]):void 0,this._handle=new t("home://"+this.genealogy.join("/"))}meta(){return new Promise(async(e,t)=>{try{await this._handle.init();let t={result:{file:this._handle.fileinfo(),schema:void 0},error:!1};if(this._table_name){const e=await this._handle.get_table_scheme(this._table_name);if(0==e.length)t.result.schema=void 0;else{t.result.schema={fields:[],types:{},pk:void 0},t.result.schema.fields=e.map(e=>e.name);for(let a of e)t.result.schema.types[a.name]=a.type,a.pk&&(t.result.schema.pk=a.name)}}return e(t)}catch(e){return t(__e(e))}})}_rd(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={id:this._id});let i=await this._handle.select(a);this._id?this.cache=i[0]:this.cache=i,t(this.cache)}else{let e=await this._handle.list_tables();const a={};for(let t of e)a[t.name]=`${this.path}@${t.name}`.asFileHandle();this.cache=a,t(a)}}catch(e){return a(__e(e))}})}_wr(e){return new Promise(async(t,a)=>{try{if(!this.cache)throw new Error(__("No data to submit to remote database, please check the `cache` field").__());if(await this.onready(),this._id&&this._table_name)return this.cache.id=this._id,void t({result:await this._handle.update(this._table_name,this.cache,this.info.schema.pk),error:!1});if(this._table_name)return void t({result:await this._handle.insert(this._table_name,this.cache,this.info.schema.pk),error:!1});t({result:await this._handle.create_table(e,this.cache),error:!1})}catch(e){return a(__e(e))}})}_rm(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={id:this._id}),t({result:await this._handle.delete_records(a),error:!1})}else{let a=e;if(!a)throw new Error(__("No table specified for dropping").__());t({result:await this._handle.drop_table(a),error:!1})}}catch(e){return a(__e(e))}})}}e.register("^sqlite$",a)}(a=e.VFS||(e.VFS={}))}(t=e.API||(e.API={}))}(OS||(OS={})); \ No newline at end of file +var OS;!function(e){let t;!function(e){class t{constructor(e){t.REGISTY||(t.REGISTY={}),this.db_file=e.asFileHandle(),t.REGISTY[this.db_file.path]?this.db_file=t.REGISTY[this.db_file.path]:t.REGISTY[this.db_file.path]=this.db_file}pwd(){return"pkg://SQLiteDB/".asFileHandle()}fileinfo(){return this.db_file.info}init(){return new Promise(async(e,t)=>{try{if(this.db_file.ready)return e(!0);let t={action:"init",args:{db_source:this.db_file.path}},a=await this.call(t);if(a=await this.db_file.onready(),!this.db_file||!this.db_file.ready||"file"!==this.db_file.info.type)throw __("DB file meta-data is invalid: {0}",this.db_file.path).__();e(!0)}catch(e){t(__e(e))}})}call(t){return new Promise(async(a,i)=>{t.args.db_source=this.db_file.path;let s={path:this.pwd().path+"/api/api.lua",parameters:t},r=await e.apigateway(s,!1);r.error?i(e.throwe(__("SQLiteDB server call error: {0}",r.error))):a(r.result)})}request(e){return new Promise(async(t,a)=>{try{this.db_file.ready||await this.init(),t(await this.call(e))}catch(e){a(__e(e))}})}select(e){let t={action:"select",args:{filter:e}};return this.request(t)}delete_records(e){let t={action:"delete_records",args:{filter:e}};return this.request(t)}drop_table(e){let t={action:"drop_table",args:{table_name:e}};return this.request(t)}list_tables(){return this.request({action:"list_table",args:{}})}create_table(e,t){let a={action:"create_table",args:{table_name:e,scheme:t}};return this.request(a)}get_table_scheme(e){let t={action:"table_scheme",args:{table_name:e}};return this.request(t)}insert(e,t,a){let i={action:"insert",args:{table_name:e,record:t,pk:a}};return this.request(i)}update(e,t,a){let i={action:"update",args:{table_name:e,record:t,pk:a}};return this.request(i)}last_insert_id(){return this.request({action:"last_insert_id",args:{}})}}let a;!function(e){class a extends e.BaseFileHandle{setPath(e){let a=e.split("@");if(super.setPath(a[0]),a.length>3)throw new Error(__("Invalid file path").__());this.path=e,this._table_name=a[1],this._id=a[2]?parseInt(a[2]):void 0,this._handle=new t("home://"+this.genealogy.join("/"))}meta(){return new Promise(async(e,t)=>{try{await this._handle.init();let t={result:{file:this._handle.fileinfo(),schema:void 0},error:!1};if(this._table_name){const e=await this._handle.get_table_scheme(this._table_name);if(0==e.length)t.result.schema=void 0;else{t.result.schema={fields:[],types:{},pk:void 0},t.result.schema.fields=e.map(e=>e.name);for(let a of e)t.result.schema.types[a.name]=a.type,a.pk&&(t.result.schema.pk=a.name)}}return e(t)}catch(e){return t(__e(e))}})}_rd(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={},a.where[this.info.schema.pk]=this._id);let i=await this._handle.select(a);if(this._id)this.cache=i[0];else{for(let e of i)e[this.info.schema.pk]&&Object.defineProperty(e,"$vfs",{value:`${this.path}@${e[this.info.schema.pk]}`.asFileHandle(),enumerable:!1,configurable:!1});this.cache=i}t(this.cache)}else{let e=await this._handle.list_tables();const a={};for(let t of e)a[t.name]=`${this.path}@${t.name}`.asFileHandle();this.cache=a,t(a)}}catch(e){return a(__e(e))}})}_wr(e){return new Promise(async(t,a)=>{try{if(!this.cache)throw new Error(__("No data to submit to remote database, please check the `cache` field").__());if(await this.onready(),this._id&&this._table_name)return this.cache[this.info.schema.pk]=this._id,void t({result:await this._handle.update(this._table_name,this.cache,this.info.schema.pk),error:!1});if(this._table_name)return void t({result:await this._handle.insert(this._table_name,this.cache,this.info.schema.pk),error:!1});t({result:await this._handle.create_table(e,this.cache),error:!1})}catch(e){return a(__e(e))}})}_rm(e){return new Promise(async(t,a)=>{try{if(this._table_name&&!this.info.schema)throw new Error(__("Table `{0}` does not exists in database: {1}",this._table_name,this.path).__());if(this._table_name){let a=e;a&&!this._id||(a={}),a.table_name=this._table_name,this._id&&(a.where={},a.where[this.info.schema.pk]=this._id),t({result:await this._handle.delete_records(a),error:!1})}else{let a=e;if(!a)throw new Error(__("No table specified for dropping").__());t({result:await this._handle.drop_table(a),error:!1})}}catch(e){return a(__e(e))}})}}e.register("^sqlite$",a)}(a=e.VFS||(e.VFS={}))}(t=e.API||(e.API={}))}(OS||(OS={})); \ No newline at end of file diff --git a/SQLiteDB/build/debug/main.js b/SQLiteDB/build/debug/main.js index f9a242b..0718b43 100644 --- a/SQLiteDB/build/debug/main.js +++ b/SQLiteDB/build/debug/main.js @@ -1 +1 @@ -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 +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.find("bt-refresh").onbtclick=async t=>{try{this.last_max_id=0;const t=this.tbl_list.selectedItem.data.handle,e=await t.read({fields:["COUNT(*)"]});this.n_records=e[0]["COUNT(*)"],this.grid_table.rows=[],await this.load_table()}catch(t){this.error(__("Error reload 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&&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/debug/scheme.html b/SQLiteDB/build/debug/scheme.html index 5e8c268..1fd5902 100644 --- a/SQLiteDB/build/debug/scheme.html +++ b/SQLiteDB/build/debug/scheme.html @@ -16,6 +16,7 @@
+
diff --git a/SQLiteDB/build/release/SQLiteDB.zip b/SQLiteDB/build/release/SQLiteDB.zip index 5a3e81e..30bc339 100644 Binary files a/SQLiteDB/build/release/SQLiteDB.zip and b/SQLiteDB/build/release/SQLiteDB.zip differ diff --git a/SQLiteDB/main.ts b/SQLiteDB/main.ts index 74b53d0..b61619f 100644 --- a/SQLiteDB/main.ts +++ b/SQLiteDB/main.ts @@ -235,6 +235,23 @@ namespace OS { this.error(__("Error reading table: {0}", e.toString()),e); } } + + (this.find("bt-refresh") as GUI.tag.ButtonTag).onbtclick = async (e) => { + try + { + this.last_max_id = 0; + const handle: API.VFS.BaseFileHandle = this.tbl_list.selectedItem.data.handle; + const records = await handle.read({fields:["COUNT(*)"]}); + this.n_records = records[0]["COUNT(*)"]; + this.grid_table.rows = []; + await this.load_table(); + } + catch(e) + { + this.error(__("Error reload table: {0}", e.toString()),e); + } + } + this.tbl_list.onlistselect = async (_) => { try { @@ -406,7 +423,7 @@ namespace OS { for(let v in headers) { let text:string = e[headers[v].text]; - if(text.length > 100) + if(text && text.length > 100) { text = text.substring(0,100); } diff --git a/SQLiteDB/scheme.html b/SQLiteDB/scheme.html index 5e8c268..1fd5902 100644 --- a/SQLiteDB/scheme.html +++ b/SQLiteDB/scheme.html @@ -16,6 +16,7 @@
+
diff --git a/packages.json b/packages.json index 0b1fc65..f3fa40d 100644 --- a/packages.json +++ b/packages.json @@ -95,8 +95,8 @@ "description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/Blogger/README.md", "category": "Internet", "author": "Xuan Sang LE", - "version": "0.2.9-a", - "dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r"],"mimes":["none"], + "version": "0.2.10-a", + "dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r","SQLiteDB@0.1.0-a"],"mimes":["none"], "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/Blogger/build/release/Blogger.zip" }, { @@ -409,6 +409,16 @@ "dependencies": [], "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/SimpleMDE/build/release/SimpleMDE.zip" }, + { + "pkgname": "SQLiteDB", + "name": "SQLite3 Browser", + "description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/SQLiteDB/README.md", + "category": "Library", + "author": "Dany LE", + "version": "0.1.0-a", + "dependencies": [], + "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/SQLiteDB/build/release/SQLiteDB.zip" + }, { "pkgname": "SystemControl", "name": "System monitoring",