update to latest backend changes

This commit is contained in:
DanyLE 2023-02-17 12:37:26 +01:00
parent 2c436533f7
commit c15318f31b
41 changed files with 52 additions and 3043 deletions

View File

@ -1,21 +0,0 @@
# Blogger
Blackend for my blog at https://blog.iohub.dev
## Change logs
### v0.2.x-a
* Patch 7: Fix sendmail API security bug
* Patch 6: Chage libraries load order
* Patch 5: Add user photo to portfolio
* Patch 4: Add package dependencies
* Patch 3: Correct JSON text decoding
* Patch 2: Bug fix rendering content
* Patch 0-1 Important change: Store raw post content to the database instead of base64 string as before
### v0.1.x-a
* Patch 3-4: Enhance youtube video embedding feature in markdown
* Patch 2: CV Category now can be created when database is not created yet
* Patch 1: Fix package archive broken
* Patch 0: Change default email of the sender

View File

@ -1,32 +0,0 @@
local data = ...
-- print(data.content)
local error_msg = {}
local iserror = false
local tmp_name = "/tmp/"..os.time(os.date("!*t"))
local file = io.open (tmp_name , "w")
if file then
file:write("From: mrsang@lxsang.me\n")
file:write("Subject: " .. data.title .. "\n")
file:write( data.content.."\n")
file:close()
for k,v in pairs(data.to) do
print("sent to:"..v)
local to = v
local cmd = 'cat ' ..tmp_name .. '| sendmail ' .. to
--print(cmd)
local r = os.execute(cmd)
if not r then
iserror = true
table.insert(error_msg, v)
print("Unable to send mail to: "..v)
end
end
else
iserror = true
table.insert(error_msg, "Cannot create mail file")
end
local result = {}
result.error = iserror
result.result = error_msg
return result

Binary file not shown.

View File

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

View File

@ -1,190 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# AnTOS Web desktop is is licensed under the GNU General Public
# License v3.0, see the LICENCE file for more information
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
#along with this program. If not, see https://www.gnu.org/licenses/.
class BloggerCategoryDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "BloggerCategoryDialog", BloggerCategoryDialog.scheme
main: () ->
super.main()
@tree = @find "tree"
@txtinput = @find "txtinput"
(@find "bt-ok").onbtclick = (e) =>
sel = @tree.selectedItem
return @notify __("Please select a parent category") unless sel
seldata = sel.data
val = @txtinput.value
return @notify __("Please enter category name") if val is "" and not @data.selonly
return @notify __("Parent can not be the category itself") if @data.cat and @data.cat.id is seldata.id
@handle { p: seldata, value: val } if @handle
@quit()
(@find "bt-cancel").onbtclick = (e) =>
@quit()
if @data and @data.tree
if @data and @data.cat
@txtinput.value = @data.cat.name
if @data.cat.pid is "0"
seldata = @data.tree
else
seldata = @findDataByID @data.cat.pid, @data.tree.nodes
seldata.selected = true if seldata
@tree.data = @data.tree
@tree.expandAll()
# TODO set selected category name
findDataByID: (id, list) ->
for data in list
return data if data.id is id
if data.nodes
@findDataByID id, data.nodes
return undefined
BloggerCategoryDialog.scheme = """
<afx-app-window width='300' height='400'>
<afx-vbox>
<afx-label text="__(Pick a parent)" data-height="25" class="lbl-header" ></afx-label>
<afx-tree-view data-id="tree" ></afx-tree-view>
<afx-label text="__(Category name)" data-height="25" class="lbl-header" ></afx-label>
<input type="text" data-height="25" data-id = "txtinput"/ >
<afx-hbox data-height = '30'>
<div style=' text-align:right;'>
<afx-button data-id = "bt-ok" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
<div data-width="5"></div>
</afx-hbox>
</afx-vbox>
</afx-app-window>
"""
# This dialog is use for cv section editing
class BloggerCVSectionDiaglog extends this.OS.GUI.BasicDialog
constructor: (parent) ->
file = "#{parent.meta().path}/cvsection.html".asFileHandle()
super "BloggerCVSectionDiaglog", file
main: () ->
super.main()
@editor = new SimpleMDE
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 "vboxchange", () => @resizeContent()
@resizeContent()
resizeContent: () ->
container = @find "editor-container"
children = ($ container).children()
cheight = ($ container).height() - 30
($ children[1]).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
"""

View File

@ -1,506 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# AnTOS Web desktop is is licensed under the GNU General Public
# License v3.0, see the LICENCE file for more information
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
#along with this program. If not, see https://www.gnu.org/licenses/.
class Blogger extends this.OS.application.BaseApplication
constructor: (args) ->
super "Blogger", args
main: () ->
@user = {}
@cvlist = @find "cv-list"
@cvlist.ontreeselect = (d) =>
return unless d
data = d.data.item.data
@CVSectionByCID Number(data.id)
@inputtags = @.find "input-tags"
@bloglist = @find "blog-list"
@seclist = @find "cv-sec-list"
el = @find("photo")
$(el)
.click (e) =>
@openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
})
.then (d) =>
el.value = d.file.path
.catch (e) => @error __("Unable to get file"), e
@userdb = new @_api.DB("user")
@cvcatdb = new @_api.DB("cv_cat")
@cvsecdb = new @_api.DB("cv_sections")
@blogdb = new @_api.DB("blogs")
@tabcontainer = @find "tabcontainer"
@tabcontainer.ontabselect = (e) =>
@fetchData e.data.container.aid
(@find "bt-user-save").onbtclick = (e) =>
@saveUser()
(@find "cv-cat-add").onbtclick = (e) =>
fn = (tree) =>
@openDialog(new BloggerCategoryDialog(), {
title: __("Add category"),
tree: tree
}).then (d) =>
c =
name: d.value,
pid: d.p.id,
publish: 1
@cvcatdb.save c
.then (r) =>
@refreshCVCat()
.catch (e) => @error __("Cannot add new category"), e
.catch (e) => @error e.toString(), e
@fetchCVCat()
.then (tree) => fn(tree)
.catch (e) =>
data =
text: "Porfolio",
id:"0",
nodes: []
fn(data)
@error __("Unable to fetch categories"), e
(@find "cv-cat-edit").onbtclick = (e) =>
sel = @cvlist.selectedItem
return unless sel
cat = sel.data
return unless cat
@fetchCVCat().then (tree) =>
@openDialog(new BloggerCategoryDialog(), {
title: __("Edit category"),
tree: tree, cat: cat
}).then (d) =>
c =
id: cat.id,
publish: cat.publish,
pid: d.p.id,
name: d.value
@cvcatdb.save c
.then (r) =>
@refreshCVCat()
.catch (e) =>
@error __("Cannot Edit category"), e
.catch (e) => @error __("Unable to fetch categories"), e
(@find "cv-cat-del").onbtclick = (e) =>
sel = @cvlist.selectedItem
return unless sel
cat = sel.data
return unless cat
@openDialog("YesNoDialog", {
title: __("Delete category") ,
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", cat.name)
}).then (d) =>
return unless d
@deleteCVCat cat
.catch (e) => @error e.toString(), e
(@find "cv-sec-add").onbtclick = (e) =>
sel = @cvlist.selectedItem
return unless sel
cat = sel.data
return @notify __("Please select a category") unless cat and cat.id isnt "0"
@openDialog(new BloggerCVSectionDiaglog(@), {
title: __("New section entry for {0}", cat.name)
}).then (d) =>
d.cid = Number cat.id
d.start = Number d.start
d.end = Number d.end
# d.publish = 1
@cvsecdb.save d
.then (r) =>
@CVSectionByCID Number(cat.id)
.catch (e) => @error __("Cannot save section: {0}", e.toString()), e
(@find "cv-sec-move").onbtclick = (e) =>
sel = (@find "cv-sec-list").selectedItem
return @notify __("Please select a section to move") unless sel
sec = sel.data
@fetchCVCat().then (tree) =>
@openDialog(new BloggerCategoryDialog(),{
title: __("Move to"),
tree: tree,
selonly: true
}).then (d) =>
c =
id: sec.id,
cid: d.p.id
@cvsecdb.save c
.then (r) =>
@CVSectionByCID(sec.cid)
(@find "cv-sec-list").unselect()
.catch (e) => @error __("Cannot move section"), e
(@find "cv-sec-edit").onbtclick = (e) =>
sel = (@find "cv-sec-list").selectedItem
return @notify __("Please select a section to edit") unless sel
sec = sel.data
@openDialog(new BloggerCVSectionDiaglog(@), {
title: __("Modify section entry"),
section: sec
}).then (d) =>
d.cid = Number sec.cid
d.start = Number d.start
d.end = Number d.end
#d.publish = Number sec.publish
@cvsecdb.save d
.then (r) =>
@CVSectionByCID Number(sec.cid)
.catch (e) => return @error __("Cannot save section: {0}", e.toString()), e
@seclist.onitemclose = (e) =>
return unless e
data = e.data.item.data
@openDialog("YesNoDialog", {
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", data.title)
}).then (b) =>
return unless b
@cvsecdb.delete data.id
.then (r) =>
@seclist.delete e.data.item
.catch (e) => @error __("Cannot delete the section: {0}", e.toString()), e
return false
@editor = new SimpleMDE
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
SimpleMDE.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 "vboxchange", () =>
@resizeContent()
@resizeContent()
@loadBlogs()
# @fetchData 0
# USER TAB
fetchData: (idx) ->
switch idx
when "user-container" #user info
@userdb.get null
.then (d) =>
@user = d[0]
inputs = @select "[input-class='user-input']"
($ v).val @user[v.name] for v in inputs
.catch (e) => @error __("Cannot fetch user data"), e
when "cv-container" # category
@refreshCVCat()
else
@loadBlogs()
saveUser:() ->
inputs = @select "[input-class='user-input']"
@user[v.name] = ($ v).val() for v in inputs
return @notify __("Full name must be entered") if not @user.fullname or @user.fullname is ""
#console.log @user
@userdb.save @user
.then (r) =>
return @notify __("User data updated")
.catch (e) => return @error __("Cannot save user data"), e
# PORFOLIO TAB
refreshCVCat: () ->
@fetchCVCat().then (data) =>
@cvlist.data = data
@cvlist.expandAll()
.catch (e) => @error __("Unable to load categories"), e
fetchCVCat: () ->
new Promise (resolve, reject) =>
data =
text: "Porfolio",
id:"0",
nodes: []
cnd =
order:
name: "ASC"
@cvcatdb.find cnd
.then (d) =>
@catListToTree d, data, "0"
resolve data
.catch (e) -> reject __e e
#it = (@cvlist.find "pid", "2")[0]
#@cvlist.set "selectedItem", it
catListToTree: (table, data, id) ->
result = (v for v in table when v.pid is id)
return data.nodes = null if result.length is 0
for v in result
v.nodes = []
v.text = v.name
@catListToTree table, v, v.id
#v.nodes = null if v.nodes.length is 0
data.nodes.push v
deleteCVCat: (cat) ->
me = @
ids = []
func = (c) ->
ids.push c.id
func(v) for v in c.nodes if c.nodes
func(cat)
cond = ({ "=": { cid: v } } for v in ids)
# delete all content
@cvsecdb.delete({ "or": cond }).then (r) =>
cond = ({ "=": { id: v } } for v in ids)
@cvcatdb.delete({ "or": cond }).then (re) =>
@refreshCVCat()
@seclist.data=[]
.catch (e) =>
@error __("Cannot delete the category: {0} [{1}]", cat.name, e.toString()), e
.catch (e) =>
@error __("Cannot delete all content of: {0} [{1}]", cat.name, e.toString()), e
CVSectionByCID: (cid) ->
cond =
exp:
"=":
cid: cid
order:
start: "DESC"
@cvsecdb.find(cond).then (d) =>
items = []
(@find "cv-sec-status").text = __("Found {0} sections", d.length)
for v in d
v.closable = true
v.tag = "afx-blogger-cvsection-item"
v.start = Number(v.start)
v.end = Number(v.end)
v.start = undefined if v.start < 1000
v.end = undefined if v.end < 1000
items.push v
@seclist.data = items
.catch (e) => @error e.toString(), e
# blog
saveBlog: () ->
sel = undefined
selel = @bloglist.selectedItem
sel = selel.data if selel
tags = @inputtags.value
content = @editor.value()
title = (new RegExp "^#+(.*)\n", "g").exec content
return @notify __("Please insert a title in the text: beginning with heading") unless title and title.length is 2
return @notify __("Please enter tags") if tags is ""
d = new Date()
data =
content: content
title: title[1].trim()
tags: tags
ctime: if sel then sel.ctime else d.timestamp()
ctimestr: if sel then sel.ctimestr else d.toString()
utime: d.timestamp()
utimestr: d.toString()
rendered: @process(@editor.options.previewRender(content))
publish: if (@find "blog-publish").swon then 1 else 0
data.id = sel.id if sel
#save the data
@blogdb.save data
.then (r) =>
@loadBlogs()
.catch (e) => @error __("Cannot save blog: {0}", e.toString()), e
process: (text) ->
# find video tag and rendered it
embed = (id) ->
return """
<iframe
class = "embeded-video"
width="560" height="315"
src="https://www.youtube.com/embed/#{id}"
frameborder="0" allow="encrypted-media" allowfullscreen
></iframe>
"""
re = /\[\[youtube:([^\]]*)\]\]/g
replace = []
while (found = re.exec text) isnt null
replace.push found
return text unless replace.length > 0
ret = ""
begin = 0
for it in replace
ret += text.substring begin, it.index
ret += embed(it[1])
begin = it.index + it[0].length
ret += text.substring begin, text.length
#console.log ret
return ret
clearEditor:() ->
@.editor.value ""
@.inputtags.value = ""
(@.find "blog-publish").swon = false
# load blog
loadBlogs: () ->
selidx = -1
el = @bloglist.selectedItem
selidx = $(el).index()
cond =
order:
ctime: "DESC"
fields: [
"id",
"title",
"ctimestr",
"ctime",
"utime",
"utimestr"
]
@blogdb.find cond
.then (r) =>
v.tag = "afx-blogger-post-item" for v in r
@bloglist.data = r
if selidx isnt -1
@bloglist.selected = selidx
else
@clearEditor()
@bloglist.selected = -1
.catch (e) => @error __("No post found: {0}", e.toString()), e
resizeContent: () ->
container = @find "editor-container"
children = ($ container).children()
titlebar = (($ @scheme).find ".afx-window-top")[0]
toolbar = children[1]
statusbar = children[4]
cheight = ($ @scheme).height() - ($ titlebar).height() - ($ toolbar).height() - ($ statusbar).height() - 90
($ children[2]).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

View File

@ -1,92 +0,0 @@
afx-app-window[data-id="blogger-win"] afx-tab-container[data-id="tabcontainer"] afx-tab-bar afx-list-view > div.list-container {
padding: 0;
margin: 0;
border-right: 1px solid #292929;
}
afx-app-window[data-id="blogger-win"] afx-tab-container[data-id="tabcontainer"] afx-tab-bar afx-list-view > div.list-container > ul li{
font-size: 15px;
padding:0;
width: 100%;
border-radius: 0;
border:0;
margin: 0;
text-align: center;
}
afx-app-window[data-id="blogger-win"] afx-tab-container[data-id="tabcontainer"] afx-tab-bar afx-list-view > div.list-container > ul li.selected {
background-color: #116cd6;
color:white;
}
afx-app-window[data-id="blogger-win"] afx-hbox[data-id="user-container"] afx-label i.label-text{
font-weight: bold;
}
afx-app-window .lbl-header i.label-text{
font-weight: bold;
}
afx-app-window[data-id="blogger-win"] afx-hbox[data-id="cv-container"] .cat-header{
border-bottom: 1px solid #cbcbcb;
text-align: center;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] > .list-container > ul .afx-cv-sec-title .label-text{
font-weight: bold;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] afx-blogger-cvsection-item afx-label {
display: block;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] afx-blogger-cvsection-item p {
padding: 0;
margin: 0;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] afx-blogger-cvsection-item .afx-cv-sec-period,
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] afx-blogger-cvsection-item .afx-cv-sec-loc {
text-align: right;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] afx-blogger-cvsection-item afx-cv-sec-content{
text-align: justify;
overflow-wrap: break-word;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] > div.list-container > ul li.selected {
border: 1px solid #116cd6;
background-color: transparent;
border-radius: 5px;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] .closable::before{
content: "\f014";
font-size: 14px;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "cv-sec-list"] .period-end::before{
content: "-";
}
afx-app-window[data-id ='blogger-win'] .editor-toolbar{
background-color: white;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "blog-list"] > div.list-container > ul li afx-label {
display: block;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "blog-list"] > div.list-container > ul .afx-blogpost-title .label-text{
font-weight: bold;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "blog-list"] > div.list-container > ul .blog-dates .label-text{
font-size: 10px;
font-weight: normal;
}
afx-app-window[data-id="blogger-win"] afx-list-view[ data-id = "blog-list"] > div.list-container > ul li.selected {
background-color: #116cd6;
color:white;
}

View File

@ -1,14 +0,0 @@
{
"app":"Blogger",
"name":"Blogging application",
"description":"Backend manager for blogging",
"info":{
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version":"0.2.7-a",
"category":"Internet",
"iconclass":"fa fa-book",
"dependencies": ["SimpleMDE@1.11.2-r","Katex@0.11.1-r"],
"mimes":["none"]
}

View File

@ -1,14 +0,0 @@
{
"name": "Blogger",
"css": ["main.css"],
"javascripts": [],
"coffees": ["main.coffee", "dialogs.coffee", "tags.coffee"],
"copies": [
"scheme.html",
"cvsection.html",
"api/sendmail.lua",
"sendmail.html",
"package.json",
"README.md"
]
}

View File

@ -1,86 +0,0 @@
<afx-app-window data-id = "blogger-win" apptitle="Blogger" width="600" height="500">
<afx-hbox >
<afx-tab-container data-id = "tabcontainer" dir = "row" tabbarwidth= "22">
<afx-hbox data-id="user-container" data-height="100%" iconclass="fa fa-user-circle">
<afx-vbox>
<afx-hbox data-height = "30">
<afx-label data-width= "70" text = "__(Full name)"></afx-label>
<input type = "text" name="fullname" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30">
<afx-label text = "__(Address)" data-width= "70"></afx-label>
<input type = "text" name="address" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30">
<afx-label text = "__(Phone)" data-width= "70"></afx-label>
<input type = "text" name="Phone" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30">
<afx-label text = "__(Email)" data-width= "70"></afx-label>
<input type = "text" name="email" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30">
<afx-label text = "__(Url)" data-width= "70"></afx-label>
<input type = "text" name="url" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30">
<afx-label text = "__(Photo)" data-width= "70"></afx-label>
<input type = "text" name="photo" data-id="photo" readonly="readonly" input-class = "user-input"></input>
</afx-hbox>
<afx-label data-height = "30" text = "__(Short biblio)"></afx-label>
<textarea name="shortbiblio" input-class = "user-input"></textarea>
<afx-hbox data-height = "35">
<div></div>
<afx-button iconclass = "fa fa-save" data-id = "bt-user-save" data-width="60" text = "__(Save)"></afx-button>
</afx-hbox>
</afx-vbox>
</afx-hbox>
<afx-hbox data-id="cv-container" data-height="100%" iconclass="fa fa-info-circle">
<div data-width="5"></div>
<afx-vbox data-width="150" min-width="100">
<afx-label class="lbl-header" data-height = "23" text = "__(Categories)" iconclass = "fa fa-bars"></afx-label>
<afx-tree-view data-id = "cv-list" ></afx-tree-view>
<afx-hbox data-height="30" class = "cv-side-bar-btn">
<afx-button data-id = "cv-cat-add" data-width = "25" text = "" iconclass = "fa fa-plus-circle"></afx-button>
<afx-button data-id = "cv-cat-del" data-width = "25" text = "" iconclass = "fa fa-minus-circle"></afx-button>
<afx-button data-id = "cv-cat-edit" data-width = "25" text = "" iconclass = "fa fa-pencil-square-o"></afx-button>
</afx-hbox>
</afx-vbox>
<afx-resizer data-width = "2"></afx-resizer>
<afx-vbox>
<afx-list-view data-id = "cv-sec-list" ></afx-list-view>
<afx-hbox data-height="30" class = "cv-side-bar-btn">
<afx-label data-id = "cv-sec-status"></afx-label>
<afx-button data-id = "cv-sec-add" data-width = "25" text = "" iconclass = "fa fa-plus-circle"></afx-button>
<afx-button data-id = "cv-sec-edit" data-width = "25" text = "" iconclass = "fa fa-pencil-square-o"></afx-button>
<afx-button data-id = "cv-sec-move" data-width = "25" text = "" iconclass = "fa fa-exchange"></afx-button>
</afx-hbox>
</afx-vbox>
<div data-width="5"></div>
</afx-hbox>
<afx-hbox data-id = "blog-container" data-height="100%" iconclass="fa fa-book">
<afx-list-view data-id = "blog-list" min-width="100" data-width="200"></afx-list-view>
<afx-resizer data-width = "3"></afx-resizer>
<afx-vbox>
<div data-id = "editor-container">
<textarea data-id="markarea" ></textarea>
</div>
<afx-label text = "__(Tags)" style="font-weight:bold;" data-height="25" ></afx-label>
<afx-hbox data-height="25">
<input type = "text" data-id = "input-tags" ></input>
<div data-width="5"></div>
<afx-switch data data-id = "blog-publish" data-width="30"></afx-switch>
<div data-width="5"></div>
</afx-hbox>
<div data-height="5"></div>
</afx-vbox>
</afx-hbox>
</afx-tab-container>
</afx-hbox>
</afx-app-window>

View File

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

View File

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

View File

@ -1,15 +0,0 @@
# DBDecoder
This is an example project, generated by AntOS Development Kit
## Howto
Use the CodePad command palette to access to the SDK functionalities:
1. Create new project
2. Init the project from the current folder located in side bar
3. Build and run the project
4. Release the project in zip package
## Set up build target
Open the `project.json` file from the current project tree and add/remove
build target entries. Save the file

View File

@ -1,5 +0,0 @@
<afx-app-window apptitle="DBDecoder" width="500" height="400" data-id="DBDecoder">
<afx-hbox >
<afx-button data-id="decoder" text="GO"></afx-button>
</afx-hbox>
</afx-app-window>

View File

@ -1,15 +0,0 @@
# DBDecoder
This is an example project, generated by AntOS Development Kit
## Howto
Use the CodePad command palette to access to the SDK functionalities:
1. Create new project
2. Init the project from the current folder located in side bar
3. Build and run the project
4. Release the project in zip package
## Set up build target
Open the `project.json` file from the current project tree and add/remove
build target entries. Save the file

View File

@ -1,54 +0,0 @@
(function() {
var DBDecoder;
DBDecoder = class DBDecoder extends this.OS.application.BaseApplication {
constructor(args) {
super("DBDecoder", args);
}
main() {
var bt;
bt = this.find("decoder");
this.db = new this._api.DB("blogs");
return bt.onbtclick = (e) => {
// decode the database
return this.db.find("1=1").then((data) => {
var i, len, v;
for (i = 0, len = data.length; i < len; i++) {
v = data[i];
v.content = atob(v.content);
v.rendered = atob(v.rendered);
}
return this.saveDB(data).then(() => {
return this.notify("Data base saved");
}).catch((e) => {
return this.error(e.toString(), e);
});
});
};
}
saveDB(list) {
return new Promise((resolve, reject) => {
var record;
if (list.length === 0) {
return resolve();
}
record = list.shift();
return this.db.save(record).then(() => {
return this.saveDB(list).then(() => {
return resolve();
}).catch((e) => {
return reject(__e(e));
});
}).catch((e) => {
return reject(__e(e));
});
});
}
};
this.OS.register("DBDecoder", DBDecoder);
}).call(this);

View File

@ -1,14 +0,0 @@
{
"app":"DBDecoder",
"name":"DBDecoder",
"description":"DBDecoder",
"info":{
"author": "",
"email": ""
},
"version":"0.0.2-a",
"category":"Other",
"iconclass":"fa fa-adn",
"mimes":["none"],
"locale": {}
}

View File

@ -1,5 +0,0 @@
<afx-app-window apptitle="DBDecoder" width="500" height="400" data-id="DBDecoder">
<afx-hbox >
<afx-button data-id="decoder" text="GO"></afx-button>
</afx-hbox>
</afx-app-window>

View File

@ -1,29 +0,0 @@
class DBDecoder extends this.OS.application.BaseApplication
constructor: ( args ) ->
super "DBDecoder", args
main: () ->
bt = @find "decoder"
@db = new @_api.DB("blogs")
bt.onbtclick = (e) =>
# decode the database
@db.find("1=1").then (data) =>
for v in data
v.content = atob(v.content)
v.rendered = atob(v.rendered)
@saveDB(data).then () =>
@notify "Data base saved"
.catch (e) => @error e.toString(), e
saveDB: (list) ->
new Promise (resolve, reject) =>
return resolve() if list.length is 0
record = list.shift()
@db.save(record).then () =>
@saveDB(list)
.then () => resolve()
.catch (e) => reject __e e
.catch (e) => reject __e e
this.OS.register "DBDecoder", DBDecoder

View File

@ -1,14 +0,0 @@
{
"app":"DBDecoder",
"name":"DBDecoder",
"description":"DBDecoder",
"info":{
"author": "",
"email": ""
},
"version":"0.0.2-a",
"category":"Other",
"iconclass":"fa fa-adn",
"mimes":["none"],
"locale": {}
}

View File

@ -1,7 +0,0 @@
{
"name": "DBDecoder",
"css": [],
"javascripts": [],
"coffees": ["coffees/main.coffee"],
"copies": ["assets/scheme.html", "package.json", "README.md"]
}

View File

@ -1,10 +0,0 @@
# Docify
Simple PDF document manager
## Change logs
- v0.0.8-b: Allow upload files directly from the app
- v0.0.7-a: Change category and icon
- v0.0.6-a: Add print dialog (support server side printing)
- v0.0.5-a: Fix delete file bug
- v0.0.4-a: Display file size in entry meta-data
- v0.0.3-a: Fix document moved bug, sort entries by year, month, day

View File

@ -1,435 +0,0 @@
local arg = ...
ulib = require("ulib")
sqlite = modules.sqlite()
vfs = require("vfs")
local handle = {}
local docpath = nil
local dbpath = nil
local result = function(data)
return {
error = false,
result = data
}
end
local error = function(data)
return {
error = data,
result = false
}
end
local mkdirp =function(p)
if not vfs.exists(p) then
if not vfs.mkdir(p) then
return false, error("Unable to create directory: "..p)
end
end
return true, nil
end
local merge_files = function(data)
local firstfile = data.file[1]
local fpath = docpath.."/"..data.cid
local r, e = mkdirp(fpath)
if not r then return e end
fpath = fpath.."/"..os.date("%d-%m-%Y_%H_%M_%S")..".pdf"
-- concat the files
if #data.file > 1 then
local cmd = "gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile="..vfs.ospath(fpath)
for i,v in ipairs(data.file) do
cmd = cmd.." \""..vfs.ospath(v).."\""
end
os.execute(cmd)
if not vfs.exists(fpath) then
return error("Unable to merge PDF files")
end
cmd = "chmod 777 "..vfs.ospath(fpath)
os.execute(cmd)
else
local cmd = "mv \""..vfs.ospath(firstfile).."\" \""..vfs.ospath(fpath).."\""
os.execute(cmd)
if not vfs.exists(fpath) then
return error("Unable to move PDF file")
end
end
-- move the thumb file to the cache folder
local thumb = docpath.."/cache/"..std.sha1(firstfile:gsub(docpath, ""))..".png"
local desthumb = docpath.."/cache/"..std.sha1(fpath:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.move(thumb, desthumb)
end
-- remove all other thumb files
for i,v in ipairs(data.file) do
thumb = docpath.."/cache/"..std.sha1(v:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
-- delete all files
if vfs.exists(v) then
vfs.delete(v)
end
end
return result(fpath)
end
handle.init = function(args)
local r, e = mkdirp(docpath)
if not r then return e end
r, e = mkdirp(docpath.."/unclassified")
if not r then return e end
r, e = mkdirp(docpath.."/cache")
if not r then return e end
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to initialized database "..dbpath)
end
local sql
-- check if table exists
if sqlite.hasTable(db, "categories") == 0 then
-- create the table
sql = [[
CREATE TABLE "categories" (
"id" INTEGER,
"name" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create table categories")
end
-- insert unknown category
sql = [[
INSERT INTO categories("id","name") VALUES (0,'Uncategoried');
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create default category")
end
end
if sqlite.hasTable(db, "owners") == 0 then
-- create the table
sql = [[
CREATE TABLE "owners" (
"id" INTEGER,
"name" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create table owners")
end
-- insert unknown category
sql = [[
INSERT INTO owners("id","name") VALUES (0,'None');
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create default None owner")
end
end
if sqlite.hasTable(db, "docs") == 0 then
-- create the table
sql = [[
CREATE TABLE "docs" (
"id" INTEGER,
"name" TEXT NOT NULL,
"ctime" INTEGER,
"day" INTEGER,
"month" INTEGER,
"year" INTEGER,
"cid" INTEGER DEFAULT 0,
"oid" INTEGER DEFAULT 0,
"file" TEXT NOT NULL,
"tags" TEXT,
"note" TEXT,
"mtime" INTEGER,
FOREIGN KEY("oid") REFERENCES "owners"("id") ON DELETE SET DEFAULT ON UPDATE NO ACTION,
FOREIGN KEY("cid") REFERENCES "categories"("id") ON DELETE SET DEFAULT ON UPDATE NO ACTION,
PRIMARY KEY("id" AUTOINCREMENT)
);
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create table docs")
end
end
sqlite.dbclose(db)
return result("Docify initialized")
end
handle.select = function(param)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local r = sqlite.select(db, param.table, "*", param.cond)
sqlite.dbclose(db)
if r == nil then
return error("Unable to select data from "..param.table)
else
return result(r)
end
end
handle.fetch = function(table)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local r = sqlite.select(db, table, "*", "1=1")
sqlite.dbclose(db)
if r == nil then
return error("Unable to fetch data from "..table)
else
return result(r)
end
end
handle.insert = function(param)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local keys = {}
local values = {}
for k,v in pairs(param.data) do
if k ~= "id" then
table.insert(keys,k)
if type(v) == "number" then
table.insert(values, v)
elseif type(v) == "boolean" then
table.insert( values, v and 1 or 0 )
else
local t = "\""..v:gsub('"', '""').."\""
table.insert(values,t)
end
end
end
local sql = "INSERT INTO "..param.table.." ("..table.concat(keys,',')..') VALUES ('
sql = sql..table.concat(values,',')..');'
local r = sqlite.query(db, sql)
sqlite.dbclose(db)
if r == nil then
return error("Unable to insert data to "..param.table)
else
return result("Data inserted")
end
end
handle.preview = function(path)
-- convert -resize 300x500 noel.pdf[0] thumb.png
local name = std.sha1(path:gsub(docpath,""))..".png"
-- try to find the thumb
local tpath = docpath.."/cache/"..name
if not vfs.exists(tpath) then
-- regenerate thumb
local cmd = "convert -resize 250x500 \""..vfs.ospath(path).."\"[0] "..vfs.ospath(tpath)
os.execute(cmd)
end
if vfs.exists(tpath) then
local cmd = "chmod 777 "..vfs.ospath(tpath)
os.execute(cmd)
return result(tpath)
else
return error("do not exist")
end
end
handle.get_doc = function(id)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local r = sqlite.select(db, "docs", "*", "id = "..id)
if r == nil or #r == 0 then
sqlite.dbclose(db)
return error("Unable to select data from "..param.table)
else
r = r[1]
local ret, meta = vfs.fileinfo(r.file)
if ret then
r.fileinfo = meta
end
local o = sqlite.select(db, "owners", "*", "id = "..r.oid)
sqlite.dbclose(db)
if o == nil or #o == 0 then
return result(r)
else
o = o[1]
r.owner = o.name
if r.ctime then
r.ctime = os.date("%d/%m/%Y %H:%M:%S", r.ctime)
end
if r.mtime then
r.mtime = os.date("%d/%m/%Y %H:%M:%S", r.mtime)
end
local edate = ""
return result(r)
end
end
end
handle.deletedoc = function(param)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local sql = "DELETE FROM docs WHERE id="..param.id..";"
local ret = sqlite.query(db, sql) == 1
sqlite.dbclose(db)
if not ret then
return error("Unable to delete doc meta-data from database")
end
-- move file to unclassified
local newfile = docpath.."/unclassified/"..std.basename(param.file)
vfs.move(param.file, newfile)
-- delete thumb file
local thumb = docpath.."/cache/"..std.sha1(param.file:gsub(docpath,""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
return result("Document entry deleted")
end
handle.updatedoc = function(param)
local r = merge_files(param.data)
if r.error then return r end
if param.rm then
-- move ve the old file to unclassified
local newfile = docpath.."/unclassified/"..std.basename(param.rm)
local cmd = "rm -f "..vfs.ospath(param.rm)
os.execute(cmd)
--if vfs.exists(param.rm) then
-- vfs.move(param.rm, newfile)
--end
-- move the thumb file if needed
local thumb = docpath.."/cache/"..std.sha1(param.rm:gsub(docpath,""))..".png"
local newwthumb = docpath.."/cache/"..std.sha1(newfile:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.move(thumb, newwthumb)
end
end
param.data.file = r.result
print(r.result)
param.data.mtime = os.time(os.date("!*t"))
return handle.update({
table = "docs",
data = param.data
})
end
handle.insertdoc = function(data)
local r = merge_files(data)
if r.error then return r end
-- save data
data.file = r.result
data.ctime = os.time(os.date("!*t"))
data.mtime = os.time(os.date("!*t"))
local ret = handle.insert({
table = "docs",
data = data
})
return ret
end
handle.update = function(param)
if not param.data.id or param.data.id == 0 then
return error("Record id is 0 or not found")
end
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local lst = {}
for k,v in pairs(param.data) do
if(type(v)== "number") then
table.insert(lst,k.."="..v)
elseif type(v) == "boolean" then
table.insert( lst, k.."="..(v and 1 or 0) )
else
table.insert(lst,k.."=\""..v:gsub('"', '""').."\"")
end
end
local sql = "UPDATE "..param.table.." SET "..table.concat(lst,",").." WHERE id="..param.data.id..";"
local r = sqlite.query(db, sql)
sqlite.dbclose(db)
if r == nil then
return error("Unable to update data to "..param.table)
else
return result("Data Updated")
end
end
handle.delete = function(param)
if param.id == 0 then
return error("Record with id = 0 cannot be deleted")
end
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local sql = "DELETE FROM "..param.table.." WHERE id="..param.id..";"
local r = sqlite.query(db, sql)
sqlite.dbclose(db)
if r == nil then
return error("Unable to delete data from "..param.table)
else
return result("Data deleted")
end
end
handle.printdoc = function(opt)
local cmd = "lp "
if opt.printer and opt.printer ~= "" then
cmd = cmd .. " -d "..opt.printer
end
if opt.side == 0 then
cmd = cmd.. " -o sides=one-sided"
elseif opt.side == 1 then
cmd = cmd.. " -o sides=two-sided-long-edge"
elseif opt.side == 2 then
cmd = cmd .. " -o sides=two-sided-short-edge"
end
-- orientation landscape
if opt.orientation == 1 then
cmd = cmd.." -o orientation-requested=5"
end
if opt.range == 1 then
cmd = cmd.." -P "..opt.pages
end
cmd = cmd.. " "..vfs.ospath(opt.file)
print(cmd)
os.execute(cmd)
return result("A print job has been posted on server. Check if it successes")
end
if arg.action and handle[arg.action] then
-- check if the database exits
docpath = arg.docpath
dbpath = docpath.."/docify.db"
return handle[arg.action](arg.args)
else
return error("Invalid action parameter")
end

View File

@ -1,34 +0,0 @@
<afx-app-window apptitle="Docify" width="700" height="500" data-id="Docify">
<afx-hbox >
<div data-width="5"></div>
<afx-vbox data-width="150">
<afx-label class="header" iconclass = "fa fa-bars" text="__(Categories)" data-height="22"></afx-label>
<afx-list-view data-id="catview"></afx-list-view>
<div data-height="5"></div>
</afx-vbox>
<afx-resizer data-width="4"></afx-resizer>
<afx-vbox data-width = "300">
<afx-label class="header" iconclass = "fa fa-bars" text="__(Documents)" data-height="22"></afx-label>
<afx-list-view data-id="docview"></afx-list-view>
<afx-hbox data-height="30">
<div data-width="5"></div>
<afx-button data-id="bt-add-doc" data-width = "25" text = "" iconclass = "fa fa-plus-circle"></afx-button>
<afx-button data-id="bt-del-doc" data-width = "25" text = "" iconclass = "fa fa-minus-circle"></afx-button>
<afx-button data-id="bt-edit-doc" data-width = "25" text = "" iconclass = "fa fa-pencil-square-o"></afx-button>
<afx-button data-id="bt-upload-doc" data-width = "25" text = "" iconclass = "bi bi-cloud-upload"></afx-button>
</afx-hbox>
</afx-vbox>
<afx-resizer data-width="4"></afx-resizer>
<afx-vbox>
<div data-id = "preview-container">
<canvas data-id="preview-canvas"></canvas>
</div>
<afx-grid-view data-id="docgrid"></afx-grid-view>
<div style="text-align: right;" data-height="30" >
<afx-button text="__(Open)" data-id="btopen" ></afx-button>
<afx-button text="__(Download)" data-id="btdld" ></afx-button>
<afx-button text="__(Print on server)" data-id="btprint" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>

View File

@ -1,83 +0,0 @@
{
"name": "Docify",
"targets": {
"init": {
"jobs": [
{
"name": "vfs-mkdir",
"data": [
"build",
"build/debug",
"build/release"
]
}
]
},
"coffee": {
"require": [
"coffee"
],
"jobs": [
{
"name": "coffee-compile",
"data": {
"src": [
"coffees/dialogs.coffee",
"coffees/main.coffee"
],
"dest": "build/debug/main.js"
}
}
]
},
"uglify": {
"require": [
"terser"
],
"jobs": [
{
"name": "terser-uglify",
"data": [
"build/debug/main.js"
]
}
]
},
"copy": {
"jobs": [
{
"name": "vfs-cp",
"data": {
"src": [
"assets/scheme.html",
"api/api.lua",
"package.json",
"README.md"
],
"dest": "build/debug"
}
}
]
},
"release": {
"require": [
"zip"
],
"depend": [
"init",
"coffee",
"uglify",
"copy"
],
"jobs": [
{
"name": "zip-mk",
"data": {
"src": "build/debug",
"dest": "build/release/Docify.zip"
}
}
]
}
}
}

View File

@ -1,10 +0,0 @@
# Docify
Simple PDF document manager
## Change logs
- v0.0.8-b: Allow upload files directly from the app
- v0.0.7-a: Change category and icon
- v0.0.6-a: Add print dialog (support server side printing)
- v0.0.5-a: Fix delete file bug
- v0.0.4-a: Display file size in entry meta-data
- v0.0.3-a: Fix document moved bug, sort entries by year, month, day

View File

@ -1,435 +0,0 @@
local arg = ...
ulib = require("ulib")
sqlite = modules.sqlite()
vfs = require("vfs")
local handle = {}
local docpath = nil
local dbpath = nil
local result = function(data)
return {
error = false,
result = data
}
end
local error = function(data)
return {
error = data,
result = false
}
end
local mkdirp =function(p)
if not vfs.exists(p) then
if not vfs.mkdir(p) then
return false, error("Unable to create directory: "..p)
end
end
return true, nil
end
local merge_files = function(data)
local firstfile = data.file[1]
local fpath = docpath.."/"..data.cid
local r, e = mkdirp(fpath)
if not r then return e end
fpath = fpath.."/"..os.date("%d-%m-%Y_%H_%M_%S")..".pdf"
-- concat the files
if #data.file > 1 then
local cmd = "gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -sOutputFile="..vfs.ospath(fpath)
for i,v in ipairs(data.file) do
cmd = cmd.." \""..vfs.ospath(v).."\""
end
os.execute(cmd)
if not vfs.exists(fpath) then
return error("Unable to merge PDF files")
end
cmd = "chmod 777 "..vfs.ospath(fpath)
os.execute(cmd)
else
local cmd = "mv \""..vfs.ospath(firstfile).."\" \""..vfs.ospath(fpath).."\""
os.execute(cmd)
if not vfs.exists(fpath) then
return error("Unable to move PDF file")
end
end
-- move the thumb file to the cache folder
local thumb = docpath.."/cache/"..std.sha1(firstfile:gsub(docpath, ""))..".png"
local desthumb = docpath.."/cache/"..std.sha1(fpath:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.move(thumb, desthumb)
end
-- remove all other thumb files
for i,v in ipairs(data.file) do
thumb = docpath.."/cache/"..std.sha1(v:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
-- delete all files
if vfs.exists(v) then
vfs.delete(v)
end
end
return result(fpath)
end
handle.init = function(args)
local r, e = mkdirp(docpath)
if not r then return e end
r, e = mkdirp(docpath.."/unclassified")
if not r then return e end
r, e = mkdirp(docpath.."/cache")
if not r then return e end
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to initialized database "..dbpath)
end
local sql
-- check if table exists
if sqlite.hasTable(db, "categories") == 0 then
-- create the table
sql = [[
CREATE TABLE "categories" (
"id" INTEGER,
"name" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create table categories")
end
-- insert unknown category
sql = [[
INSERT INTO categories("id","name") VALUES (0,'Uncategoried');
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create default category")
end
end
if sqlite.hasTable(db, "owners") == 0 then
-- create the table
sql = [[
CREATE TABLE "owners" (
"id" INTEGER,
"name" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create table owners")
end
-- insert unknown category
sql = [[
INSERT INTO owners("id","name") VALUES (0,'None');
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create default None owner")
end
end
if sqlite.hasTable(db, "docs") == 0 then
-- create the table
sql = [[
CREATE TABLE "docs" (
"id" INTEGER,
"name" TEXT NOT NULL,
"ctime" INTEGER,
"day" INTEGER,
"month" INTEGER,
"year" INTEGER,
"cid" INTEGER DEFAULT 0,
"oid" INTEGER DEFAULT 0,
"file" TEXT NOT NULL,
"tags" TEXT,
"note" TEXT,
"mtime" INTEGER,
FOREIGN KEY("oid") REFERENCES "owners"("id") ON DELETE SET DEFAULT ON UPDATE NO ACTION,
FOREIGN KEY("cid") REFERENCES "categories"("id") ON DELETE SET DEFAULT ON UPDATE NO ACTION,
PRIMARY KEY("id" AUTOINCREMENT)
);
]]
if sqlite.query(db, sql) ~= 1 then
sqlite.dbclose(db)
return error("Unable to create table docs")
end
end
sqlite.dbclose(db)
return result("Docify initialized")
end
handle.select = function(param)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local r = sqlite.select(db, param.table, "*", param.cond)
sqlite.dbclose(db)
if r == nil then
return error("Unable to select data from "..param.table)
else
return result(r)
end
end
handle.fetch = function(table)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local r = sqlite.select(db, table, "*", "1=1")
sqlite.dbclose(db)
if r == nil then
return error("Unable to fetch data from "..table)
else
return result(r)
end
end
handle.insert = function(param)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local keys = {}
local values = {}
for k,v in pairs(param.data) do
if k ~= "id" then
table.insert(keys,k)
if type(v) == "number" then
table.insert(values, v)
elseif type(v) == "boolean" then
table.insert( values, v and 1 or 0 )
else
local t = "\""..v:gsub('"', '""').."\""
table.insert(values,t)
end
end
end
local sql = "INSERT INTO "..param.table.." ("..table.concat(keys,',')..') VALUES ('
sql = sql..table.concat(values,',')..');'
local r = sqlite.query(db, sql)
sqlite.dbclose(db)
if r == nil then
return error("Unable to insert data to "..param.table)
else
return result("Data inserted")
end
end
handle.preview = function(path)
-- convert -resize 300x500 noel.pdf[0] thumb.png
local name = std.sha1(path:gsub(docpath,""))..".png"
-- try to find the thumb
local tpath = docpath.."/cache/"..name
if not vfs.exists(tpath) then
-- regenerate thumb
local cmd = "convert -resize 250x500 \""..vfs.ospath(path).."\"[0] "..vfs.ospath(tpath)
os.execute(cmd)
end
if vfs.exists(tpath) then
local cmd = "chmod 777 "..vfs.ospath(tpath)
os.execute(cmd)
return result(tpath)
else
return error("do not exist")
end
end
handle.get_doc = function(id)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local r = sqlite.select(db, "docs", "*", "id = "..id)
if r == nil or #r == 0 then
sqlite.dbclose(db)
return error("Unable to select data from "..param.table)
else
r = r[1]
local ret, meta = vfs.fileinfo(r.file)
if ret then
r.fileinfo = meta
end
local o = sqlite.select(db, "owners", "*", "id = "..r.oid)
sqlite.dbclose(db)
if o == nil or #o == 0 then
return result(r)
else
o = o[1]
r.owner = o.name
if r.ctime then
r.ctime = os.date("%d/%m/%Y %H:%M:%S", r.ctime)
end
if r.mtime then
r.mtime = os.date("%d/%m/%Y %H:%M:%S", r.mtime)
end
local edate = ""
return result(r)
end
end
end
handle.deletedoc = function(param)
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local sql = "DELETE FROM docs WHERE id="..param.id..";"
local ret = sqlite.query(db, sql) == 1
sqlite.dbclose(db)
if not ret then
return error("Unable to delete doc meta-data from database")
end
-- move file to unclassified
local newfile = docpath.."/unclassified/"..std.basename(param.file)
vfs.move(param.file, newfile)
-- delete thumb file
local thumb = docpath.."/cache/"..std.sha1(param.file:gsub(docpath,""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
return result("Document entry deleted")
end
handle.updatedoc = function(param)
local r = merge_files(param.data)
if r.error then return r end
if param.rm then
-- move ve the old file to unclassified
local newfile = docpath.."/unclassified/"..std.basename(param.rm)
local cmd = "rm -f "..vfs.ospath(param.rm)
os.execute(cmd)
--if vfs.exists(param.rm) then
-- vfs.move(param.rm, newfile)
--end
-- move the thumb file if needed
local thumb = docpath.."/cache/"..std.sha1(param.rm:gsub(docpath,""))..".png"
local newwthumb = docpath.."/cache/"..std.sha1(newfile:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.move(thumb, newwthumb)
end
end
param.data.file = r.result
print(r.result)
param.data.mtime = os.time(os.date("!*t"))
return handle.update({
table = "docs",
data = param.data
})
end
handle.insertdoc = function(data)
local r = merge_files(data)
if r.error then return r end
-- save data
data.file = r.result
data.ctime = os.time(os.date("!*t"))
data.mtime = os.time(os.date("!*t"))
local ret = handle.insert({
table = "docs",
data = data
})
return ret
end
handle.update = function(param)
if not param.data.id or param.data.id == 0 then
return error("Record id is 0 or not found")
end
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local lst = {}
for k,v in pairs(param.data) do
if(type(v)== "number") then
table.insert(lst,k.."="..v)
elseif type(v) == "boolean" then
table.insert( lst, k.."="..(v and 1 or 0) )
else
table.insert(lst,k.."=\""..v:gsub('"', '""').."\"")
end
end
local sql = "UPDATE "..param.table.." SET "..table.concat(lst,",").." WHERE id="..param.data.id..";"
local r = sqlite.query(db, sql)
sqlite.dbclose(db)
if r == nil then
return error("Unable to update data to "..param.table)
else
return result("Data Updated")
end
end
handle.delete = function(param)
if param.id == 0 then
return error("Record with id = 0 cannot be deleted")
end
local db = sqlite._getdb(vfs.ospath(dbpath))
if not db then
return error("Unable to get database "..dbpath)
end
local sql = "DELETE FROM "..param.table.." WHERE id="..param.id..";"
local r = sqlite.query(db, sql)
sqlite.dbclose(db)
if r == nil then
return error("Unable to delete data from "..param.table)
else
return result("Data deleted")
end
end
handle.printdoc = function(opt)
local cmd = "lp "
if opt.printer and opt.printer ~= "" then
cmd = cmd .. " -d "..opt.printer
end
if opt.side == 0 then
cmd = cmd.. " -o sides=one-sided"
elseif opt.side == 1 then
cmd = cmd.. " -o sides=two-sided-long-edge"
elseif opt.side == 2 then
cmd = cmd .. " -o sides=two-sided-short-edge"
end
-- orientation landscape
if opt.orientation == 1 then
cmd = cmd.." -o orientation-requested=5"
end
if opt.range == 1 then
cmd = cmd.." -P "..opt.pages
end
cmd = cmd.. " "..vfs.ospath(opt.file)
print(cmd)
os.execute(cmd)
return result("A print job has been posted on server. Check if it successes")
end
if arg.action and handle[arg.action] then
-- check if the database exits
docpath = arg.docpath
dbpath = docpath.."/docify.db"
return handle[arg.action](arg.args)
else
return error("Invalid action parameter")
end

View File

@ -1,25 +0,0 @@
afx-app-window[data-id = "Docify"] .header .label-text
{
font-weight: bold;
}
div[data-id = "preview-container"]
{
overflow: auto;
display: block;
}
canvas[data-id = "preview-canvas"]
{
display: block;
margin:0 auto;
}
afx-app-window[data-id = "DocifyPrintDialog"] i.label-text {
font-weight: bold;
}
afx-app-window[data-id = "DocifyPrintDialog"] input[type="radio"] {
margin: 0;
height: 12px;
margin-left: 10px;
}

File diff suppressed because one or more lines are too long

View File

@ -1,15 +0,0 @@
{
"pkgname": "Docify",
"app":"Docify",
"name":"Docify",
"description":"Docify",
"info":{
"author": "",
"email": ""
},
"version":"0.0.8-b",
"category":"Office",
"iconclass":"bi bi-collection-fill",
"mimes":["none"],
"locale": {}
}

View File

@ -1,34 +0,0 @@
<afx-app-window apptitle="Docify" width="700" height="500" data-id="Docify">
<afx-hbox >
<div data-width="5"></div>
<afx-vbox data-width="150">
<afx-label class="header" iconclass = "fa fa-bars" text="__(Categories)" data-height="22"></afx-label>
<afx-list-view data-id="catview"></afx-list-view>
<div data-height="5"></div>
</afx-vbox>
<afx-resizer data-width="4"></afx-resizer>
<afx-vbox data-width = "300">
<afx-label class="header" iconclass = "fa fa-bars" text="__(Documents)" data-height="22"></afx-label>
<afx-list-view data-id="docview"></afx-list-view>
<afx-hbox data-height="30">
<div data-width="5"></div>
<afx-button data-id="bt-add-doc" data-width = "25" text = "" iconclass = "fa fa-plus-circle"></afx-button>
<afx-button data-id="bt-del-doc" data-width = "25" text = "" iconclass = "fa fa-minus-circle"></afx-button>
<afx-button data-id="bt-edit-doc" data-width = "25" text = "" iconclass = "fa fa-pencil-square-o"></afx-button>
<afx-button data-id="bt-upload-doc" data-width = "25" text = "" iconclass = "bi bi-cloud-upload"></afx-button>
</afx-hbox>
</afx-vbox>
<afx-resizer data-width="4"></afx-resizer>
<afx-vbox>
<div data-id = "preview-container">
<canvas data-id="preview-canvas"></canvas>
</div>
<afx-grid-view data-id="docgrid"></afx-grid-view>
<div style="text-align: right;" data-height="30" >
<afx-button text="__(Open)" data-id="btopen" ></afx-button>
<afx-button text="__(Download)" data-id="btdld" ></afx-button>
<afx-button text="__(Print on server)" data-id="btprint" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>

Binary file not shown.

View File

@ -1,329 +0,0 @@
class OwnerDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "OwnerDialog", OwnerDialog.scheme
main: () ->
super.main()
@oview = @find("ownview")
@oview.buttons = [
{
text: "",
iconclass: "fa fa-plus-circle",
onbtclick: (e) =>
@openDialog("PromptDialog", { title: __("Owner"), label: __("Name")})
.then (d) =>
@parent.exec("insert", { table: "owners", data: { name: d } })
.then (r) =>
return @error r.error if r.error
@owner_refresh()
.catch (e) => @error __("Unable to insert owner: {0}", e.toString()),e
.catch (e) => @error e.toString(),e
},
{
text: "",
iconclass: "fa fa-minus-circle",
onbtclick: (e) =>
item = @oview.selectedItem
return unless item
@ask({ text:__("Do you realy want to delete: `{0}`", item.data.text)})
.then (d) =>
return unless d
@parent.exec("delete", {table:"owners", id: parseInt(item.data.id)})
.then (d) =>
return @error d.error if d.error
@owner_refresh()
.catch (e) =>
@error __("Unable delete category: {0}", e.toString()), e
},
{
text: "",
iconclass: "fa fa-pencil-square-o",
onbtclick: (e) =>
item = @oview.selectedItem
return unless item
@openDialog("PromptDialog", { title: __("Owner"), label: __("Name"), value: item.data.name })
.then (d) =>
@parent.exec("update", { table: "owners", data: { id: parseInt(item.data.id), name: d } })
.then (r) =>
return @error r.error if r.error
@owner_refresh()
.catch (e) => @error __("Unable to update owner: {0}", e.toString()), e
.catch (e) => @error e.toString()
}
]
@owner_refresh()
owner_refresh: () ->
@parent.exec("fetch", "owners")
.then (d) =>
v.text = v.name for v in d.result
@oview.data = d.result
.catch (err) => @error __("Unable to fetch owners: {0}", err.toString()), e
OwnerDialog.scheme = """
<afx-app-window width='200' height='300'>
<afx-vbox>
<afx-list-view data-id="ownview"></afx-list-view>
</afx-vbox>
</afx-app-window>
"""
class DocDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "DocDialog", DocDialog.scheme
main: () ->
super.main()
@flist = @find("file-list")
@dlist = @find("dlist")
@mlist = @find("mlist")
@ylist = @find("ylist")
@olist = @find("olist")
@setting = @parent.setting
@exec = @parent.exec
@preview = @parent.preview
@exec("fetch", "owners")
.then (d) =>
return @error d.error if d.error
v.text = v.name for v in d.result
v.selected = (@data and @data.oid is v.id) for v in d.result
@olist.data = d.result
@olist.selected = 0 if not @olist.selectedItem
.catch (e) =>
@error __("Unable to fetch owner list: {0}", e.toString()), e
@dlist.push {
text:"None",
value: 0
}
selected = 0
for d in [1..31]
@dlist.push {
text:"#{d}",
value: d
}
selected = d if @data and parseInt(@data.day) is d
@dlist.selected = selected
@mlist.push {
text:"None",
value: 0
}
selected = 0
for d in [1..12]
@mlist.push {
text:"#{d}",
value: d
}
selected = d if @data and parseInt(@data.month) is d
@mlist.selected = selected
@ylist.push {
text:"None",
value: 0
}
@ylist.selected = 0
for y in [1960..new Date().getFullYear()]
@ylist.push {
text:"#{y}",
value: y,
selected: @data and parseInt(@data.year) is y
}
@flist.buttons = [
{
text: "",
iconclass: "fa fa-plus-circle",
onbtclick: (e) =>
@openDialog(new FilePreviewDialog())
.then (d) =>
d.text = d.filename
@flist.push d
},
{
text: "",
iconclass: "fa fa-minus-circle",
onbtclick: (e) =>
item = @flist.selectedItem
return unless item
@flist.delete item
}
]
@flist.onlistselect = (e) =>
@parent.preview(e.data.item.data.path, @find("preview-canvas"))
@find("btsave").onbtclick = (e) =>
data = {
name: @find("title").value.trim(),
day: @dlist.selectedItem.data.value,
month: @mlist.selectedItem.data.value,
year: @ylist.selectedItem.data.value,
file: (v.path for v in @flist.data),
note: @find("note").value.trim(),
tags: @find("tag").value.trim(),
oid: parseInt(@olist.selectedItem.data.id)
}
return @notify __("Please enter title") unless data.name and data.title != ""
return @notify __("Please attach files to the entry") unless data.file.length > 0
@handle data if @handle
@quit()
return unless @data
@find("title").value = @data.name
@find("note").value = @data.note
@find("tag").value = @data.tags
file = @data.file.asFileHandle()
file.text = file.filename
@flist.data = [ file ]
# owner
DocDialog.scheme = """
<afx-app-window width='600' height='400'>
<afx-hbox>
<afx-vbox data-width="350">
<afx-hbox data-height="22">
<afx-label text = "__(title)" data-width="50"></afx-label>
<input type="text" data-id="title"></input>
</afx-hbox>
<afx-hbox data-height="22">
<afx-label text = "__(Day)" data-width="50"></afx-label>
<afx-list-view dropdown="true" data-id="dlist"></afx-list-view>
<afx-label text = "__(Month)"data-width="50" ></afx-label>
<afx-list-view dropdown="true" data-id="mlist"></afx-list-view>
<afx-label text = "__(Year)"data-width="50" ></afx-label>
<afx-list-view dropdown="true" data-id="ylist"></afx-list-view>
</afx-hbox>
<afx-label text = "__(Files)" data-height="22"></afx-label>
<afx-list-view data-id="file-list"></afx-list-view>
<afx-label text = "__(Note)" data-height="22"></afx-label>
<textarea data-id="note"></textarea>
<afx-hbox data-height = "27">
<afx-label text = "__(Owner)" data-width="50"></afx-label>
<afx-list-view dropdown="true" data-id="olist"></afx-list-view>
<afx-label text = "__(Tags)" data-width="50"></afx-label>
<input type="text" data-id="tag"></input>
</afx-hbox>
</afx-vbox>
<afx-vbox>
<div data-id = "preview-container">
<canvas data-id="preview-canvas"></canvas>
</div>
<div style="text-align: right;" data-height="30" >
<afx-button text="__(Save)" data-id="btsave" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>
"""
class FilePreviewDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "FilePreviewDialog", FilePreviewDialog.scheme
main: () ->
super.main()
@flist = @find("file-list")
@flist.buttons = [
{
text: "",
iconclass: "fa fa-refresh",
onbtclick: (e) => @refresh()
}
]
@flist.onlistselect = (e) =>
# console.log e.data.item.data
@parent.preview(e.data.item.data.path, @find("preview-canvas"))
@find("btok").onbtclick = (e) =>
item = @flist.selectedItem
return @quit() unless item
@handle(item.data) if @handle
@quit()
@refresh()
refresh: () ->
"#{@parent.setting.docpath}/unclassified".asFileHandle().read()
.then (d) =>
return @error d.error if d.error
v.text = v.filename for v in d.result
@flist.data = (v for v in d.result when v.filename[0] isnt '.')
.catch (e) =>
@error __("Unable to fetch unclassified file list: {0}", e.toString()), e
FilePreviewDialog.scheme = """
<afx-app-window width='400' height='400' apptitle = "__(Document preview)">
<afx-hbox>
<afx-vbox data-width="150">
<afx-label text = "__(Files)" data-height="22"></afx-label>
<afx-list-view data-id="file-list"></afx-list-view>
</afx-vbox>
<afx-vbox>
<div data-id = "preview-container">
<canvas data-id="preview-canvas"></canvas>
</div>
<div style="text-align: right;" data-height="30" >
<afx-button text="__(Ok)" data-id="btok" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>
"""
class PrintDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "PrintDialog", PrintDialog.scheme
main: () ->
super.main()
@find("printerName").value = @parent.setting.printer
@find("btnprint").onbtclick = (e) =>
data = {}
data.range = parseInt($('input[name=range]:checked', @scheme).val())
data.pages = @find("txtPageRange").value
data.printer = @find("printerName").value
data.orientation = parseInt($('input[name=orientation]:checked', @scheme).val())
data.side = parseInt($('input[name=side]:checked', @scheme).val())
@handle data if @handle
@quit()
PrintDialog.scheme = """
<afx-app-window width='300' height='300' data-id="DocifyPrintDialog" apptitle = "__(Print)">
<afx-vbox>
<afx-label text = "__(Printer name)" data-height="22"></afx-label>
<input type="text" data-id="printerName" data-height="25"></input>
<afx-label text = "__(Range)" data-height="22"></afx-label>
<div>
<input type="radio" name="range" value="0" checked ></input>
<label for="0">All</label><br>
<input type="radio" name="range" value="1" ></input>
<label for="1">Pages: </label>
<input type="text" data-id="txtPageRange" ></input>
</div>
<afx-label text = "__(Orientation)" data-height="22"></afx-label>
<div>
<input type="radio" name="orientation" value="0" checked ></input>
<label for="0">Portrait</label><br>
<input type="radio" name="orientation" value="1" ></input>
<label for="1">Landscape</label>
</div>
<afx-label text = "__(Side)" data-height="22"></afx-label>
<div>
<input type="radio" name="side" value="0" ></input>
<label for="0">One side</label><br>
<input type="radio" name="side" value="1" checked ></input>
<label for="1">Double side long edge</label><br>
<input type="radio" name="side" value="2" ></input>
<label for="2">Double side short edge</label>
</div>
<div data-height="30" style="text-align:right;">
<afx-button text="__(Print)" style="margin-right:5px;" data-id="btnprint"></afx-button>
</div>
</afx-vbox>
</afx-app-window>
"""

View File

@ -1,290 +0,0 @@
class Docify extends this.OS.application.BaseApplication
constructor: ( args ) ->
super "Docify", args
main: () ->
@setting.printer = "" unless @setting.printer
@catview = @find "catview"
@docview = @find "docview"
@docpreview = @find "preview-canvas"
@docgrid = @find "docgrid"
@docgrid.header = [
{ text: "", width: 100 },
{ text: "" },
]
@find("btdld").onbtclick = (e) =>
item = @docview.selectedItem
return unless item
item.data.file.asFileHandle()
.download()
.catch (e) => @error __("Unable to download: {}", e.toString()), e
@find("btopen").onbtclick = (e) =>
item = @docview.selectedItem
return unless item
item.data.file.asFileHandle().meta()
.then (m) =>
return @error m.error if m.error
@_gui.openWith m.result
.catch (e) => @error e.toString(), e
@find("btprint").onbtclick = (e) =>
item = @docview.selectedItem
return unless item
@openDialog new PrintDialog(), {}
.then (d) =>
return unless d
d.file = item.data.file
@exec("printdoc", d)
.then (r) =>
return @error r.error if r.error
@notify r.result
.catch (e) => @error __("Unable to insert category: {0}", e.toString()), e
@catview.buttons = [
{
text: "",
iconclass: "fa fa-plus-circle",
onbtclick: (e) =>
@openDialog("PromptDialog", { title: __("Category"), label: __("Name")})
.then (d) =>
@exec("insert", { table: "categories", data: { name: d } })
.then (r) =>
return @error r.error if r.error
@cat_refresh()
.catch (e) => @error __("Unable to insert category: {0}", e.toString()), e
.catch (e) => @error e.toString(), e
},
{
text: "",
iconclass: "fa fa-minus-circle",
onbtclick: (e) =>
item = @catview.selectedItem
return unless item
@ask({ text:__("Do you realy want to delete: `{0}`", item.data.text)})
.then (d) =>
return unless d
@exec("delete", {table:"categories", id: parseInt(item.data.id)})
.then (d) =>
return @error d.error if d.error
@cat_refresh()
.catch (e) =>
@error __("Unable delete category: {0}", e.toString()), e
},
{
text: "",
iconclass: "fa fa-pencil-square-o",
onbtclick: (e) =>
item = @catview.selectedItem
return unless item
@openDialog("PromptDialog", { title: __("Category"), label: __("Name"), value: item.data.name })
.then (d) =>
@exec("update", { table: "categories", data: { id: parseInt(item.data.id), name: d } })
.then (r) =>
return @error r.error if r.error
@cat_refresh()
.catch (e) => @error __("Unable to update category: {0}", e.toString()), e
.catch (e) => @error e.toString(), e
}
]
@docview.onlistselect = (e) =>
@clear_preview()
item = e.data.item
return unless item
@exec("get_doc", item.data.id)
.then (d) =>
return @error d.error if d.error
@preview d.result.file, @docpreview
rows = []
d.result.size = (d.result.fileinfo.size / 1024.0).toFixed(2) + " Kb" if d.result.fileinfo
map = {
ctime: "Created on",
mtime: "Modified on",
note: "Note",
tags: "Tags",
name: "Title",
owner: "Owner",
edate: "Effective date",
file: "File",
size: "Size"
}
d.result.edate = "#{d.result.day}/#{d.result.month}/#{d.result.year}"
for key, value of d.result
field = map[key]
rows.push [{text: field}, {text: value}] if field
@docgrid.rows = rows
.catch (e) => @error e.toString(), e
@catview.onlistselect = (e) =>
@clear_preview()
item = e.data.item
return unless item
@update_doclist(item.data.id)
@find("bt-add-doc").onbtclick = (e) =>
catiem = @catview.selectedItem
return @notify __("Please select a category") unless catiem
@openDialog(new DocDialog())
.then (data) =>
data.cid = parseInt(catiem.data.id)
@exec("insertdoc", data)
.then (d) =>
return @error d.error if d.error
@notify d.result if d.result
@update_doclist(catiem.data.id)
@clear_preview()
.catch (e) => @error e.toString(), e
@find("bt-del-doc").onbtclick = (e) =>
item = @docview.selectedItem
return unless item
@ask({ text: __("Do you really want to delete: `{0}`", item.data.name) })
.then (d) =>
return unless d
@exec("deletedoc", {id: item.data.id, file: item.data.file})
.then (r) =>
return @error r.error if r.error
@notify r.result
@update_doclist(item.data.cid)
@clear_preview()
.catch (e) =>
@error e.toString(), e
@find("bt-upload-doc").onbtclick = (e) =>
"#{@setting.docpath}/unclassified".asFileHandle().upload()
.then (r) =>
@notify __("File uploaded")
.catch (e) =>
@error e.toString(), e
@find("bt-edit-doc").onbtclick = (e) =>
item = @docview.selectedItem
catiem = @catview.selectedItem
return unless item
@openDialog(new DocDialog(), item.data)
.then (data) =>
data.cid = parseInt(catiem.data.id)
data.id = item.data.id
@exec("updatedoc", {
data:data,
rm: if not data.file.includes(item.data.file) then item.data.file else false
})
.then (d) =>
return @error d.error if d.error
@notify d.result if d.result
@update_doclist(catiem.data.id)
@clear_preview()
.catch (e) => @error e.toString(), e
@initialize()
update_doclist: (cid) ->
@exec("select",{table: "docs", cond:"cid = #{cid} ORDER BY year DESC, month DESC, day DESC"})
.then (d) =>
return @error d.error if d.error
v.text = v.name for v in d.result
@docview.data = d.result
.catch (e) =>
@error e.toString(), e
clear_preview: () ->
@docpreview.getContext('2d').clearRect(0,0,@docpreview.width,@docpreview.height)
@docgrid.rows = []
preview: (path, canvas) ->
@exec("preview", path)
.then (d) =>
return @error d.error if d.error
file = d.result.asFileHandle()
file.read("binary")
.then (d) =>
img = new Image()
#($ me.view).append img
img.onload = () =>
context = canvas.getContext '2d'
canvas.height = img.height
canvas.width = img.width
#console.log canvas.width, canvas.height
context.drawImage img, 0, 0
blob = new Blob [d], { type: file.info.mime }
img.src = URL.createObjectURL blob
.catch (e) => @error e.toString(), e
.catch (e) =>
@error e.toString(), e
cat_refresh: () ->
@docview.data = []
@clear_preview()
@exec("fetch", "categories")
.then (d) =>
v.text = v.name for v in d.result
@catview.data = d.result
.catch (err) => @error __("Unable to fetch categories: {0}", err.toString()), err
initialize: () ->
# Check if we have configured docpath
if @setting.docpath
# check data base
@initdb()
else
# ask user to choose a docpath
@openDialog "FileDialog", { title:__("Please select a doc path"), mimes: ['dir'] }
.then (d) =>
@setting.docpath = d.file.path
@_api.setting()
@initdb()
.catch (msg) => @error msg.toString(), msg
exec: (action, args) ->
cmd =
path: "#{@path()}/api.lua",
parameters:
action: action,
docpath: @setting.docpath,
args: args
return @call(cmd)
initdb: () ->
return @error __("No configured docpath") unless @setting.docpath
# fetch the categories from the database
@exec("init")
.then (d) =>
return @error d.error if d.error
@notify d.result
# load categories
@cat_refresh()
.catch (e) =>
@error __("Unable to init database: {0}", e.toString()), e
menu: () ->
[
{
text: "__(Options)",
nodes: [
{ text: "__(Owners)", id:"owners"},
{ text: "__(Preview)", id:"preview"},
{ text: "__(Change doc path)", id:"setdocp"},
{ text: "__(Set default printer)", id:"setprinter"}
],
onchildselect: (e) => @fileMenuHandle e.data.item.data.id
}
]
fileMenuHandle:(id) ->
switch id
when "owners"
@openDialog new OwnerDialog(), { title: __("Owners")}
when "preview"
@openDialog(new FilePreviewDialog())
.then (d) =>
@notify d.path
when "setdocp"
@setting.docpath = undefined
@initialize()
when "setprinter"
@openDialog "PromptDialog", {title: __("Default Printer"), label: __("Enter printer name")}
.then (n) =>
@setting.printer = n
this.OS.register "Docify", Docify

View File

@ -1,24 +0,0 @@
afx-app-window[data-id = "Docify"] .header .label-text
{
font-weight: bold;
}
div[data-id = "preview-container"]
{
overflow: auto;
display: block;
}
canvas[data-id = "preview-canvas"]
{
display: block;
margin:0 auto;
}
afx-app-window[data-id = "DocifyPrintDialog"] i.label-text {
font-weight: bold;
}
afx-app-window[data-id = "DocifyPrintDialog"] input[type="radio"] {
margin: 0;
height: 12px;
margin-left: 10px;
}

View File

@ -1,15 +0,0 @@
{
"pkgname": "Docify",
"app":"Docify",
"name":"Docify",
"description":"Docify",
"info":{
"author": "",
"email": ""
},
"version":"0.0.8-b",
"category":"Office",
"iconclass":"bi bi-collection-fill",
"mimes":["none"],
"locale": {}
}

View File

@ -1,7 +0,0 @@
{
"name": "Docify",
"css": ["css/main.css"],
"javascripts": [],
"coffees": ["coffees/dialogs.coffee", "coffees/main.coffee"],
"copies": ["assets/scheme.html", "api/api.lua", "package.json", "README.md"]
}

View File

@ -6,7 +6,6 @@ if not args then
args = REQUEST args = REQUEST
end end
local vfs = require("vfs") local vfs = require("vfs")
local DLCMD="wget --no-check-certificate -O"
local handle = {} local handle = {}
--local logger = Logger:new{ levels = {INFO = true, ERROR = true, DEBUG = false}} --local logger = Logger:new{ levels = {INFO = true, ERROR = true, DEBUG = false}}
local result = function(data) local result = function(data)
@ -17,12 +16,22 @@ local error = function(msg)
return {error = msg, result = false} return {error = msg, result = false}
end end
local fetch = function(url)
local https = require('ssl.https')
local body, code, headers = https.request(url)
if code~=200 then
LOG_ERROR("Error: ".. (code or '') )
return nil
end
return body
end
handle.token = function(data) handle.token = function(data)
local file = vfs.ospath(data.file) local file = vfs.ospath(data.file)
local stat = ulib.file_stat(file) local stat = ulib.file_stat(file)
local ret = { local ret = {
sid = "access_token="..SESSION.sessionid, sid = "access_token="..SESSION.sessionid,
key = std.sha1(file..":"..stat.mtime) key = enc.sha1(file..":"..stat.mtime)
} }
return result(ret) return result(ret)
end end
@ -43,14 +52,9 @@ handle.duplicate = function(data)
end end
handle.discover = function(data) handle.discover = function(data)
local tmpfile = "/tmp/libreoffice_discover.xml" content = fetch(url)
local cmd = DLCMD.." "..tmpfile..' '..data.uri
os.execute(cmd)
-- move file to correct position -- move file to correct position
if ulib.exists(tmpfile) then if content then
local f = assert(io.open(tmpfile, "rb"))
local content = f:read("*all")
f:close()
return result(content) return result(content)
else else
return error("Unable to discover data") return error("Unable to discover data")
@ -74,7 +78,7 @@ handle.file = function(data)
elseif REQUEST.method == "POST" then elseif REQUEST.method == "POST" then
--local clen = tonumber(HEADER['Content-Length']) --local clen = tonumber(HEADER['Content-Length'])
local barr = REQUEST["application/octet-stream"] local barr = REQUEST["application/octet-stream"]
bytes.write(barr, path) barr:fileout(path)
return result(true) return result(true)
else else
return error("Unknown request method") return error("Unknown request method")

View File

@ -1,7 +1,6 @@
local args=... local args=...
local vfs = require("vfs") local vfs = require("vfs")
local DLCMD="wget --no-check-certificate -O"
if not args then if not args then
args = REQUEST args = REQUEST
end end
@ -20,6 +19,25 @@ local error = function(data)
} }
end end
local download_file = function(src, dest)
local https = require('ssl.https')
local ltn12 = require("ltn12")
local file = io.open(dest, "w")
if not file then
LOG_ERROR("Unable to open file %s to write", dest)
return false
end
local body, code, headers = https.request{
url = src,
sink = ltn12.sink.file(file)
}
if code~=200 then
LOG_ERROR("Error: ".. (code or '') )
return false
end
return true
end
local handle = {} local handle = {}
handle.token = function(data) handle.token = function(data)
@ -27,17 +45,17 @@ handle.token = function(data)
local stat = ulib.file_stat(file) local stat = ulib.file_stat(file)
local ret = { local ret = {
sid = "sessionid="..SESSION.sessionid, sid = "sessionid="..SESSION.sessionid,
key = std.sha1(file..":"..stat.mtime) key = enc.sha1(file..":"..stat.mtime)
} }
return result(ret) return result(ret)
end end
handle.history = function(data) handle.history = function(data)
local file = vfs.ospath(data.file) local file = vfs.ospath(data.file)
local history_file = vfs.ospath("home://.office/"..std.sha1(file).."/history.json") local history_file = vfs.ospath("home://.office/"..enc.sha1(file).."/history.json")
if(ulib.exists(history_file)) then if(ulib.exists(history_file)) then
local obj = JSON.decodeFile(history_file) local obj = JSON.decodeFile(history_file)
obj.hash = std.sha1(file) obj.hash = enc.sha1(file)
return result(obj) return result(obj)
else else
return error("No history found") return error("No history found")
@ -64,7 +82,7 @@ end
handle.restore = function(data) handle.restore = function(data)
local version = data.version local version = data.version
local file = vfs.ospath(data.file) local file = vfs.ospath(data.file)
local basepath = vfs.ospath("home://.office/"..std.sha1(file)) local basepath = vfs.ospath("home://.office/"..enc.sha1(file))
if ulib.exists(basepath.."/history.json") then if ulib.exists(basepath.."/history.json") then
local history = JSON.decodeFile(basepath.."/history.json") local history = JSON.decodeFile(basepath.."/history.json")
local obj = handle.clean_up_version(basepath, history,version) local obj = handle.clean_up_version(basepath, history,version)
@ -95,22 +113,14 @@ handle.restore = function(data)
end end
handle.duplicate = function(data) handle.duplicate = function(data)
local file = vfs.ospath(data.as) local file = vfs.ospath(data.as)
local tmpfile = "/tmp/"..std.sha1(file) download_file(data.remote, file)
local cmd = DLCMD.." "..tmpfile..' "'..data.remote..'"' if not ulib.exists(file) then
os.execute(cmd)
-- move file to correct position
if ulib.exists(tmpfile) then
cmd = "mv "..tmpfile.." "..file
os.execute(cmd)
print("File "..file.." is duplicated with remote")
else
return error("Unable to duplicate file") return error("Unable to duplicate file")
end end
return result("File duplicated") return result("File duplicated")
end end
handle.save = function() handle.save = function()
--print(JSON.encode(REQUEST))
if not REQUEST.json then if not REQUEST.json then
return error("Invalid request") return error("Invalid request")
end end
@ -123,31 +133,31 @@ handle.save = function()
end end
local file = vfs.ospath(REQUEST.file) local file = vfs.ospath(REQUEST.file)
if data.status == 2 then if data.status == 2 then
local tmpfile = "/tmp/"..std.sha1(file) local tmpfile = "/tmp/"..enc.sha1(file)
local cmd = DLCMD.." "..tmpfile..' "'..data.url..'"' download_file(data.url, tmpfile)
os.execute(cmd)
-- move file to correct position -- move file to correct position
if ulib.exists(tmpfile) then if ulib.exists(tmpfile) then
LOG_INFO("Remote file saved to %s", tmpfile)
-- back up the file version -- back up the file version
local history_dir = "home://.office" local history_dir = "home://.office"
vfs.mkdir(history_dir) vfs.mkdir(history_dir)
history_dir = history_dir.."/"..std.sha1(file) history_dir = history_dir.."/"..enc.sha1(file)
vfs.mkdir(history_dir) vfs.mkdir(history_dir)
history_dir = vfs.ospath(history_dir) history_dir = vfs.ospath(history_dir)
-- backup old version -- backup old version
cmd = 'cp "'..file..'" "'..history_dir.."/"..data.key..'"' ulib.send_file(file,history_dir.."/"..data.key)
os.execute(cmd) LOG_INFO("Backup file saved to %s", history_dir.."/"..data.key)
-- create new version -- create new version
local old_stat = ulib.file_stat(file) local old_stat = ulib.file_stat(file)
cmd = 'mv "'..tmpfile..'" "'..file..'"' if not ulib.move(tmpfile, file) then
os.execute(cmd) ulib.send_file(tmpfile, file)
end
-- get the new key -- get the new key
local stat = ulib.file_stat(file) local stat = ulib.file_stat(file)
local new_key = std.sha1(file..":"..stat.mtime) local new_key = enc.sha1(file..":"..stat.mtime)
-- save changes -- save changes
if(data.changesurl) then if(data.changesurl) then
cmd = DLCMD.." "..history_dir.."/"..new_key..'.zip "'..data.changesurl..'"' download_file(data.changesurl, history_dir.."/"..new_key..'.zip')
os.execute(cmd)
end end
-- now save version object -- now save version object
local history_file = history_dir.."/history.json" local history_file = history_dir.."/history.json"
@ -177,7 +187,7 @@ handle.save = function()
else else
return error("Cannot save history") return error("Cannot save history")
end end
print("File "..file.." sync with remote") LOG_INFO("File "..file.." sync with remote")
else else
return error("Unable to download") return error("Unable to download")
end end

View File

@ -89,16 +89,6 @@
"dependencies": [], "dependencies": [],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/Archive/build/release/Archive.zip" "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/Archive/build/release/Archive.zip"
}, },
{
"pkgname": "Blogger",
"name": "Blogging application",
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/Blogger/README.md",
"category": "Internet",
"author": "Xuan Sang LE",
"version": "0.2.7-a",
"dependencies": ["SimpleMDE@1.11.2-r","Katex@0.11.1-r"],"mimes":["none"],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/Blogger/build/release/Blogger.zip"
},
{ {
"pkgname": "Booklet", "pkgname": "Booklet",
"name": "Booklet", "name": "Booklet",
@ -129,16 +119,6 @@
"dependencies": ["ACECore@1.4.12-r"],"mimes":["text/.*","[^/]*/json.*","[^/]*/.*ml","[^/]*/javascript","dir"], "dependencies": ["ACECore@1.4.12-r"],"mimes":["text/.*","[^/]*/json.*","[^/]*/.*ml","[^/]*/javascript","dir"],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/CodePad/build/release/CodePad.zip" "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/CodePad/build/release/CodePad.zip"
}, },
{
"pkgname": "DBDecoder",
"name": "DBDecoder",
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/DBDecoder/README.md",
"category": "Other",
"author": "",
"version": "0.0.2-a",
"dependencies": [],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/DBDecoder/build/release/DBDecoder.zip"
},
{ {
"pkgname": "DiffEditor", "pkgname": "DiffEditor",
"name": "Diff Editor", "name": "Diff Editor",
@ -149,16 +129,6 @@
"dependencies": ["AceDiff@3.0.3-r"], "dependencies": ["AceDiff@3.0.3-r"],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/DiffEditor/build/release/DiffEditor.zip" "download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/DiffEditor/build/release/DiffEditor.zip"
}, },
{
"pkgname": "Docify",
"name": "Docify",
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/Docify/README.md",
"category": "Office",
"author": "",
"version": "0.0.8-b",
"dependencies": [],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/master/Docify/build/release/Docify.zip"
},
{ {
"pkgname": "Dockman", "pkgname": "Dockman",
"name": "Remote Docker Manager", "name": "Remote Docker Manager",