Docify: Use libsqlite to handle database in Docify

This commit is contained in:
DanyLE 2023-03-27 20:38:17 +02:00
parent 6354c48680
commit 7292d2ef21
18 changed files with 1098 additions and 1273 deletions

View File

@ -87,7 +87,7 @@ namespace OS {
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>\s
</afx-app-window>\
`;
// This dialog is use for cv section editing

View File

@ -2,6 +2,7 @@
Simple PDF document manager
## Change logs
- v0.1.0-b: use libsqlite for database handling
- v0.0.9-b: Adapt to support AntOS 2.0.x
- v0.0.8-b: Allow upload files directly from the app
- v0.0.7-a: Change category and icon

View File

@ -1,12 +1,10 @@
local arg = ...
ulib = require("ulib")
sqlite = modules.sqlite()
vfs = require("vfs")
local handle = {}
local docpath = nil
local dbpath = nil
local result = function(data)
return {
@ -31,7 +29,7 @@ local mkdirp =function(p)
return true, nil
end
local merge_files = function(data)
handle.merge_files = function(data)
local firstfile = data.file[1]
local fpath = docpath.."/"..data.cid
local r, e = mkdirp(fpath)
@ -57,14 +55,14 @@ local merge_files = function(data)
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"
local thumb = docpath.."/cache/"..enc.sha1(firstfile:gsub(docpath, ""))..".png"
local desthumb = docpath.."/cache/"..enc.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"
thumb = docpath.."/cache/"..enc.sha1(v:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
@ -76,166 +74,15 @@ local merge_files = function(data)
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"
local name = enc.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)
LOG_ERROR(cmd)
os.execute(cmd)
end
@ -248,57 +95,12 @@ handle.preview = function(path)
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)
local newfile = docpath.."/unclassified/"..utils.basename(param.file)
vfs.move(param.file, newfile)
-- delete thumb file
local thumb = docpath.."/cache/"..std.sha1(param.file:gsub(docpath,""))..".png"
local thumb = docpath.."/cache/"..enc.sha1(param.file:gsub(docpath,""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
@ -306,20 +108,20 @@ handle.deletedoc = function(param)
end
handle.updatedoc = function(param)
local r = merge_files(param.data)
local r = handle.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 newfile = docpath.."/unclassified/"..utils.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"
local thumb = docpath.."/cache/"..enc.sha1(param.rm:gsub(docpath,""))..".png"
local newwthumb = docpath.."/cache/"..enc.sha1(newfile:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.move(thumb, newwthumb)
end
@ -327,107 +129,16 @@ handle.updatedoc = function(param)
param.data.file = r.result
print(r.result)
param.data.mtime = os.time(os.date("!*t"))
return handle.update({
table = "docs",
data = param.data
})
return result(param.data)
--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

View File

@ -25,7 +25,6 @@
<div style="text-align: right;" data-height="35" >
<afx-button text="" iconclass="bi bi-arrow-up-right-square" data-id="btopen" ></afx-button>
<afx-button text="" iconclass="bi bi-cloud-arrow-down" data-id="btdld" ></afx-button>
<afx-button text="" iconclass = "bi bi-printer" data-id="btprint" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>

View File

@ -13,17 +13,35 @@
}
]
},
"coffee": {
"locale": {
"require": ["locale"],
"jobs": [
{
"name":"locale-gen",
"data": {
"src": "",
"exclude": ["build/", "api/", "css/", "coffees/"],
"locale": "en_GB",
"dest": "package.json"
}
}
]
},
"ts": {
"require": [
"coffee"
"ts"
],
"jobs": [
{
"name": "coffee-compile",
"name": "ts-import",
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
},
{
"name": "ts-compile",
"data": {
"src": [
"coffees/dialogs.coffee",
"coffees/main.coffee"
"ts/dialogs.ts",
"ts/main.ts"
],
"dest": "build/debug/main.js"
}
@ -65,7 +83,7 @@
],
"depend": [
"init",
"coffee",
"ts",
"uglify",
"copy"
],
@ -78,6 +96,14 @@
}
}
]
},
"debug": {
"depend": [
"init",
"ts",
"copy"
]
}
}
}

View File

@ -2,6 +2,7 @@
Simple PDF document manager
## Change logs
- v0.1.0-b: use libsqlite for database handling
- v0.0.9-b: Adapt to support AntOS 2.0.x
- v0.0.8-b: Allow upload files directly from the app
- v0.0.7-a: Change category and icon

View File

@ -1,12 +1,10 @@
local arg = ...
ulib = require("ulib")
sqlite = modules.sqlite()
vfs = require("vfs")
local handle = {}
local docpath = nil
local dbpath = nil
local result = function(data)
return {
@ -31,7 +29,7 @@ local mkdirp =function(p)
return true, nil
end
local merge_files = function(data)
handle.merge_files = function(data)
local firstfile = data.file[1]
local fpath = docpath.."/"..data.cid
local r, e = mkdirp(fpath)
@ -57,14 +55,14 @@ local merge_files = function(data)
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"
local thumb = docpath.."/cache/"..enc.sha1(firstfile:gsub(docpath, ""))..".png"
local desthumb = docpath.."/cache/"..enc.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"
thumb = docpath.."/cache/"..enc.sha1(v:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
@ -76,166 +74,15 @@ local merge_files = function(data)
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"
local name = enc.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)
LOG_ERROR(cmd)
os.execute(cmd)
end
@ -248,57 +95,12 @@ handle.preview = function(path)
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)
local newfile = docpath.."/unclassified/"..utils.basename(param.file)
vfs.move(param.file, newfile)
-- delete thumb file
local thumb = docpath.."/cache/"..std.sha1(param.file:gsub(docpath,""))..".png"
local thumb = docpath.."/cache/"..enc.sha1(param.file:gsub(docpath,""))..".png"
if vfs.exists(thumb) then
vfs.delete(thumb)
end
@ -306,20 +108,20 @@ handle.deletedoc = function(param)
end
handle.updatedoc = function(param)
local r = merge_files(param.data)
local r = handle.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 newfile = docpath.."/unclassified/"..utils.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"
local thumb = docpath.."/cache/"..enc.sha1(param.rm:gsub(docpath,""))..".png"
local newwthumb = docpath.."/cache/"..enc.sha1(newfile:gsub(docpath, ""))..".png"
if vfs.exists(thumb) then
vfs.move(thumb, newwthumb)
end
@ -327,107 +129,16 @@ handle.updatedoc = function(param)
param.data.file = r.result
print(r.result)
param.data.mtime = os.time(os.date("!*t"))
return handle.update({
table = "docs",
data = param.data
})
return result(param.data)
--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

File diff suppressed because one or more lines are too long

View File

@ -2,14 +2,67 @@
"pkgname": "Docify",
"app": "Docify",
"name": "Docify",
"description":"Docify",
"description": "Simple document manager",
"info": {
"author": "",
"email": ""
"author": "Dany LE",
"email": "mrsang@iohub.dev"
},
"version":"0.0.9-b",
"version": "0.1.0-b",
"category": "Office",
"iconclass": "bi bi-collection-fill",
"mimes":["none"],
"locale": {}
"mimes": [
"none"
],
"dependencies": [
"SQLiteDB@0.1.0-a"
],
"locale": {},
"locales": {
"en_GB": {
"title": "title",
"Day": "Day",
"Month": "Month",
"Year": "Year",
"Files": "Files",
"Note": "Note",
"Owner": "Owner",
"Tags": "Tags",
"Save": "Save",
"Document preview": "Document preview",
"Ok": "Ok",
"Unable to get owner data handle": "Unable to get owner data handle",
"Name": "Name",
"Do you realy want to delete: `{0}`": "Do you realy want to delete: `{0}`",
"Unable to fetch owner list: {0}": "Unable to fetch owner list: {0}",
"Please enter title": "Please enter title",
"Please attach files to the entry": "Please attach files to the entry",
"Unable to fetch unclassified file list: {0}": "Unable to fetch unclassified file list: {0}",
"Options": "Options",
"Owners": "Owners",
"Preview": "Preview",
"Change doc path": "Change doc path",
"No configured docpath": "No configured docpath",
"Unable to init database file: {0}": "Unable to init database file: {0}",
"Unable to download: {0}": "Unable to download: {0}",
"Unable to open file: {0}": "Unable to open file: {0}",
"Category": "Category",
"Unable to insert category: {0}": "Unable to insert category: {0}",
"Unable delete category: {0}": "Unable delete category: {0}",
"Unable to update category: {0}": "Unable to update category: {0}",
"Unable to fetch document detail: {0}": "Unable to fetch document detail: {0}",
"Please select a category": "Please select a category",
"Unable to add document: {0}": "Unable to add document: {0}",
"Do you really want to delete: `{0}`": "Do you really want to delete: `{0}`",
"Unable to delete document: {0}": "Unable to delete document: {0}",
"File uploaded": "File uploaded",
"Unable to upload document: {0}": "Unable to upload document: {0}",
"Unable to edit document metadata: {0}": "Unable to edit document metadata: {0}",
"Unable to update document list: {0}": "Unable to update document list: {0}",
"Unable to generate document thumbnail: {0}": "Unable to generate document thumbnail: {0}",
"Please select a doc path": "Please select a doc path",
"Error initialize database: {0}": "Error initialize database: {0}",
"Categories": "Categories",
"Documents": "Documents"
}
}
}

View File

@ -25,7 +25,6 @@
<div style="text-align: right;" data-height="35" >
<afx-button text="" iconclass="bi bi-arrow-up-right-square" data-id="btopen" ></afx-button>
<afx-button text="" iconclass="bi bi-cloud-arrow-down" data-id="btdld" ></afx-button>
<afx-button text="" iconclass = "bi bi-printer" data-id="btprint" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>

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="30">
<afx-label text = "__(title)" data-width="50"></afx-label>
<input type="text" data-id="title"></input>
</afx-hbox>
<afx-hbox data-height="30">
<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 = "30">
<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="35" >
<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="25"></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="35" >
<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="25"></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="25"></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="35" 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

@ -2,14 +2,67 @@
"pkgname": "Docify",
"app": "Docify",
"name": "Docify",
"description":"Docify",
"description": "Simple document manager",
"info": {
"author": "",
"email": ""
"author": "Dany LE",
"email": "mrsang@iohub.dev"
},
"version":"0.0.9-b",
"version": "0.1.0-b",
"category": "Office",
"iconclass": "bi bi-collection-fill",
"mimes":["none"],
"locale": {}
"mimes": [
"none"
],
"dependencies": [
"SQLiteDB@0.1.0-a"
],
"locale": {},
"locales": {
"en_GB": {
"title": "title",
"Day": "Day",
"Month": "Month",
"Year": "Year",
"Files": "Files",
"Note": "Note",
"Owner": "Owner",
"Tags": "Tags",
"Save": "Save",
"Document preview": "Document preview",
"Ok": "Ok",
"Unable to get owner data handle": "Unable to get owner data handle",
"Name": "Name",
"Do you realy want to delete: `{0}`": "Do you realy want to delete: `{0}`",
"Unable to fetch owner list: {0}": "Unable to fetch owner list: {0}",
"Please enter title": "Please enter title",
"Please attach files to the entry": "Please attach files to the entry",
"Unable to fetch unclassified file list: {0}": "Unable to fetch unclassified file list: {0}",
"Options": "Options",
"Owners": "Owners",
"Preview": "Preview",
"Change doc path": "Change doc path",
"No configured docpath": "No configured docpath",
"Unable to init database file: {0}": "Unable to init database file: {0}",
"Unable to download: {0}": "Unable to download: {0}",
"Unable to open file: {0}": "Unable to open file: {0}",
"Category": "Category",
"Unable to insert category: {0}": "Unable to insert category: {0}",
"Unable delete category: {0}": "Unable delete category: {0}",
"Unable to update category: {0}": "Unable to update category: {0}",
"Unable to fetch document detail: {0}": "Unable to fetch document detail: {0}",
"Please select a category": "Please select a category",
"Unable to add document: {0}": "Unable to add document: {0}",
"Do you really want to delete: `{0}`": "Do you really want to delete: `{0}`",
"Unable to delete document: {0}": "Unable to delete document: {0}",
"File uploaded": "File uploaded",
"Unable to upload document: {0}": "Unable to upload document: {0}",
"Unable to edit document metadata: {0}": "Unable to edit document metadata: {0}",
"Unable to update document list: {0}": "Unable to update document list: {0}",
"Unable to generate document thumbnail: {0}": "Unable to generate document thumbnail: {0}",
"Please select a doc path": "Please select a doc path",
"Error initialize database: {0}": "Error initialize database: {0}",
"Categories": "Categories",
"Documents": "Documents"
}
}
}

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"]
}

354
Docify/ts/dialogs.ts Normal file
View File

@ -0,0 +1,354 @@
namespace OS {
export namespace application {
export namespace docify {
export class OwnerDialog extends OS.GUI.BasicDialog {
private oview: GUI.tag.ListViewTag;
constructor() {
super("OwnerDialog", OwnerDialog.scheme);
}
main() {
super.main();
this.oview = this.find("ownview") as GUI.tag.ListViewTag;
if(!this.data.dbhandle)
{
throw new Error(__("Unable to get owner data handle").__());
}
this.oview.buttons = [
{
text: "",
iconclass: "fa fa-plus-circle",
onbtclick: async (e: any) => {
try
{
const d = await this.openDialog("PromptDialog", {
title: __("Owner"),
label: __("Name")
});
this.data.dbhandle.cache = { name: d };
const r = await this.data.dbhandle.write(undefined);
if(r.error)
{
throw new Error(r.error);
}
await this.owner_refresh();
}
catch(e)
{
this.error(e.toString(), e);
}
}
},
{
text: "",
iconclass: "fa fa-minus-circle",
onbtclick: async (e: any) => {
try{
const item = this.oview.selectedItem;
if (!item) { return; }
let d = await this.ask({ text:__("Do you realy want to delete: `{0}`", item.data.text)});
if (!d) { return; }
const handle = item.data.$vfs as API.VFS.BaseFileHandle;
let r = await handle.remove();
if(r.error)
{
throw new Error(r.error.toString());
}
await this.owner_refresh();
}
catch(e)
{
this.error(e.toString(), e);
}
}
},
{
text: "",
iconclass: "fa fa-pencil-square-o",
onbtclick: async (e: any) => {
try
{
const item = this.oview.selectedItem;
if (!item) { return; }
const d = await this.openDialog("PromptDialog", {
title: __("Owner"),
label: __("Name"),
value: item.data.name
});
const handle = item.data.$vfs as API.VFS.BaseFileHandle;
handle.cache = { name: d };
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error.toString());
}
await this.owner_refresh();
}
catch(e)
{
this.error(e.toString(), e);
}
}
}
];
return this.owner_refresh();
}
private async owner_refresh() {
const d = await this.data.dbhandle.read();
for (let v of d) { v.text = v.name; }
this.oview.data = d;
}
}
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>\
`;
export class DocDialog extends OS.GUI.BasicDialog {
private flist: GUI.tag.ListViewTag;
private dlist: GUI.tag.ListViewTag;
private mlist: GUI.tag.ListViewTag;
private ylist: GUI.tag.ListViewTag;
private olist: GUI.tag.ListViewTag;
constructor() {
super("DocDialog", DocDialog.scheme);
}
main() {
let d: number;
super.main();
this.flist = this.find("file-list") as GUI.tag.ListViewTag;
this.dlist = this.find("dlist") as GUI.tag.ListViewTag;
this.mlist = this.find("mlist") as GUI.tag.ListViewTag;
this.ylist = this.find("ylist") as GUI.tag.ListViewTag;
this.olist = this.find("olist") as GUI.tag.ListViewTag;
const app = this.parent as Docify;
const target=app.setting.docpath.asFileHandle();
const dbhandle=`sqlite://${target.genealogy.join("/")}/docify.db@owners`.asFileHandle();
dbhandle.read()
.then((d) => {
if (d.error) { return this.error(d.error); }
for (let v of d) {
v.text = v.name;
v.selected = this.data && (this.data.oid === v.id);
}
this.olist.data = d;
if (!this.olist.selectedItem) { return this.olist.selected = 0; }
}).catch((e) => {
return this.error(__("Unable to fetch owner list: {0}", e.toString()), e);
});
this.dlist.push({
text:"None",
value: 0
});
let selected = 0;
for (d = 1; d <= 31; d++) {
this.dlist.push({
text:`${d}`,
value: d
});
if (this.data && (parseInt(this.data.day) === d)) { selected = d; }
}
this.dlist.selected = selected;
this.mlist.push({
text:"None",
value: 0
});
selected = 0;
for (d = 1; d <= 12; d++) {
this.mlist.push({
text:`${d}`,
value: d
});
if (this.data && (parseInt(this.data.month) === d)) { selected = d; }
}
this.mlist.selected = selected;
this.ylist.push({
text:"None",
value: 0
});
this.ylist.selected = 0;
for (let y = 1960, end = new Date().getFullYear(), asc = 1960 <= end; asc ? y <= end : y >= end; asc ? y++ : y--) {
this.ylist.push({
text:`${y}`,
value: y,
selected: this.data && (parseInt(this.data.year) === y)
});
}
this.flist.buttons = [
{
text: "",
iconclass: "fa fa-plus-circle",
onbtclick: (e: any) => {
return this.openDialog(new FilePreviewDialog(), {
app: app
})
.then((d: { text: any; filename: any; }) => {
d.text = d.filename;
return this.flist.push(d);
});
}
},
{
text: "",
iconclass: "fa fa-minus-circle",
onbtclick: (e: any) => {
const item = this.flist.selectedItem;
if (!item) { return; }
return this.flist.delete(item);
}
}
];
this.flist.onlistselect = async (e) => {
return await app.preview(e.data.item.data.path, this.find("preview-canvas") as HTMLCanvasElement);
};
(this.find("btsave") as GUI.tag.ButtonTag).onbtclick = (e: any) => {
const data: GenericObject<any> = {
name: (this.find("title") as HTMLInputElement).value.trim(),
day: this.dlist.selectedItem.data.value,
month: this.mlist.selectedItem.data.value,
year: this.ylist.selectedItem.data.value,
file: (Array.from(this.flist.data).map((v: { path: any; }) => v.path)),
note: (this.find("note") as HTMLTextAreaElement).value.trim(),
tags: (this.find("tag") as HTMLInputElement).value.trim(),
oid: parseInt(this.olist.selectedItem.data.id)
};
if (!data.name || (data.title === "")) { return this.notify(__("Please enter title")); }
if (!(data.file.length > 0)) { return this.notify(__("Please attach files to the entry")); }
if (this.handle) { this.handle(data); }
return this.quit();
};
if (!this.data) { return; }
(this.find("title") as HTMLInputElement).value = this.data.name;
(this.find("note") as HTMLTextAreaElement).value = this.data.note;
(this.find("tag") as HTMLInputElement).value = this.data.tags;
const file = this.data.file.asFileHandle();
file.text = file.filename;
return this.flist.data = [ file ];
}
}
DocDialog.scheme = `\
<afx-app-window width='600' height='400'>
<afx-hbox>
<afx-vbox data-width="350">
<afx-hbox data-height="30">
<afx-label text = "__(title)" data-width="50"></afx-label>
<input type="text" data-id="title"></input>
</afx-hbox>
<afx-hbox data-height="30">
<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 = "30">
<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="35" >
<afx-button text="__(Save)" data-id="btsave" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>\
`;
export class FilePreviewDialog extends OS.GUI.BasicDialog {
private flist: GUI.tag.ListViewTag;
constructor() {
super("FilePreviewDialog", FilePreviewDialog.scheme);
}
main() {
super.main();
this.flist = this.find("file-list") as GUI.tag.ListViewTag;
this.flist.buttons = [
{
text: "",
iconclass: "fa fa-refresh",
onbtclick: (e: any) => this.refresh()
}
];
const app = this.data.app as Docify;
this.flist.onlistselect = async (e) => {
// console.log e.data.item.data
return await app.preview(e.data.item.data.path, this.find("preview-canvas") as HTMLCanvasElement);
};
(this.find("btok") as GUI.tag.ButtonTag).onbtclick = (e: any) => {
const item = this.flist.selectedItem;
if (!item) { return this.quit(); }
if (this.handle) { this.handle(item.data); }
return this.quit();
};
return this.refresh();
}
async refresh() {
try
{
const app = this.data.app as Docify;
const d = await `${app.setting.docpath}/unclassified`.asFileHandle().read();
if (d.error) { return this.error(d.error); }
for (let v of d.result) { v.text = v.filename; }
return this.flist.data = d.result.filter((e) => e.filename[0] !== '.');
}
catch(e)
{
return this.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="25"></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="35" >
<afx-button text="__(Ok)" data-id="btok" ></afx-button>
</div>
</afx-vbox>
</afx-hbox>
</afx-app-window>\
`;
}
}
}

543
Docify/ts/main.ts Normal file
View File

@ -0,0 +1,543 @@
namespace OS {
export namespace application {
export class Docify extends BaseApplication {
private catview: GUI.tag.ListViewTag;
private docview: GUI.tag.ListViewTag;
private docpreview: HTMLCanvasElement;
private docgrid: GUI.tag.GridViewTag;
private dbhandle: API.VFS.BaseFileHandle;
private catdb: API.VFS.BaseFileHandle;
private ownerdb: API.VFS.BaseFileHandle;
private docdb: API.VFS.BaseFileHandle;
constructor( args: any ) {
super("Docify", args);
}
private async init_db() {
try {
if (!this.setting.docpath) { return this.error(__("No configured docpath")); }
const target=this.setting.docpath.asFileHandle();
this.dbhandle=`sqlite://${target.genealogy.join("/")}/docify.db`.asFileHandle();
const tables = await this.dbhandle.read();
/**
* Init following tables if not exist:
* - categories
* - owners
* - docs
*/
await `${this.setting.docpath}`.asFileHandle().mk("unclassified");
await `${this.setting.docpath}`.asFileHandle().mk("cache");
let r = undefined;
this.catdb = `${this.dbhandle.path}@categories`.asFileHandle();
if(!tables.categories)
{
this.dbhandle.cache = {
name: "TEXT"
}
r = await this.dbhandle.write("categories");
if(r.error)
{
throw new Error(r.error as string);
}
this.catdb.cache = {
name: "Uncategoried"
};
r = await this.catdb.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
}
this.ownerdb = `${this.dbhandle.path}@owners`.asFileHandle();
if(!tables.owners)
{
this.dbhandle.cache = {
name: "TEXT",
}
r = await this.dbhandle.write("owners");
if(r.error)
{
throw new Error(r.error as string);
}
this.ownerdb.cache = {
name: "None"
};
r = await this.ownerdb.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
}
this.docdb = `${this.dbhandle.path}@docs`.asFileHandle();
if(!tables.docs)
{
this.dbhandle.cache = {
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',
}
r = await this.dbhandle.write("docs");
if(r.error)
{
throw new Error(r.error as string);
}
}
return await this.cat_refresh();
}
catch(e) {
this.error(__("Unable to init database file: {0}",e.toString()),e);
this.dbhandle = undefined;
}
}
main() {
if (!this.setting.printer) { this.setting.printer = ""; }
this.catview = this.find("catview") as GUI.tag.ListViewTag;
this.docview = this.find("docview") as GUI.tag.ListViewTag;
this.docpreview = this.find("preview-canvas") as HTMLCanvasElement;
this.docgrid = this.find("docgrid") as GUI.tag.GridViewTag;
this.docgrid.header = [
{ text: "", width: 100 },
{ text: "" },
];
(this.find("btdld") as GUI.tag.ButtonTag).onbtclick = async (e) => {
try {
const item = this.docview.selectedItem;
if (!item) { return; }
await item.data.file.asFileHandle().download();
}
catch(e)
{
this.error(__("Unable to download: {0}", e.toString()), e);
}
};
(this.find("btopen") as GUI.tag.ButtonTag).onbtclick = async (e) => {
try {
const item = this.docview.selectedItem;
if (!item) { return; }
const m = await item.data.file.asFileHandle().meta();
if (m.error)
{
throw new Error(m.error);
}
return this._gui.openWith(m.result);
}
catch(e)
{
this.error(__("Unable to open file: {0}", e.toString()), e);
}
};
this.catview.buttons = [
{
text: "",
iconclass: "fa fa-plus-circle",
onbtclick:async (e) => {
try
{
const d = await this.openDialog("PromptDialog", {
title: __("Category"),
label: __("Name")
});
this.catdb.cache = { name: d };
const r = await this.catdb.write(undefined);
if (r.error)
{
throw new Error(r.error.toString());
}
return await this.cat_refresh();
}
catch(e)
{
this.error(__("Unable to insert category: {0}", e.toString()), e);
}
}
},
{
text: "",
iconclass: "fa fa-minus-circle",
onbtclick: async (e) =>
{
try
{
const item = this.catview.selectedItem;
if (!item) { return; }
const d = await this.ask({ text:__("Do you realy want to delete: `{0}`", item.data.text)});
if (!d) { return; }
const r = await this.catdb.remove({
where: {
id: item.data.id
}
});
if(r.error)
{
throw new Error(r.error.toString());
}
await this.cat_refresh();
}
catch(e)
{
this.error(__("Unable delete category: {0}", e.toString()), e);
}
}
},
{
text: "",
iconclass: "fa fa-pencil-square-o",
onbtclick: async (_) => {
try
{
const item = this.catview.selectedItem;
if (!item) { return; };
const cat = item.data;
if (!cat) { return; }
const d = await this.openDialog("PromptDialog", {
title: __("Category"),
label: __("Name"),
value: item.data.name
});
const handle: API.VFS.BaseFileHandle = cat.$vfs;
handle.cache = { id: parseInt(item.data.id), name: d };
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error.toString());
}
await this.cat_refresh();
}
catch(e)
{
this.error(__("Unable to update category: {0}", e.toString()), e);
}
}
}
];
this.docview.onlistselect = async (evt) => {
try
{
this.clear_preview();
const item = evt.data.item;
if(!item) return;
const handle = item.data.$vfs as API.VFS.BaseFileHandle;
// TODO join owner here
const d = await handle.read();
await this.preview(d.file, this.docpreview);
const rows = [];
// TODO: if (d.result.fileinfo) { d.result.size = (d.result.fileinfo.size / 1024.0).toFixed(2) + " Kb"; }
const map = {
ctime: "Created on",
mtime: "Modified on",
note: "Note",
tags: "Tags",
name: "Title",
owner: "Owner",
edate: "Effective date",
file: "File",
size: "Size"
};
d.edate = `${d.day}/${d.month}/${d.year}`;
for (let key in d) {
let value = d[key];
const field = map[key];
if(key === "ctime" || key == "mtime")
{
value = (new Date(value*1000)).toDateString();
}
if (field) { rows.push([{text: field}, {text: value}]); }
}
return this.docgrid.rows = rows;
}
catch(e)
{
this.error(__("Unable to fetch document detail: {0}", e.toString()), e);
}
};
this.catview.onlistselect = (e) => {
this.clear_preview();
const item = e.data.item;
if (!item) { return; }
return this.update_doclist(item.data.id);
};
(this.find("bt-add-doc") as GUI.tag.ButtonTag).onbtclick = async (evt) => {
try
{
const catiem = this.catview.selectedItem;
if (!catiem) { return this.notify(__("Please select a category")); }
const data = await this.openDialog(new docify.DocDialog());
data.cid = parseInt(catiem.data.id);
const timestamp = Math.floor(Date.now() / 1000);
data.ctime = timestamp;
data.mtime = timestamp;
const r = await this.exec("merge_files", data);
if(r.error)
{
throw new Error(r.error.toString());
}
data.file = r.result;
this.docdb.cache = data;
const d = await this.docdb.write(undefined);
if(d.error)
{
throw new Error(d.error.toString());
}
if (d.result) { this.toast(d.result); }
this.update_doclist(catiem.data.id);
this.clear_preview();
}
catch(e)
{
this.error(__("Unable to add document: {0}", e.toString()), e);
}
};
(this.find("bt-del-doc") as GUI.tag.ButtonTag).onbtclick = async (evt) => {
try
{
const item = this.docview.selectedItem;
if (!item) { return; }
const d = await this.ask({ text: __("Do you really want to delete: `{0}`", item.data.name) });
if (!d) { return; }
let r = await this.docdb.remove({
where: {
id: item.data.id
}
});
if(r.error)
{
throw new Error(r.error.toString());
}
r = await this.exec("deletedoc", {file: item.data.file});
if(r.error)
{
throw new Error(r.error.toString());
}
this.notify(r.result.toString());
this.update_doclist(item.data.cid);
return this.clear_preview();
}
catch(e)
{
this.error(__("Unable to delete document: {0}", e.tostring()), e);
}
};
(this.find("bt-upload-doc") as GUI.tag.ButtonTag).onbtclick = async (evt) => {
try
{
await `${this.setting.docpath}/unclassified`.asFileHandle().upload();
this.toast(__("File uploaded"));
}
catch(e)
{
this.error(__("Unable to upload document: {0}", e.toString()), e);
}
}
(this.find("bt-edit-doc") as GUI.tag.ButtonTag).onbtclick = async (evt) => {
try
{
const item = this.docview.selectedItem;
const catiem = this.catview.selectedItem;
if (!item) { return; }
const data = await this.openDialog(new docify.DocDialog(), item.data);
data.cid = parseInt(catiem.data.id);
data.id = item.data.id;
const timestamp = Math.floor(Date.now() / 1000);
data.mtime = timestamp;
let d = await this.exec("updatedoc", {
data,
rm: !data.file.includes(item.data.file) ? item.data.file : false
});
if(d.error)
{
throw new Error(d.error);
}
const handle = item.data.$vfs;
handle.cache = d.result;
d = await handle.write(undefined);
if(d.error)
{
throw new Error(d.error);
}
if (d.result) { this.toast(d.result); }
this.update_doclist(catiem.data.id);
return this.clear_preview();
}
catch(e)
{
this.error(__("Unable to edit document metadata: {0}", e.toString()));
}
};
return this.initialize();
}
private async update_doclist(cid: any) {
try
{
const d = await this.docdb.read({
where: {
cid: cid
},
order: ["year$desc", "month$desc", "day$desc"]
});
// this.exec("select",{table: "docs", cond:`cid = ${cid} ORDER BY year DESC, month DESC, day DESC`});
if(d.error)
{
throw new Error(d.error);
}
for (let v of d)
{
v.text = v.name;
}
return this.docview.data = d;
}
catch(e)
{
this.error(__("Unable to update document list: {0}", e.toString()), e);
}
}
private clear_preview() {
this.docpreview.getContext('2d').clearRect(0,0,this.docpreview.width,this.docpreview.height);
return this.docgrid.rows = [];
}
async preview(path: any, canvas: HTMLCanvasElement) {
try {
const d = await this.exec("preview", path);
if (d.error) {
throw new Error(d.error);
}
const file = d.result.asFileHandle();
const data = await file.read("binary");
const img = new Image();
//($ me.view).append img
img.onload = () => {
const context = canvas.getContext('2d');
canvas.height = img.height;
canvas.width = img.width;
//console.log canvas.width, canvas.height
return context.drawImage(img, 0, 0);
};
const blob = new Blob([data], { type: file.info.mime });
return img.src = URL.createObjectURL(blob);
}
catch(e)
{
this.error(__("Unable to generate document thumbnail: {0}", e.toString()), e);
}
}
private cat_refresh(): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
this.docview.data = [];
this.clear_preview();
const d = await this.catdb.read();
for (let v of d) {
v.text = v.name;
}
return this.catview.data = d;
}
catch(e)
{
reject(__e(e));
}
});
}
private async initialize() {
try
{
// Check if we have configured docpath
if (this.setting.docpath) {
// check data base
return await this.init_db();
} else
{
// ask user to choose a docpath
const d = await this.openDialog("FileDialog", {
title:__("Please select a doc path"),
type: 'dir'
});
this.setting.docpath = d.file.path;
// save the doc path to local setting
//await this._api.setting();
return await this.init_db();
}
}
catch(e)
{
this.error(__("Error initialize database: {0}", e.toString()), e);
}
}
exec(action: string, args?: GenericObject<any>) {
const cmd = {
path: `${this.path()}/api.lua`,
parameters: {
action,
docpath: this.setting.docpath,
args
}
};
return this.call(cmd);
}
menu() {
return [
{
text: "__(Options)",
nodes: [
{ text: "__(Owners)", id:"owners"},
{ text: "__(Preview)", id:"preview"},
{ text: "__(Change doc path)", id:"setdocp"}
],
onchildselect: (e) => this.fileMenuHandle(e.data.item.data.id)
}
];
}
private fileMenuHandle(id: any) {
switch (id) {
case "owners":
return this.openDialog(new docify.OwnerDialog(), {
title: __("Owners"),
dbhandle: this.ownerdb
});
case "preview":
return this.openDialog(new docify.FilePreviewDialog(), {
app: this
})
.then((d: { path: any; }) => {
return this.notify(d.path);
});
case "setdocp":
this.setting.docpath = undefined;
return this.initialize();
}
}
}
Docify.dependencies = ["pkg://SQLiteDB/libsqlite.js"];
}
}

View File

@ -154,9 +154,9 @@
"name": "Docify",
"description": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/Docify/README.md",
"category": "Office",
"author": "",
"version": "0.0.9-b",
"dependencies": [],
"author": "Dany LE",
"version": "0.1.0-b",
"dependencies": ["SQLiteDB@0.1.0-a"],
"download": "https://raw.githubusercontent.com/lxsang/antosdk-apps/2.0.x/Docify/build/release/Docify.zip"
},
{