Compare commits

..

42 Commits

Author SHA1 Message Date
5594516697 fix: Libreoffice v0.1.6-a 2025-05-28 22:44:47 +02:00
4a62c4d1cf add SVGEdit package
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-08-17 23:54:41 +02:00
3a3f55e7c8 RemoteDesktop: allow saving current connection to setting
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-23 13:18:43 +02:00
761c2556cb GPClient: enable clipboard read/write in iframe
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-21 21:51:15 +02:00
cb6b76d067 GPClient: minor fix on dialog
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-18 19:16:37 +02:00
d9726d6041 GPClient: minor fix on dialog
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-18 19:16:14 +02:00
675f245b3f update 2023-07-18 18:31:16 +02:00
5618965d3c fix(GPClient): do not assign directly the setting object as data to an UI element 2023-07-18 18:28:34 +02:00
d8d38f5b05 feat(LibreOffice): allow setting document server URI
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-17 18:27:29 +02:00
3b14ff5b9f fix: bugs on new GUI API
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-14 12:05:12 +02:00
4d03eff031 SystemControl: use new AntoS local setting APIo
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-08 13:54:22 +02:00
7174dfb8c9 Blogger: fix blog saving bug
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-07-08 13:34:27 +02:00
f8435cd87c Docify: remove server side script, move all operation to client side
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-27 22:27:09 +02:00
e8aa62735a add PDFLib package
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-27 10:08:46 +02:00
b3ea32f4bc fix(CodePad,vfx,Antedit): use new Task API and setting API
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-20 17:43:31 +02:00
4c86a315dd fix: relese regen
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-07 15:04:55 +02:00
d70391c7ba update(SystemControl): use app base dialog instead of global dialog
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-07 09:54:35 +02:00
8153ca6f9d add jenkinsfile
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-01 17:07:38 +02:00
a628b57109 add jenkinsfile
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-01 17:00:12 +02:00
e22e8c8e8c add jenkinsfile
All checks were successful
gitea-sync/antosdk-apps/pipeline/head This commit looks good
2023-06-01 16:37:53 +02:00
4a0a611acc add release job using AntOS build system 2023-06-01 16:33:07 +02:00
0227f02ad6 regen all archive using antos build system 2023-06-01 16:14:35 +02:00
7cbd4ce979 regen package indexes 2023-06-01 12:26:10 +02:00
79ddff1cbd libantosdk: linux worker is renamed to backend, linux-exec job renamed to cmd-exec, add lua-exec job for backend lua script execution 2023-06-01 12:24:34 +02:00
402f0edb49 libantosdk: cleanup delivery 2023-05-30 14:07:55 +02:00
77258bd48b libantosdk: do not includes sdk api to package, API files shall be featched from delivery page 2023-05-30 13:30:05 +02:00
d011b004ec update(Blogger): generate index file for front-end blog search 2023-05-22 14:04:05 +02:00
f9f27321ab feat(Blogger): support blog posts TF-IDF analyse 2023-04-24 16:41:26 +02:00
545f630b0e Archive: fix file dialog bug when extract zip content 2023-03-31 14:47:51 +02:00
7292d2ef21 Docify: Use libsqlite to handle database in Docify 2023-03-27 20:38:17 +02:00
6354c48680 Blogger: use luasocket for mail sending 2023-02-24 18:41:36 +01:00
5dec0a2b56 Blogger: migrate code to typescript,use SQLiteDB as database access API 2023-02-23 16:59:33 +01:00
c8ddd5ec6e SQLiteDB: fix database select bug 2023-02-20 18:06:10 +01:00
04050f124f SQLiteDB: first working version 2023-02-17 12:26:59 +01:00
cf21ef60e0 Update AntOSDK and SQLiteDB
- AntOSDK: add some public API on grid view
- SQLiteDB: add main application as a basic SQLiteBrowser
2023-02-02 21:06:10 +01:00
cd5b0f66cc SQLiteDB: add basic VFS binding for SQLite database file 2023-02-01 10:12:51 +01:00
e3deffe907 antosdk: use latest AntOS description files 2023-01-31 20:56:53 +01:00
11df616319 add lib for sqlite3 database 2023-01-30 09:37:44 +01:00
9afa2f5d1c Update LibreOffice and OnlyOffice to latests backend API 2023-01-29 21:41:28 +01:00
a2a602f5b2 RemoteDesktop: v0.1.16 - Allow to enable/disable mouse capture in remote desktop, remove some unused toolbar buttons 2023-01-08 00:21:02 +01:00
b3db4861ef RemoteDesktop: * v0.1.15 - Only send ACK command when finish rendering the received frame, this allows to vastly improve performance and bandwidth 2023-01-07 19:36:47 +01:00
fc515ff012 Send only ready when we finish rendering the received frame 2023-01-07 19:29:37 +01:00
746 changed files with 15625 additions and 145430 deletions

View File

@ -17,8 +17,10 @@
"data": ["build","build/debug","build/release"]
},
{
"name": "ts-import",
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",

Binary file not shown.

Binary file not shown.

View File

@ -5,4 +5,7 @@ It's built on top of google-diff-match-patch library. That lib handles the hard
Github page: [https://github.com/ace-diff/ace-diff](https://github.com/ace-diff/ace-diff).
The ACE diff depends on the ACECore package.
The ACE diff depends on the ACECore package.
## Change logs
- v0.1.1-a: add dependencies

Binary file not shown.

View File

@ -5,6 +5,8 @@ the editor that powers VS Code.
The editor functionality can be extended by its extension mechanism.
Extension can be developed/released/isntalled by the editor itself.
### Change logs
- 0.2.6-b: Fix resizer bug on new UI API
- 0.2.5-b: Fix setting bug with new AntOS setting API
- 0.2.3-b: Minor changes to adapt the core UI to the new AntOS 2.0.x
- 0.2.3-b: Allow reload current file via context menu in case of external changes
- 0.2.2-b: Support horizotal scrolling on horizotal tabbars

View File

@ -35,7 +35,7 @@
<div data-id="right-editorarea"></div>
</afx-vbox>
</afx-hbox>
<afx-resizer data-height = "3" dir = "ve" attachnext = "true" ></afx-resizer>
<afx-resizer data-height = "3" attachnext = "true" ></afx-resizer>
<afx-tab-container data-id = "bottombar" data-height="150" min-height="150" tabbarheight= "35">
<afx-hbox tabname="__(Output)" iconclass = "fa fa-file-text" class = "bottom-tab-content">
<afx-button text = "" data-id="logger-clear" iconclass="fa fa-trash" data-width="21"></afx-button>

View File

@ -27,8 +27,10 @@
"require":["ts"],
"jobs": [
{
"name": "ts-import",
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",

View File

@ -5,6 +5,8 @@ the editor that powers VS Code.
The editor functionality can be extended by its extension mechanism.
Extension can be developed/released/isntalled by the editor itself.
### Change logs
- 0.2.6-b: Fix resizer bug on new UI API
- 0.2.5-b: Fix setting bug with new AntOS setting API
- 0.2.3-b: Minor changes to adapt the core UI to the new AntOS 2.0.x
- 0.2.3-b: Allow reload current file via context menu in case of external changes
- 0.2.2-b: Support horizotal scrolling on horizotal tabbars

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@iohub.dev"
},
"version": "0.2.4-b",
"version": "0.2.6-b",
"category": "Development",
"iconclass": "bi bi-journal-code",
"mimes": [

View File

@ -35,7 +35,7 @@
<div data-id="right-editorarea"></div>
</afx-vbox>
</afx-hbox>
<afx-resizer data-height = "3" dir = "ve" attachnext = "true" ></afx-resizer>
<afx-resizer data-height = "3" attachnext = "true" ></afx-resizer>
<afx-tab-container data-id = "bottombar" data-height="150" min-height="150" tabbarheight= "35">
<afx-hbox tabname="__(Output)" iconclass = "fa fa-file-text" class = "bottom-tab-content">
<afx-button text = "" data-id="logger-clear" iconclass="fa fa-trash" data-width="21"></afx-button>

Binary file not shown.

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@iohub.dev"
},
"version": "0.2.4-b",
"version": "0.2.6-b",
"category": "Development",
"iconclass": "bi bi-journal-code",
"mimes": [

View File

@ -59,8 +59,7 @@ namespace OS {
});
}
protected _wr(t: string, d: any): Promise<any> {
this.cache = d;
protected _wr(t: string): Promise<any> {
return new Promise((resolve, reject) =>
{
resolve({

View File

@ -561,7 +561,7 @@ namespace OS {
this.loadExtensionMetaData();
this.toggleSideBar();
this.toggleSplitMode();
this.applyAllSetting();
//this.applyAllSetting();
}
/**
@ -718,7 +718,7 @@ namespace OS {
showOutput(toggle: boolean = false): void {
if (toggle)
this.showBottomBar(true);
this.setting.showBottomBar = true;
this.bottombar.selectedIndex = 0;
}
@ -749,7 +749,6 @@ namespace OS {
* @memberof Antedit
*/
public showBottomBar(v: boolean): void {
this.setting.showBottomBar = v;
if (v) {
$(this.bottombar).show();
}
@ -765,7 +764,7 @@ namespace OS {
* @memberof Antedit
*/
private toggleBottomBar(): void {
this.showBottomBar(!this.setting.showBottomBar);
this.setting.showBottomBar = !this.setting.showBottomBar;
}
/**
@ -817,7 +816,7 @@ namespace OS {
dataid: "recent",
nodes: recent,
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>,
e: GUI.TagEventType<GUI.tag.StackMenuEventData>,
r: Antedit
) => {
const handle = e.data.item.data.text.asFileHandle();
@ -849,7 +848,7 @@ namespace OS {
},
],
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>,
e: GUI.TagEventType<GUI.tag.StackMenuEventData>,
r: Antedit
) => {
return this.menuAction(e.data.item.data.dataid, r);
@ -866,9 +865,9 @@ namespace OS {
* @memberof Antedit
*/
private ctxFileMenuHandle(
e: GUI.TagEventType<GUI.tag.MenuEventData>
e: GUI.TagEventType<GUI.tag.StackMenuEventData>
): void {
const el = e.data.item as GUI.tag.MenuEntryTag;
const el = e.data.item;
if (!el) {
return;
}
@ -1150,7 +1149,7 @@ namespace OS {
}
],
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>,
e: GUI.TagEventType<GUI.tag.StackMenuEventData>,
r: EditorFileHandle
) => {
switch (e.data.item.data.dataid) {

Binary file not shown.

View File

@ -17,8 +17,10 @@
"data": ["build","build/debug","build/release"]
},
{
"name": "ts-import",
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",

View File

@ -9,7 +9,10 @@ Small application for zip file manager
## Changle log
### v0.0.2-a
### v0.0.4-a
* Fix file dialog bug when extract zip content
### v0.0.3-a
* Change category to utility
### v0.0.2-a

View File

@ -9,7 +9,10 @@ Small application for zip file manager
## Changle log
### v0.0.2-a
### v0.0.4-a
* Fix file dialog bug when extract zip content
### v0.0.3-a
* Change category to utility
### v0.0.2-a

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@lxsang.me"
},
"version":"0.0.3-a",
"version":"0.0.4-a",
"category":"Utility",
"iconclass":"fa fa-archive",
"mimes":["application/zip"],

Binary file not shown.

View File

@ -20,7 +20,7 @@ class Archive extends this.OS.application.BaseApplication
item = @filetree.selectedItem
return @notify __("Please select file/folder to extract") unless item
treedata = item.data
@openDialog "FileDialog", { title: __("Select a folder"), mimes: ["dir"] }
@openDialog "FileDialog", { title: __("Select a folder"), type: "dir" }
.then (d) =>
@xtract(treedata, d.file.path)
.then () => @notify __("extract successful: {0}", treedata.path)

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@lxsang.me"
},
"version":"0.0.3-a",
"version":"0.0.4-a",
"category":"Utility",
"iconclass":"fa fa-archive",
"mimes":["application/zip"],

View File

@ -6,6 +6,10 @@ Blackend for my blog at https://blog.iohub.dev
## Change logs
### v0.2.x-a
* Patch 13: fix bug on blog save
* Patch 12: support send mail via SSL
* Patch 11: Add TFIDF analyse functionality
* Patch 10: Migrate code to typescript, use SQLiteDB lib for database access
* Patch 9: Update to use the new MDE library
* Patch 8: Support for antOS 2.0.x
* Patch 7: Fix sendmail API security bug

View File

@ -0,0 +1,83 @@
local args = ...
local ret = {
error = false,
result = nil
}
local __dir__ = debug.getinfo(1).source:match("@?(.*/)")
LOG_DEBUG("CURRENT PATH:%s", __dir__)
local cluster = loadfile(__dir__.."/cluster.lua")()
local dbpath = require("vfs").ospath(args.dbpath)
LOG_DEBUG("DB PATH:%s", dbpath)
local gettext = {}
gettext.get = function(file)
local db = DBModel:new{db=file}
db:open()
if not db then return nil end
local data, sort = db:find("blogs", {
where = { publish = 1 },
fields = {"id", "content"}
})
db:close()
if not data or #data == 0 then return nil end
return data
end
gettext.stopwords = function(ospath)
local words = {}
for line in io.lines(ospath) do
words[line] = true
end
return words
end
local data = gettext.get(dbpath)
local documents = {}
if data then
local sw = gettext.stopwords(__dir__.."/stopwords.txt")
for k, v in pairs(data) do
local bag = cluster.bow(data[k].content, sw)
documents[data[k].id] = bag
end
cluster.tfidf(documents)
-- indexing all terms to cache file
local cache_file = dbpath..".index.json"
local f = io.open(cache_file, "w")
if f then
local indexes = {}
for id, doc in pairs(documents) do
for term,v in pairs(doc) do
if not indexes[term] then
indexes[term] = {}
end
indexes[term][tostring(id)] = doc[term].tfidf
end
end
f:write(JSON.encode(indexes))
f:close()
end
--
--local v = cluster.search("arm", documents)
--echo(JSON.encode(v))
local vectors, maxv, size = cluster.get_vectors(documents)
local analytical = DBModel:new{db=dbpath}
analytical:open()
-- purge the table
analytical:delete("st_similarity", nil)
-- get similarity and put to the table
for id, v in pairs(vectors) do
local top = cluster.top_similarity(id, vectors, args.top, 0.1)
for a, b in pairs(top) do
local record = {pid = id, sid = a, score = b}
analytical:insert("st_similarity", record)
end
end
analytical:close()
ret.result = "Analyse complete"
else
ret.error = "Unable to query database for post"
end
return ret

346
Blogger/api/ai/cluster.lua Normal file
View File

@ -0,0 +1,346 @@
local doclassify = {}
local st = require("stmr")
doclassify.bow = function(data, stopwords)
-- first step get a table of worlds that contain
-- world: occurences
local bag = {}
for w in data:gmatch('%w+') do
local word = w:lower()
if not stopwords[word] then
word = st.stmr(word)
if bag[word] then
bag[word].count = bag[word].count + 1
else
bag[word] = {count=0, tf=0, tfidf=0.0}
bag[word].count = 1
end
end
end
-- now calculate the tf of the bag
for k,v in pairs(bag) do
bag[k].tf = math.log(1 + bag[k].count)
end
return bag
end
doclassify.len = function(table)
local cnt = 0
for k,v in pairs(table) do cnt = cnt+1 end
return cnt
end
doclassify.tfidf = function(documents)
-- now for each term in a bag, calculate
-- the inverse document frequency, which
-- is a measure of how much information
-- the word provides, that is, whether the
-- term is common or rare across all documents
local ndoc = doclassify.len(documents)
for k,bag in pairs(documents) do
-- for eacht term in bag
-- calculate its idf across all documents
for term,b in pairs(bag) do
local n = 0
for id,doc in pairs(documents) do
if doc[term] then n = n+1 end
end
--echo("term:"..term.." appears in"..n.." documents")
b.tfidf = b.tf*math.log(ndoc/n)
end
end
end
doclassify.search = function(term, documents)
local r = {}
for id, doc in pairs(documents) do
if doc[term:lower()] then
r[id] = doc[term].tfidf
end
end
return r
end
doclassify.get_vectors = function(documents)
-- get a list of vector from documents
local index = 0
local vectors = {}
local maps = {}
local terms = {}
local maxv = 0
for id in pairs(documents) do
maps[id] = {}
vectors[id] = {}
end
-- first loop, get the term
for id, doc in pairs(documents) do
for k,v in pairs(doc) do
-- get max value
if v.tfidf > maxv then
maxv = v.tfidf
end
-- get the term
if not terms[k] then
index = index + 1
terms[k] = index
end
for pid in pairs(documents) do
if not maps[pid][k] then
if id == pid then
maps[pid][k] = v.tfidf
else
maps[pid][k] = 0
end
else
if maps[pid][k] == 0 and id == pid then
maps[pid][k] = v.tfidf
end
end
end
end
end
-- reindexing the vectors
for id in pairs(documents) do
for k,v in pairs(maps[id]) do
vectors[id][terms[k]] = v
end
end
--echo("Max tfidf "..maxv.." in document #"..maxid.." of term "..term)
return vectors, maxv, index, terms
end
doclassify.similarity = function(va, vb)
-- using cosin similarity
local dotp = 0
local maga = 0
local magb = 0
for k = 1,#va do
dotp = dotp + va[k]*vb[k]
maga = maga + va[k]*va[k]
magb = magb + vb[k]*vb[k]
end
maga = math.sqrt(maga)
magb = math.sqrt(magb)
local d = 0
if maga ~= 0 and magb ~= 0 then
d = dotp/ (magb*maga)
end
return d
end
doclassify.similarities = function(v1, collection)
local similarities = {}
assert(#v1 == #(collection[1]), "Incorrect vectors size")
for i=1,#collection do
similarities[i] = doclassify.similarity(v1, collection[i])
end
return similarities
end
doclassify.mean_similarity = function(v1, v2)
assert(#v1 == #v2, "Incorrect vectors size")
local similarities = {}
for i = 1,#v1 do similarities[i] = doclassify.similarity(v1[i], v2[i]) end
return doclassify.mean(similarities)
end
doclassify.similarity_chart = function(id, vectors)
local vs = {}
local cnt = 0
local lut = {}
for k,v in pairs(vectors) do
if k ~= id then
cnt = cnt + 1
vs[cnt] = v
lut[cnt] = k
end
end
if not vs[1] then return {} end
return doclassify.similarities(vectors[id], vs), lut
end
doclassify.top_similarity = function(id, vectors, n, th)
local chart,lut = doclassify.similarity_chart(id,vectors)
--echo(JSON.encode(chart))
--echo(JSON.encode(lut))
if not lut or #lut <= 0 then return nil end
local top = {}
local j=0
local goon = true
if not th then
goon = false
th = 0
end
while j < n or goon
do
local i,maxv = doclassify.argmax(chart)
top[lut[i]] = maxv
chart[i] = 0.0
j=j+1
if maxv < th and goon then
goon = false
end
end
--for j=1,n do
-- local i,maxv = doclassify.argmax(chart)
-- top[lut[i]] = maxv
-- chart[i] = 0.0
--end
return top
end
doclassify.save_vectors = function(vectors, name)
local f = io.open(name,"w")
if f == nil then return false end
for id, v in pairs(vectors) do
f:write(id)
for i=1,#v do f:write(","..v[i]) end
f:write("\n")
end
f:close()
return true
end
doclassify.save_topchart = function(vectors, name,n)
local f = io.open(name,"w")
if f == nil then return false end
for k,v in pairs(vectors) do
local top = doclassify.top_similarity(k,vectors,n, 0.1)
for a,b in pairs(top) do
f:write(k.." "..a.." "..b.."\n")
end
end
f:close()
return true
end
doclassify.kmean = function(nclass, documents, maxstep, ids)
-- now
local vectors, maxv, size = doclassify.get_vectors(documents)
-- random centroids
local centroids = {}
local old_centroids = {}
local clusters = {}
--for pid in pairs(documents) do clusters[pid] = 0 end
-- add noise to mean_vector
for i = 1,nclass do
if ids == nil then
centroids[i] = doclassify.random(size,math.floor(maxv))
else
centroids[i] = vectors[ids[i]]
end
old_centroids[i] = doclassify.zeros(size)
end
-- loop until convergence or maxstep reached
local similarity = doclassify.mean_similarity(centroids, old_centroids)
local step = maxstep
while 1.0-similarity > 1e-9 and step > 0 do
clusters = {}
--echo(JSON.encode(centroids))
for id,v in pairs(vectors) do
local similarities = doclassify.similarities(v, centroids)
--echo(JSON.encode(similarities))
local cluster, maxvalue = doclassify.argmax(similarities)
--echo("doc #"..id.." is in clusters #"..cluster.." max value is "..maxvalue)
clusters[id] = cluster
end
-- storing the old centroids
old_centroids = centroids
-- calculate new centroids
local new_centroids = {}
for class in pairs(centroids) do
local cnt = 0
local cvectors = {}
for id,v in pairs(vectors) do
if clusters[id] == class then
cnt = cnt + 1
cvectors[cnt] = v
end
end
new_centroids[class] = doclassify.mean_vector(cvectors, size)
end
centroids = new_centroids
--echo(JSON.encode(centroids))
--echo(JSON.encode(old_centroids))
similarity = doclassify.mean_similarity(centroids, old_centroids)
echo("step #"..step..", similarity "..similarity)
step = step - 1
end
local results = {}
for i = 1,nclass do
local list = {}
local cnt = 0
for id,c in pairs(clusters) do
if c == i then
cnt = cnt + 1
list[cnt] = id
end
end
results[i] = list
end
return results, clusters, centroids
end
doclassify.zeros = function(n)
local vector = {}
for i = 1,n do vector[i] = 0.0 end
return vector
end
doclassify.random = function(n,maxv)
local vector = {}
for i=1,n do
vector[i] = math.random() + math.random(0, maxv)
end
return vector
end
doclassify.sum = function(v)
local sum = 0.0
for i=1,#v do sum = sum + v[i] end
return sum
end
doclassify.mean = function(v)
return doclassify.sum(v)/#v
end
doclassify.mean_vector = function(vectors, size)
local means = doclassify.zeros(size)
if not vectors or #vectors == 0 then return means end
--local size = #(vectors[1])
local times = 0
for k,v in pairs(vectors) do
for i=1,#v do means[i] = means[i] + v[i] end
times = times + 1
end
for i = 1,size do means[i] = means[i]/times end
return means
end
doclassify.argmin = function(v)
local minv = 0.0
local mini = 0.0
for i = 1,#v do
if v[i] <= minv then
mini = i
minv = v[i]
end
end
--echo("min index"..mini.." val "..minv)
return mini, minv
end
doclassify.argmax = function(v)
local maxv = 0.0
local maxi = 0.0
for i = 1,#v do
if v[i] >= maxv then
maxi = i
maxv = v[i]
end
end
return maxi,maxv
end
return doclassify

View File

@ -0,0 +1,151 @@
i
me
my
myself
we
our
ours
ourselves
you
your
yours
yourself
yourselves
he
him
his
himself
she
her
hers
herself
it
its
itself
they
them
their
theirs
themselves
what
which
who
whom
this
that
these
those
am
is
are
was
were
be
been
being
have
has
had
having
do
does
did
doing
a
an
the
and
but
if
or
because
as
until
while
of
at
by
for
with
about
against
between
into
through
during
before
after
above
below
to
from
up
down
in
out
on
off
over
under
again
further
then
once
here
there
when
where
why
how
all
any
both
each
few
more
most
other
some
such
no
nor
not
only
own
same
so
than
too
very
s
t
can
will
just
don
should
now
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
w
r
s
t
x
y
z

50
Blogger/api/ai/test.lua Normal file
View File

@ -0,0 +1,50 @@
local path = require("fs/vfs").ospath("home://aiws/blog-clustering")
local gettext = loadfile(path.."/gettext.lua")()
local cluster = loadfile(path.."/cluster.lua")()
local refresh = false
local file = "/home/mrsang/test.csv"
if refresh then
local data = gettext.get({publish=1})
local documents = {}
if data then
local sw = gettext.stopwords("home://aiws/blog-clustering/stopwords.txt")
for k,v in pairs(data) do
local bag = cluster.bow(data[k].content, sw)
documents[data[k].id] = bag
end
cluster.tfidf(documents)
--local v = cluster.search("arm", documents)
--echo(JSON.encode(v))
local vectors, maxv, size = cluster.get_vectors(documents)
local s = cluster.save_topchart(vectors,file, 3)
if s then echo("file saved") else echo("error save file") end
--echo(JSON.encode(r))
--r = cluster.similarity(vectors["14"],vectors["16"])
--echo("Similarity "..r)
--local c,l = cluster.kmean(3, documents, 10)
--echo(JSON.encode(c))
--echo(JSON.encode(l))
else
echo("Data missing")
end
else
local f = io.open(file,"r")
local result = {}
for line in f:lines() do
local arr = {}
local cnt = 0
for i in line:gmatch( "%S+") do
cnt = cnt + 1
arr[cnt] = i
end
if not result[arr[1]] then result[arr[1]] = {} end
result[arr[1]][arr[2]] = tonumber(arr[3])
end
f:close()
echo(JSON.encode(result))
--local r = cluster.top_similarity("2",vectors, 3)
--echo(JSON.encode(r))
end

View File

@ -1,30 +1,70 @@
local data = ...
-- print(data.content)
-- Michal Kottman, 2011, public domain
local socket = require 'socket'
local smtp = require 'socket.smtp'
local ssl = require 'ssl'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
function sslCreate()
local sock = socket.tcp()
return setmetatable({
connect = function(_, host, port)
local r, e = sock:connect(host, port)
if not r then return r, e end
sock = ssl.wrap(sock, {mode='client', protocol='tlsv1_2'})
return sock:dohandshake()
end
}, {
__index = function(t,n)
return function(_, ...)
return sock[n](sock, ...)
end
end
})
end
function sendMail(user, password, to,subject, body)
local msg = {
headers = {
from = string.format("%s <%s@iohub.dev>", user, user),
to = string.format("%s <%s>",to.text, to.email),
subject = subject
},
body = body
}
local ok, err = smtp.send {
from = string.format('<%s@iohub.dev>', user),
rcpt = string.format('<%s>', to.email),
source = smtp.message(msg),
user = string.format('%s@iohub.dev', user),
password = password,
server = 'iohub.dev',
port = 465,
create = sslCreate
}
if not ok then
return false, error
else
return true
end
end
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
for k,v in pairs(data.to) do
LOG_DEBUG("Send email to:"..v.email)
local r,e = sendMail(data.user, data.password, v, data.title, data.content)
if not r then
iserror = true
table.insert(error_msg, v.email)
LOG_ERROR(string.format("Unable to send mail to %s: %s",v.email, e))
end
else
iserror = true
table.insert(error_msg, "Cannot create mail file")
end
local result = {}
result.error = iserror

View File

@ -13,18 +13,24 @@
}
]
},
"coffee": {
"ts": {
"require": [
"coffee"
"ts"
],
"jobs": [
{
"name": "coffee-compile",
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",
"data": {
"src": [
"main.coffee",
"dialogs.coffee",
"tags.coffee"
"main.ts",
"dialogs.ts",
"tags.ts"
],
"dest": "build/debug/main.js"
}
@ -51,9 +57,7 @@
"data": {
"src": [
"scheme.html",
"cvsection.html",
"api/sendmail.lua",
"sendmail.html",
"api",
"package.json",
"README.md",
"main.css"
@ -62,6 +66,27 @@
}
}
]
},
"locale": {
"require": ["locale"],
"jobs": [
{
"name":"locale-gen",
"data": {
"src": "",
"exclude": ["build/", "api/"],
"locale": "en_GB",
"dest": "package.json"
}
}
]
},
"debug": {
"depend": [
"init",
"ts",
"copy"
]
},
"release": {
"require": [
@ -69,7 +94,7 @@
],
"depend": [
"init",
"coffee",
"ts",
"uglify",
"copy"
],

View File

@ -6,6 +6,10 @@ Blackend for my blog at https://blog.iohub.dev
## Change logs
### v0.2.x-a
* Patch 13: fix bug on blog save
* Patch 12: support send mail via SSL
* Patch 11: Add TFIDF analyse functionality
* Patch 10: Migrate code to typescript, use SQLiteDB lib for database access
* Patch 9: Update to use the new MDE library
* Patch 8: Support for antOS 2.0.x
* Patch 7: Fix sendmail API security bug

View File

@ -0,0 +1,83 @@
local args = ...
local ret = {
error = false,
result = nil
}
local __dir__ = debug.getinfo(1).source:match("@?(.*/)")
LOG_DEBUG("CURRENT PATH:%s", __dir__)
local cluster = loadfile(__dir__.."/cluster.lua")()
local dbpath = require("vfs").ospath(args.dbpath)
LOG_DEBUG("DB PATH:%s", dbpath)
local gettext = {}
gettext.get = function(file)
local db = DBModel:new{db=file}
db:open()
if not db then return nil end
local data, sort = db:find("blogs", {
where = { publish = 1 },
fields = {"id", "content"}
})
db:close()
if not data or #data == 0 then return nil end
return data
end
gettext.stopwords = function(ospath)
local words = {}
for line in io.lines(ospath) do
words[line] = true
end
return words
end
local data = gettext.get(dbpath)
local documents = {}
if data then
local sw = gettext.stopwords(__dir__.."/stopwords.txt")
for k, v in pairs(data) do
local bag = cluster.bow(data[k].content, sw)
documents[data[k].id] = bag
end
cluster.tfidf(documents)
-- indexing all terms to cache file
local cache_file = dbpath..".index.json"
local f = io.open(cache_file, "w")
if f then
local indexes = {}
for id, doc in pairs(documents) do
for term,v in pairs(doc) do
if not indexes[term] then
indexes[term] = {}
end
indexes[term][tostring(id)] = doc[term].tfidf
end
end
f:write(JSON.encode(indexes))
f:close()
end
--
--local v = cluster.search("arm", documents)
--echo(JSON.encode(v))
local vectors, maxv, size = cluster.get_vectors(documents)
local analytical = DBModel:new{db=dbpath}
analytical:open()
-- purge the table
analytical:delete("st_similarity", nil)
-- get similarity and put to the table
for id, v in pairs(vectors) do
local top = cluster.top_similarity(id, vectors, args.top, 0.1)
for a, b in pairs(top) do
local record = {pid = id, sid = a, score = b}
analytical:insert("st_similarity", record)
end
end
analytical:close()
ret.result = "Analyse complete"
else
ret.error = "Unable to query database for post"
end
return ret

View File

@ -0,0 +1,346 @@
local doclassify = {}
local st = require("stmr")
doclassify.bow = function(data, stopwords)
-- first step get a table of worlds that contain
-- world: occurences
local bag = {}
for w in data:gmatch('%w+') do
local word = w:lower()
if not stopwords[word] then
word = st.stmr(word)
if bag[word] then
bag[word].count = bag[word].count + 1
else
bag[word] = {count=0, tf=0, tfidf=0.0}
bag[word].count = 1
end
end
end
-- now calculate the tf of the bag
for k,v in pairs(bag) do
bag[k].tf = math.log(1 + bag[k].count)
end
return bag
end
doclassify.len = function(table)
local cnt = 0
for k,v in pairs(table) do cnt = cnt+1 end
return cnt
end
doclassify.tfidf = function(documents)
-- now for each term in a bag, calculate
-- the inverse document frequency, which
-- is a measure of how much information
-- the word provides, that is, whether the
-- term is common or rare across all documents
local ndoc = doclassify.len(documents)
for k,bag in pairs(documents) do
-- for eacht term in bag
-- calculate its idf across all documents
for term,b in pairs(bag) do
local n = 0
for id,doc in pairs(documents) do
if doc[term] then n = n+1 end
end
--echo("term:"..term.." appears in"..n.." documents")
b.tfidf = b.tf*math.log(ndoc/n)
end
end
end
doclassify.search = function(term, documents)
local r = {}
for id, doc in pairs(documents) do
if doc[term:lower()] then
r[id] = doc[term].tfidf
end
end
return r
end
doclassify.get_vectors = function(documents)
-- get a list of vector from documents
local index = 0
local vectors = {}
local maps = {}
local terms = {}
local maxv = 0
for id in pairs(documents) do
maps[id] = {}
vectors[id] = {}
end
-- first loop, get the term
for id, doc in pairs(documents) do
for k,v in pairs(doc) do
-- get max value
if v.tfidf > maxv then
maxv = v.tfidf
end
-- get the term
if not terms[k] then
index = index + 1
terms[k] = index
end
for pid in pairs(documents) do
if not maps[pid][k] then
if id == pid then
maps[pid][k] = v.tfidf
else
maps[pid][k] = 0
end
else
if maps[pid][k] == 0 and id == pid then
maps[pid][k] = v.tfidf
end
end
end
end
end
-- reindexing the vectors
for id in pairs(documents) do
for k,v in pairs(maps[id]) do
vectors[id][terms[k]] = v
end
end
--echo("Max tfidf "..maxv.." in document #"..maxid.." of term "..term)
return vectors, maxv, index, terms
end
doclassify.similarity = function(va, vb)
-- using cosin similarity
local dotp = 0
local maga = 0
local magb = 0
for k = 1,#va do
dotp = dotp + va[k]*vb[k]
maga = maga + va[k]*va[k]
magb = magb + vb[k]*vb[k]
end
maga = math.sqrt(maga)
magb = math.sqrt(magb)
local d = 0
if maga ~= 0 and magb ~= 0 then
d = dotp/ (magb*maga)
end
return d
end
doclassify.similarities = function(v1, collection)
local similarities = {}
assert(#v1 == #(collection[1]), "Incorrect vectors size")
for i=1,#collection do
similarities[i] = doclassify.similarity(v1, collection[i])
end
return similarities
end
doclassify.mean_similarity = function(v1, v2)
assert(#v1 == #v2, "Incorrect vectors size")
local similarities = {}
for i = 1,#v1 do similarities[i] = doclassify.similarity(v1[i], v2[i]) end
return doclassify.mean(similarities)
end
doclassify.similarity_chart = function(id, vectors)
local vs = {}
local cnt = 0
local lut = {}
for k,v in pairs(vectors) do
if k ~= id then
cnt = cnt + 1
vs[cnt] = v
lut[cnt] = k
end
end
if not vs[1] then return {} end
return doclassify.similarities(vectors[id], vs), lut
end
doclassify.top_similarity = function(id, vectors, n, th)
local chart,lut = doclassify.similarity_chart(id,vectors)
--echo(JSON.encode(chart))
--echo(JSON.encode(lut))
if not lut or #lut <= 0 then return nil end
local top = {}
local j=0
local goon = true
if not th then
goon = false
th = 0
end
while j < n or goon
do
local i,maxv = doclassify.argmax(chart)
top[lut[i]] = maxv
chart[i] = 0.0
j=j+1
if maxv < th and goon then
goon = false
end
end
--for j=1,n do
-- local i,maxv = doclassify.argmax(chart)
-- top[lut[i]] = maxv
-- chart[i] = 0.0
--end
return top
end
doclassify.save_vectors = function(vectors, name)
local f = io.open(name,"w")
if f == nil then return false end
for id, v in pairs(vectors) do
f:write(id)
for i=1,#v do f:write(","..v[i]) end
f:write("\n")
end
f:close()
return true
end
doclassify.save_topchart = function(vectors, name,n)
local f = io.open(name,"w")
if f == nil then return false end
for k,v in pairs(vectors) do
local top = doclassify.top_similarity(k,vectors,n, 0.1)
for a,b in pairs(top) do
f:write(k.." "..a.." "..b.."\n")
end
end
f:close()
return true
end
doclassify.kmean = function(nclass, documents, maxstep, ids)
-- now
local vectors, maxv, size = doclassify.get_vectors(documents)
-- random centroids
local centroids = {}
local old_centroids = {}
local clusters = {}
--for pid in pairs(documents) do clusters[pid] = 0 end
-- add noise to mean_vector
for i = 1,nclass do
if ids == nil then
centroids[i] = doclassify.random(size,math.floor(maxv))
else
centroids[i] = vectors[ids[i]]
end
old_centroids[i] = doclassify.zeros(size)
end
-- loop until convergence or maxstep reached
local similarity = doclassify.mean_similarity(centroids, old_centroids)
local step = maxstep
while 1.0-similarity > 1e-9 and step > 0 do
clusters = {}
--echo(JSON.encode(centroids))
for id,v in pairs(vectors) do
local similarities = doclassify.similarities(v, centroids)
--echo(JSON.encode(similarities))
local cluster, maxvalue = doclassify.argmax(similarities)
--echo("doc #"..id.." is in clusters #"..cluster.." max value is "..maxvalue)
clusters[id] = cluster
end
-- storing the old centroids
old_centroids = centroids
-- calculate new centroids
local new_centroids = {}
for class in pairs(centroids) do
local cnt = 0
local cvectors = {}
for id,v in pairs(vectors) do
if clusters[id] == class then
cnt = cnt + 1
cvectors[cnt] = v
end
end
new_centroids[class] = doclassify.mean_vector(cvectors, size)
end
centroids = new_centroids
--echo(JSON.encode(centroids))
--echo(JSON.encode(old_centroids))
similarity = doclassify.mean_similarity(centroids, old_centroids)
echo("step #"..step..", similarity "..similarity)
step = step - 1
end
local results = {}
for i = 1,nclass do
local list = {}
local cnt = 0
for id,c in pairs(clusters) do
if c == i then
cnt = cnt + 1
list[cnt] = id
end
end
results[i] = list
end
return results, clusters, centroids
end
doclassify.zeros = function(n)
local vector = {}
for i = 1,n do vector[i] = 0.0 end
return vector
end
doclassify.random = function(n,maxv)
local vector = {}
for i=1,n do
vector[i] = math.random() + math.random(0, maxv)
end
return vector
end
doclassify.sum = function(v)
local sum = 0.0
for i=1,#v do sum = sum + v[i] end
return sum
end
doclassify.mean = function(v)
return doclassify.sum(v)/#v
end
doclassify.mean_vector = function(vectors, size)
local means = doclassify.zeros(size)
if not vectors or #vectors == 0 then return means end
--local size = #(vectors[1])
local times = 0
for k,v in pairs(vectors) do
for i=1,#v do means[i] = means[i] + v[i] end
times = times + 1
end
for i = 1,size do means[i] = means[i]/times end
return means
end
doclassify.argmin = function(v)
local minv = 0.0
local mini = 0.0
for i = 1,#v do
if v[i] <= minv then
mini = i
minv = v[i]
end
end
--echo("min index"..mini.." val "..minv)
return mini, minv
end
doclassify.argmax = function(v)
local maxv = 0.0
local maxi = 0.0
for i = 1,#v do
if v[i] >= maxv then
maxi = i
maxv = v[i]
end
end
return maxi,maxv
end
return doclassify

View File

@ -0,0 +1,151 @@
i
me
my
myself
we
our
ours
ourselves
you
your
yours
yourself
yourselves
he
him
his
himself
she
her
hers
herself
it
its
itself
they
them
their
theirs
themselves
what
which
who
whom
this
that
these
those
am
is
are
was
were
be
been
being
have
has
had
having
do
does
did
doing
a
an
the
and
but
if
or
because
as
until
while
of
at
by
for
with
about
against
between
into
through
during
before
after
above
below
to
from
up
down
in
out
on
off
over
under
again
further
then
once
here
there
when
where
why
how
all
any
both
each
few
more
most
other
some
such
no
nor
not
only
own
same
so
than
too
very
s
t
can
will
just
don
should
now
a
b
c
d
e
f
g
h
i
j
k
l
m
n
o
p
q
w
r
s
t
x
y
z

View File

@ -0,0 +1,50 @@
local path = require("fs/vfs").ospath("home://aiws/blog-clustering")
local gettext = loadfile(path.."/gettext.lua")()
local cluster = loadfile(path.."/cluster.lua")()
local refresh = false
local file = "/home/mrsang/test.csv"
if refresh then
local data = gettext.get({publish=1})
local documents = {}
if data then
local sw = gettext.stopwords("home://aiws/blog-clustering/stopwords.txt")
for k,v in pairs(data) do
local bag = cluster.bow(data[k].content, sw)
documents[data[k].id] = bag
end
cluster.tfidf(documents)
--local v = cluster.search("arm", documents)
--echo(JSON.encode(v))
local vectors, maxv, size = cluster.get_vectors(documents)
local s = cluster.save_topchart(vectors,file, 3)
if s then echo("file saved") else echo("error save file") end
--echo(JSON.encode(r))
--r = cluster.similarity(vectors["14"],vectors["16"])
--echo("Similarity "..r)
--local c,l = cluster.kmean(3, documents, 10)
--echo(JSON.encode(c))
--echo(JSON.encode(l))
else
echo("Data missing")
end
else
local f = io.open(file,"r")
local result = {}
for line in f:lines() do
local arr = {}
local cnt = 0
for i in line:gmatch( "%S+") do
cnt = cnt + 1
arr[cnt] = i
end
if not result[arr[1]] then result[arr[1]] = {} end
result[arr[1]][arr[2]] = tonumber(arr[3])
end
f:close()
echo(JSON.encode(result))
--local r = cluster.top_similarity("2",vectors, 3)
--echo(JSON.encode(r))
end

View File

@ -0,0 +1,72 @@
local data = ...
-- Michal Kottman, 2011, public domain
local socket = require 'socket'
local smtp = require 'socket.smtp'
local ssl = require 'ssl'
local https = require 'ssl.https'
local ltn12 = require 'ltn12'
function sslCreate()
local sock = socket.tcp()
return setmetatable({
connect = function(_, host, port)
local r, e = sock:connect(host, port)
if not r then return r, e end
sock = ssl.wrap(sock, {mode='client', protocol='tlsv1_2'})
return sock:dohandshake()
end
}, {
__index = function(t,n)
return function(_, ...)
return sock[n](sock, ...)
end
end
})
end
function sendMail(user, password, to,subject, body)
local msg = {
headers = {
from = string.format("%s <%s@iohub.dev>", user, user),
to = string.format("%s <%s>",to.text, to.email),
subject = subject
},
body = body
}
local ok, err = smtp.send {
from = string.format('<%s@iohub.dev>', user),
rcpt = string.format('<%s>', to.email),
source = smtp.message(msg),
user = string.format('%s@iohub.dev', user),
password = password,
server = 'iohub.dev',
port = 465,
create = sslCreate
}
if not ok then
return false, error
else
return true
end
end
local error_msg = {}
local iserror = false
for k,v in pairs(data.to) do
LOG_DEBUG("Send email to:"..v.email)
local r,e = sendMail(data.user, data.password, v, data.title, data.content)
if not r then
iserror = true
table.insert(error_msg, v.email)
LOG_ERROR(string.format("Unable to send mail to %s: %s",v.email, e))
end
end
local result = {}
result.error = iserror
result.result = error_msg
return result

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,14 +1,98 @@
{
"app":"Blogger",
"name":"Blogging application",
"description":"Backend manager for blogging",
"info":{
"app": "Blogger",
"name": "Blogging application",
"description": "Backend manager for blogging",
"info": {
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version":"0.2.9-a",
"category":"Internet",
"iconclass":"fa fa-book",
"dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r"],
"mimes":["none"]
"version": "0.2.13-a",
"category": "Internet",
"iconclass": "fa fa-book",
"dependencies": [
"SimpleMDE@2.18.0-r",
"Katex@0.11.1-r",
"SQLiteDB@0.1.0-a"
],
"mimes": [
"none"
],
"locales": {
"en_GB": {
"Pick a parent": "Pick a parent",
"Category name": "Category name",
"Ok": "Ok",
"Cancel": "Cancel",
"Title": "Title",
"Subtitle": "Subtitle",
"Location": "Location",
"From": "From",
"Save": "Save",
"Content": "Content",
"IO Hub mail username/password": "IO Hub mail username/password",
"Send": "Send",
"Please select a parent category": "Please select a parent category",
"Please enter category name": "Please enter category name",
"Parent can not be the category itself": "Parent can not be the category itself",
"Title or content must not be blank": "Title or content must not be blank",
"No email selected": "No email selected",
"Unable to send mail to: {0}": "Unable to send mail to: {0}",
"Error sending mail: {0}": "Error sending mail: {0}",
"Open/Create database": "Open/Create database",
"Open/create new database": "Open/create new database",
"Unable to init database file: {0}": "Unable to init database file: {0}",
"Select image file": "Select image file",
"Unable to get file": "Unable to get file",
"Add category": "Add category",
"cv-cat-add: {0}": "cv-cat-add: {0}",
"Edit category": "Edit category",
"cv-cat-edit: {0}": "cv-cat-edit: {0}",
"Delete category": "Delete category",
"Do you really want to delete: {0}?": "Do you really want to delete: {0}?",
"cv-cat-del: {0}": "cv-cat-del: {0}",
"Please select a category": "Please select a category",
"New section entry for {0}": "New section entry for {0}",
"cv-sec-add: {0}": "cv-sec-add: {0}",
"Please select a section to move": "Please select a section to move",
"Move to": "Move to",
"cv-sec-move: {0}": "cv-sec-move: {0}",
"Please select a section to edit": "Please select a section to edit",
"Modify section entry": "Modify section entry",
"cv-sec-edit: {0}": "cv-sec-edit: {0}",
"Cannot delete the section: {0}": "Cannot delete the section: {0}",
"New": "New",
"Cannot export file for embedding to text": "Cannot export file for embedding to text",
"Preview": "Preview",
"Send mail": "Send mail",
"No post selected": "No post selected",
"Emails sent": "Emails sent",
"Error sending mails: {0}": "Error sending mails: {0}",
"No record found for ID {}": "No record found for ID {}",
"Cannot fetch the entry content": "Cannot fetch the entry content",
"Delete a post": "Delete a post",
"Do you really want to delete this post ?": "Do you really want to delete this post ?",
"Cannot fetch user data": "Cannot fetch user data",
"Full name must be entered": "Full name must be entered",
"User data updated": "User data updated",
"Cannot save user data: {0}": "Cannot save user data: {0}",
"Unable to load categories": "Unable to load categories",
"Found {0} sections": "Found {0} sections",
"Please insert a title in the text: beginning with heading": "Please insert a title in the text: beginning with heading",
"Please enter tags": "Please enter tags",
"Cannot save blog: {0}": "Cannot save blog: {0}",
"No more record to load": "No more record to load",
"Full name": "Full name",
"Address": "Address",
"Phone": "Phone",
"Email": "Email",
"Url": "Url",
"Photo": "Photo",
"Short biblio": "Short biblio",
"Categories": "Categories",
"Load more": "Load more",
"Tags": "Tags",
"Created: {0}": "Created: {0}",
"Updated: {0}": "Updated: {0}"
}
}
}

View File

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

View File

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

View File

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

View File

@ -1,189 +0,0 @@
# Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
# AnTOS Web desktop is is licensed under the GNU General Public
# License v3.0, see the LICENCE file for more information
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# You should have received a copy of the GNU General Public License
#along with this program. If not, see https://www.gnu.org/licenses/.
class BloggerCategoryDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "BloggerCategoryDialog", BloggerCategoryDialog.scheme
main: () ->
super.main()
@tree = @find "tree"
@txtinput = @find "txtinput"
(@find "bt-ok").onbtclick = (e) =>
sel = @tree.selectedItem
return @notify __("Please select a parent category") unless sel
seldata = sel.data
val = @txtinput.value
return @notify __("Please enter category name") if val is "" and not @data.selonly
return @notify __("Parent can not be the category itself") if @data.cat and @data.cat.id is seldata.id
@handle { p: seldata, value: val } if @handle
@quit()
(@find "bt-cancel").onbtclick = (e) =>
@quit()
if @data and @data.tree
if @data and @data.cat
@txtinput.value = @data.cat.name
if @data.cat.pid is "0"
seldata = @data.tree
else
seldata = @findDataByID @data.cat.pid, @data.tree.nodes
seldata.selected = true if seldata
@tree.data = @data.tree
@tree.expandAll()
# TODO set selected category name
findDataByID: (id, list) ->
for data in list
return data if data.id is id
if data.nodes
@findDataByID id, data.nodes
return undefined
BloggerCategoryDialog.scheme = """
<afx-app-window width='300' height='400'>
<afx-vbox padding="5">
<afx-label text="__(Pick a parent)" data-height="25" class="lbl-header" ></afx-label>
<afx-tree-view data-id="tree" ></afx-tree-view>
<afx-label text="__(Category name)" data-height="25" class="lbl-header" ></afx-label>
<input type="text" data-height="25" data-id = "txtinput"/ >
<afx-hbox data-height = '35'>
<div style=' text-align:right;'>
<afx-button data-id = "bt-ok" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>
"""
# This dialog is use for cv section editing
class BloggerCVSectionDiaglog extends this.OS.GUI.BasicDialog
constructor: (parent) ->
file = "#{parent.meta().path}/cvsection.html".asFileHandle()
super "BloggerCVSectionDiaglog", file
main: () ->
super.main()
@editor = new EasyMDE
autoDownloadFontAwesome: false
element: @find "contentarea"
status: false
toolbar: false
($ (@select '[class = "CodeMirror-scroll"]')[0]).css "min-height", "50px"
($ (@select '[class="CodeMirror cm-s-paper CodeMirror-wrap"]')[0]).css "min-height", "50px"
inputs = @select "[input-class='user-input']"
(($ v).val @data.section[v.name] for v in inputs ) if @data and @data.section
@editor.value @data.section.content if @data and @data.section
(@find "section-publish").swon = (if @data and @data.section and Number(@data.section.publish) then true else false)
(@find "bt-cv-sec-save").onbtclick = (e) =>
data = {}
data[v.name] = ($ v).val() for v in inputs
data.content = @editor.value()
return @notify __("Title or content must not be blank") if data.title is "" and data.content is ""
#return @notify "Content must not be blank" if data.content is ""
data.id = @data.section.id if @data and @data.section
val = (@find "section-publish").swon
if val is true
data.publish = 1
else
data.publish = 0
@handle data if @handle
@quit()
@on "resize", () => @resizeContent()
@resizeContent()
resizeContent: () ->
container = @find "editor-container"
children = ($ ".EasyMDEContainer", container).children()
cheight = ($ container).height() - 30
($ children[0]).css("height", cheight + "px")
# this dialog is for send mail
class BloggerSendmailDiaglog extends this.OS.GUI.BasicDialog
constructor: (parent) ->
file = "#{parent.meta().path}/sendmail.html".asFileHandle()
super "BloggerSendmailDiaglog", file
main: () ->
super.main()
@subdb = new @.parent._api.DB("subscribers")
@maillinglist = @find "email-list"
title = (new RegExp "^#+(.*)\n", "g").exec @data.content
(@find "mail-title").value = title[1]
content = (@data.content.substring 0, 500) + "..."
(@find "contentarea").value = BloggerSendmailDiaglog.template.format @data.id, content
@subdb.find {}
.then (d) =>
for v in d
v.text = v.name
v.switch = true
v.checked = true
@maillinglist. items = d
.catch (e) =>
@error __("Cannot fetch subscribers data: {0}", e.toString()), e
(@find "bt-sendmail").onbtclick = (e) =>
items = @maillinglist.items
emails = []
for v in items
if v.checked is true
console.log v.email
emails.push v.email
return @notify __("No email selected") if emails.length is 0
# send the email
data =
path: "#{@parent.path()}/sendmail.lua",
parameters:
to: emails,
title: (@find "mail-title").value,
content: (@find "contentarea").value
@_api.apigateway data, false
.then (d) =>
return @notify __("Unable to send mail to: {0}", d.result.join(", ")) if d.error
@quit()
.catch (e) =>
console.log e
@error __("Error sending mail: {0}", e.toString()), e
BloggerSendmailDiaglog.template = """
Hello,
Xuan Sang LE has just published a new post on his blog: https://blog.lxsang.me/post/id/{0}
==========
{1}
==========
Read the full article via:
https://blog.lxsang.me/post/id/{0}
You receive this email because you have been subscribed to his blog.
Have a nice day,
Sent from Blogger, an AntOS application
"""

282
Blogger/dialogs.ts Normal file
View File

@ -0,0 +1,282 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// AnTOS Web desktop is is licensed under the GNU General Public
// License v3.0, see the LICENCE file for more information
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
//along with this program. If not, see https://www.gnu.org/licenses/.
namespace OS {
export namespace application {
export namespace blogger {
declare var EasyMDE;
export class BloggerCategoryDialog extends OS.GUI.BasicDialog {
private tree: OS.GUI.tag.TreeViewTag;
private txtinput: HTMLInputElement;
constructor() {
super("BloggerCategoryDialog", BloggerCategoryDialog.scheme);
}
main() {
super.main();
this.tree = this.find("tree") as OS.GUI.tag.TreeViewTag;
this.txtinput = this.find("txtinput") as HTMLInputElement;
(this.find("bt-ok") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
const sel = this.tree.selectedItem;
if (!sel) { return this.notify(__("Please select a parent category")); }
const seldata = sel.data;
const val = this.txtinput.value;
if ((val === "") && !this.data.selonly) { return this.notify(__("Please enter category name")); }
if (this.data.cat && (this.data.cat.id === seldata.id)) { return this.notify(__("Parent can not be the category itself")); }
if (this.handle) { this.handle({ p: seldata, value: val }); }
return this.quit();
};
(this.find("bt-cancel") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
return this.quit();
};
if (this.data && this.data.tree) {
if (this.data && this.data.cat) {
let seldata: GenericObject<any>;
this.txtinput.value = this.data.cat.name;
if (this.data.cat.pid === "0") {
seldata = this.data.tree;
} else {
seldata = this.findDataByID(this.data.cat.pid, this.data.tree.nodes);
}
if (seldata) { seldata.selected = true; }
}
this.tree.data = this.data.tree;
return this.tree.expandAll();
}
}
// TODO set selected category name
findDataByID(id: number, list: GenericObject<any>[]) {
for (let data of list) {
if (data.id === id) { return data; }
if (data.nodes) {
this.findDataByID(id, data.nodes);
}
}
return undefined;
}
}
BloggerCategoryDialog.scheme = `\
<afx-app-window width='300' height='400'>
<afx-vbox padding="5">
<afx-label text="__(Pick a parent)" data-height="25" class="lbl-header" ></afx-label>
<afx-tree-view data-id="tree" ></afx-tree-view>
<afx-label text="__(Category name)" data-height="25" class="lbl-header" ></afx-label>
<input type="text" data-height="25" data-id = "txtinput"/ >
<afx-hbox data-height = '35'>
<div style=' text-align:right;'>
<afx-button data-id = "bt-ok" text = "__(Ok)"></afx-button>
<afx-button data-id = "bt-cancel" text = "__(Cancel)"></afx-button>
</div>
</afx-hbox>
</afx-vbox>
</afx-app-window>\
`;
// This dialog is use for cv section editing
export class BloggerCVSectionDiaglog extends OS.GUI.BasicDialog {
private editor: GenericObject<any>;
constructor() {
super("BloggerCVSectionDiaglog");
}
main() {
super.main();
this.editor = new EasyMDE({
autoDownloadFontAwesome: false,
element: this.find("contentarea"),
status: false,
toolbar: false
});
($((this.select('[class = "CodeMirror-scroll"]'))[0])).css("min-height", "50px");
($((this.select('[class="CodeMirror cm-s-paper CodeMirror-wrap"]'))[0])).css("min-height", "50px");
const inputs = this.select("[input-class='user-input']");
if (this.data && this.data.section) { for (let v of inputs) { ($(v)).val(this.data.section[(v as HTMLInputElement).name]); } }
if (this.data && this.data.section) { this.editor.value(this.data.section.content); }
(this.find("section-publish") as OS.GUI.tag.SwitchTag).swon = (this.data && this.data.section && Number(this.data.section.publish) ? true : false);
(this.find("bt-cv-sec-save") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
const data: GenericObject<any> = {};
for (let v of inputs) { data[(v as HTMLInputElement).name] = ($(v)).val(); }
data.content = this.editor.value();
if ((data.title === "") && (data.content === "")) { return this.notify(__("Title or content must not be blank")); }
//return @notify "Content must not be blank" if data.content is ""
if (this.data && this.data.section) { data.id = this.data.section.id; }
const val = (this.find("section-publish") as OS.GUI.tag.SwitchTag).swon;
if (val === true) {
data.publish = 1;
} else {
data.publish = 0;
}
if (this.handle) { this.handle(data); }
return this.quit();
};
this.on("resize", () => this.resizeContent());
return this.resizeContent();
}
resizeContent() {
const container = this.find("editor-container");
const children = ($(".EasyMDEContainer", container)).children();
const cheight = ($(container)).height() - 30;
return ($(children[0])).css("height", cheight + "px");
}
}
BloggerCVSectionDiaglog.scheme = `\
<afx-app-window data-id = "blogger-cv-sec-win" apptitle="Porforlio section" width="450" height="400">
<afx-vbox padding="5">
<afx-hbox data-height = "30" >
<afx-label data-width= "70" text = "__(Title)"></afx-label>
<input type = "text" name="title" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(Subtitle)" data-width= "70"></afx-label>
<input type = "text" name="subtitle" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(Location)" data-width= "70"></afx-label>
<input type = "text" name="location" input-class = "user-input"></input>
</afx-hbox>
<afx-hbox data-height = "30" >
<afx-label text = "__(From)" data-width= "70"></afx-label>
<input type = "text" name="start" input-class = "user-input"></input>
<afx-label text = "To:" style="text-align:center;" data-width= "70"></afx-label>
<input type = "text" name="end" input-class = "user-input"></input>
</afx-hbox>
<afx-label data-height = "30" text = "Content" style = "margin-left:5px;"></afx-label>
<div data-id="editor-container">
<textarea name="content" data-id = "contentarea" ></textarea>
</div>
<div data-height = "35" style="text-align: right;">
<afx-switch data-id = "section-publish" data-width="30"></afx-switch>
<afx-button iconclass = "fa fa-save" data-id = "bt-cv-sec-save" text = "__(Save)"></afx-button>
</div>
</afx-vbox>
</afx-app-window>`;
// this dialog is for send mail
export class BloggerSendmailDiaglog extends OS.GUI.BasicDialog {
static template: string;
private maillinglist: OS.GUI.tag.StackMenuTag;
// TODO: convert to SQLite handle
private subdb: API.VFS.BaseFileHandle;
constructor() {
super("BloggerSendmailDiaglog");
}
main() {
super.main();
this.maillinglist = this.find("email-list") as OS.GUI.tag.StackMenuTag;
const title = (new RegExp("^#+(.*)\n", "g")).exec(this.data.content);
(this.find("mail-title") as HTMLInputElement).value = title[1];
const content = (this.data.content.substring(0, 500)) + "...";
(this.find("contentarea") as HTMLTextAreaElement).value = BloggerSendmailDiaglog.template.format(this.data.id, content);
const mlist = this.data.mails.map((el)=> {
return {
text: el.name,
email: el.email,
switch: true,
checked: true
}
});
console.log(mlist);
this.maillinglist.items = mlist;
return (this.find("bt-sendmail") as OS.GUI.tag.ButtonTag).onbtclick = (e: any) => {
const items = this.maillinglist.items;
const emails = [];
for (let v of items) {
if (v.checked === true) {
emails.push(v);
}
}
if (emails.length === 0) { return this.notify(__("No email selected")); }
// send the email
const data = {
path: `${this.meta().path}/api/sendmail.lua`,
parameters: {
to: emails,
title: (this.find("mail-title") as HTMLInputElement).value,
content: (this.find("contentarea") as HTMLTextAreaElement).value,
user: (this.find("mail-user") as HTMLInputElement).value,
password: (this.find("mail-password") as HTMLInputElement).value,
}
};
return this._api.apigateway(data, false)
.then((d) => {
if (d.error) {
const str = d.result.join(',');
return this.notify(__("Unable to send mail to: {0}", str)); }
return this.quit();
}).catch((e) => {
return this.error(__("Error sending mail: {0}", e.toString()), e);
});
};
}
}
BloggerSendmailDiaglog.scheme = `\
<afx-app-window data-id = "blogger-send-mail-win" apptitle="Send mail" width="600" height="400" resizable = "false">
<afx-hbox padding="5">
<afx-stack-menu data-width="200" data-id="email-list"></afx-stack-menu>
<afx-resizer data-width="3"></afx-resizer>
<afx-vbox >
<afx-label data-height="20" text = "__(Title)"></afx-label>
<input type = "text" data-height="25" name="title" data-id = "mail-title"></input>
<afx-label data-height = "20" text = "__(Content)" ></afx-label>
<textarea name="content" data-id = "contentarea" ></textarea>
<afx-label data-height="20" text = "__(IO Hub mail username/password)"></afx-label>
<afx-hbox data-height="25">
<input type = "text" name="username" data-id = "mail-user"></input>
<input type = "password" name="password" data-id = "mail-password"></input>
</afx-hbox>
<afx-hbox data-height = "30">
<div></div>
<afx-button iconclass = "fa fa-paper-plane" data-id = "bt-sendmail" data-width="content" text = "__(Send)"></afx-button>
</afx-hbox>
</afx-vbox>
</afx-hbox>
</afx-app-window>`;
BloggerSendmailDiaglog.template = `\
Hello,
Dany LE has just published a new post on his blog: https://blog.iohub.dev/post/id/{0}
==========
{1}
==========
Read the full article via:
https://blog.iohub.dev/post/id/{0}
You receive this email because you have been subscribed to his blog.
Have a nice day,
Sent from Blogger, an AntOS application\
`;
}
}
}

View File

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

925
Blogger/main.ts Normal file
View File

@ -0,0 +1,925 @@
// Copyright 2017-2018 Xuan Sang LE <xsang.le AT gmail DOT com>
// AnTOS Web desktop is is licensed under the GNU General Public
// License v3.0, see the LICENCE file for more information
// This program is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of
// the License, or (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
// You should have received a copy of the GNU General Public License
//along with this program. If not, see https://www.gnu.org/licenses/.
namespace OS {
export namespace application {
declare function renderMathInElement(el: HTMLElement):void;
declare var EasyMDE;
export class Blogger extends BaseApplication {
private user: GenericObject<any>;
private cvlist: GUI.tag.TreeViewTag;
private inputtags: HTMLInputElement;
private bloglist: GUI.tag.ListViewTag;
private seclist: GUI.tag.ListViewTag;
private tabcontainer: GUI.tag.TabContainerTag;
private editor: GenericObject<any>;
private previewOn: boolean;
// datatbase objects
private dbhandle: API.VFS.BaseFileHandle;
// database handles
private userdb: API.VFS.BaseFileHandle;
private cvcatdb: API.VFS.BaseFileHandle;
private cvsecdb: API.VFS.BaseFileHandle;
private blogdb: API.VFS.BaseFileHandle;
private subdb: API.VFS.BaseFileHandle;
private last_ctime: number;
constructor(args: any) {
super("Blogger", args);
this.previewOn = false;
}
private async init_db() {
try {
const f = await this.openDialog("FileDialog",{
title: __("Open/create new database"),
file: "Untitled.db"
});
var d_1 = f.file.path.asFileHandle();
if(f.file.type==="file") {
d_1=d_1.parent();
}
const target=`${d_1.path}/${f.name}`.asFileHandle();
this.dbhandle=`sqlite://${target.genealogy.join("/")}`.asFileHandle();
const tables = await this.dbhandle.read();
/**
* Init following tables if not exist:
* - user
* - cvcat
* - cvsec
* - blogdb
*/
if(!tables.user)
{
this.dbhandle.cache = {
address: "TEXT",
Phone: "TEXT",
shortbiblio: "TEXT",
fullname: "TEXT",
email: "TEXT",url: "TEXT",
photo: "TEXT"
}
const r = await this.dbhandle.write("user");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.cv_cat)
{
this.dbhandle.cache = {
publish: "NUMERIC",
name: "TEXT",
pid: "NUMERIC"
}
const r = await this.dbhandle.write("cv_cat");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.cv_sections)
{
this.dbhandle.cache = {
title: "TEXT",
start: "NUMERIC",
location: "TEXT",
end: "NUMERIC",
content: "TEXT",
subtitle: "TEXT",
publish: "NUMERIC",
cid: "NUMERIC"
}
const r = await this.dbhandle.write("cv_sections");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.blogs)
{
this.dbhandle.cache = {
tags: "TEXT",
content: "TEXT",
utime: "NUMERIC",
rendered: "TEXT",
title: "TEXT",
utimestr: "TEXT",
ctime: "NUMERIC",
ctimestr: "TEXT",
publish: "INTEGER DEFAULT 0",
}
const r = await this.dbhandle.write("blogs");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.st_similarity)
{
this.dbhandle.cache = {
pid: "NUMERIC",
sid: "NUMERIC",
score: "NUMERIC"
}
const r = await this.dbhandle.write("st_similarity");
if(r.error)
{
throw new Error(r.error as string);
}
}
if(!tables.subscribers)
{
this.dbhandle.cache = {
name: "TEXT",
email: "TEXT"
}
const r = await this.dbhandle.write("subscribers");
if(r.error)
{
throw new Error(r.error as string);
}
}
this.userdb = `${this.dbhandle.path}@user`.asFileHandle();
this.cvcatdb = `${this.dbhandle.path}@cv_cat`.asFileHandle();
this.cvsecdb = `${this.dbhandle.path}@cv_sections`.asFileHandle();
this.blogdb = `${this.dbhandle.path}@blogs`.asFileHandle();
this.subdb = `${this.dbhandle.path}@subscribers`.asFileHandle();
this.last_ctime = 0;
this.bloglist.data = [];
this.loadBlogs();
}
catch(e) {
this.error(__("Unable to init database file: {0}",e.toString()),e);
this.dbhandle = undefined;
}
}
menu() {
return [
{
text: "__(Open/Create database)",
onmenuselect: (e) => {
this.init_db();
}
}
];
}
main() {
this.user = {};
this.cvlist = this.find("cv-list") as GUI.tag.TreeViewTag;
this.cvlist.ontreeselect = (d) => {
if (!d) { return; }
const {
data
} = d.data.item;
return this.CVSectionByCID(Number(data.id));
};
this.inputtags = this.find("input-tags") as HTMLInputElement;
this.bloglist = this.find("blog-list") as GUI.tag.ListViewTag;
this.seclist = this.find("cv-sec-list") as GUI.tag.ListViewTag;
let el = this.find("photo") as HTMLInputElement;
$(el)
.on("click", async (e: any) => {
try {
const ret = await this.openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
});
return el.value = ret.file.path;
} catch (e) {
return this.error(__("Unable to get file"), e);
}
});
this.tabcontainer = this.find("tabcontainer") as GUI.tag.TabContainerTag;
this.tabcontainer.ontabselect = (e) => {
return this.fetchData((e.data.container as GUI.tag.TileLayoutTag).aid);
};
(this.find("bt-user-save") as GUI.tag.ButtonTag).onbtclick = (e: any) => {
return this.saveUser();
};
(this.find("blog-load-more") as GUI.tag.ButtonTag).onbtclick = (e) => {
this.loadBlogs();
}
(this.find("cv-cat-add") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const tree = await this.fetchCVCat();
const d = await this.openDialog(new blogger.BloggerCategoryDialog(), {
title: __("Add category"),
tree
});
this.cvcatdb.cache = {
name: d.value,
pid: d.p.id,
publish: 1
};
const r = await this.cvcatdb.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.refreshCVCat();
}
catch(e)
{
this.error(__("cv-cat-add: {0}", e.toString()), e);
}
};
(this.find("cv-cat-edit") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.cvlist.selectedItem;
if (!sel) { return; }
const cat = sel.data;
if (!cat) { return; }
const tree = await this.fetchCVCat();
const d = await this.openDialog(new blogger.BloggerCategoryDialog(), {
title: __("Edit category"),
tree, cat
});
const handle = cat.$vfs;
handle.cache = {
id: cat.id,
publish: cat.publish,
pid: d.p.id,
name: d.value
};
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.refreshCVCat();
}
catch(e)
{
this.error(__("cv-cat-edit: {0}", e.toString()), e);
}
};
(this.find("cv-cat-del") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.cvlist.selectedItem;
if (!sel) { return; }
const cat = sel.data;
if (!cat) { return; }
const d = await this.openDialog("YesNoDialog", {
title: __("Delete category"),
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", cat.name)
});
if (!d) { return; }
await this.deleteCVCat(cat);
}
catch(e)
{
this.error(__("cv-cat-del: {0}", e.toString()), e);
}
};
(this.find("cv-sec-add") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try
{
const sel = this.cvlist.selectedItem;
if (!sel) { return; }
const cat = sel.data;
if (!cat || (cat.id === "0")) { return this.toast(__("Please select a category")); }
const d = await this.openDialog(new blogger.BloggerCVSectionDiaglog(), {
title: __("New section entry for {0}", cat.name)
});
d.cid = Number(cat.id);
d.start = Number(d.start);
d.end = Number(d.end);
this.cvsecdb.cache = d;
// d.publish = 1
const r = await this.cvsecdb.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.CVSectionByCID(Number(cat.id));
}
catch(e)
{
this.error(__("cv-sec-add: {0}", e.toString()), e);
}
};
(this.find("cv-sec-move") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.seclist.selectedItem;
if (!sel) { return this.toast(__("Please select a section to move")); }
const sec = sel.data;
const handle = sec.$vfs;
console.log(handle);
const tree = await this.fetchCVCat();
const d = await this.openDialog(new blogger.BloggerCategoryDialog(), {
title: __("Move to"),
tree,
selonly: true
});
handle.cache = {
id: sec.id,
cid: d.p.id
};
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.CVSectionByCID(sec.cid);
this.seclist.unselect();
}
catch(e)
{
this.error(__("cv-sec-move: {0}", e.toString()), e);
}
};
(this.find("cv-sec-edit") as GUI.tag.ButtonTag).onbtclick = async (e: any) => {
try {
const sel = this.seclist.selectedItem;
if (!sel) { return this.toast(__("Please select a section to edit")); }
const sec = sel.data;
const d = await this.openDialog(new blogger.BloggerCVSectionDiaglog(), {
title: __("Modify section entry"),
section: sec
});
d.cid = Number(sec.cid);
d.start = Number(d.start);
d.end = Number(d.end);
const handle = sec.$vfs;
handle.cache = d;
//d.publish = Number sec.publish
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
await this.CVSectionByCID(Number(sec.cid));
}
catch(e)
{
this.error(__("cv-sec-edit: {0}", e.toString()), e);
}
};
this.seclist.onitemclose = (evt) => {
if (!evt) { return; }
const data = evt.data.item.data;
this.openDialog("YesNoDialog", {
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete: {0}?", data.title)
}).then(async (b: any) => {
if (!b) { return; }
try {
const r = await this.cvsecdb.remove({
where: {
id: data.id
}
});
if(r.error)
{
throw new Error(r.error as string);
}
return this.seclist.delete(evt.data.item);
} catch(e) {
return this.error(__("Cannot delete the section: {0}",e.toString()),e);
}
});
return false;
};
this.editor = new EasyMDE({
element: this.find("markarea"),
autoDownloadFontAwesome: false,
autofocus: true,
tabSize: 4,
indentWithTabs: true,
toolbar: [
{
name: __("New"),
className: "fa fa-file",
action: (e: any) => {
this.bloglist.unselect();
return this.clearEditor();
}
},
{
name: __("Save"),
className: "fa fa-save",
action: (e: any) => {
return this.saveBlog();
}
}
, "|", "bold", "italic", "heading", "|", "quote", "code",
"unordered-list", "ordered-list", "|", "link",
"image", "table", "horizontal-rule",
{
name: "image",
className: "fa fa-file-image-o",
action: (_) => {
return this.openDialog("FileDialog", {
title: __("Select image file"),
mimes: ["image/.*"]
}).then((d) => {
return d.file.path.asFileHandle().publish()
.then((r: { result: any; }) => {
const doc = this.editor.codemirror.getDoc();
return doc.replaceSelection(`![](${this._api.handle.shared}/${r.result})`);
}).catch((e: any) => this.error(__("Cannot export file for embedding to text"), e));
});
}
},
{
name: "Youtube",
className: "fa fa-youtube",
action: (e: any) => {
const doc = this.editor.codemirror.getDoc();
return doc.replaceSelection("[[youtube:]]");
}
},
"|",
{
name: __("Preview"),
className: "fa fa-eye no-disable",
action: (e: any) => {
this.previewOn = !this.previewOn;
EasyMDE.togglePreview(e);
///console.log @select ".editor-preview editor-preview-active"
renderMathInElement(this.find("editor-container"));
}
},
"|",
{
name: __("Send mail"),
className: "fa fa-paper-plane",
action: async (e: any) => {
try {
const d = await this.subdb.read();
const sel = this.bloglist.selectedItem;
if (!sel) { return this.error(__("No post selected")); }
const data = sel.data;
await this.openDialog(new blogger.BloggerSendmailDiaglog(), {
title: __("Send mail"),
content: this.editor.value(),
mails: d,
id: data.id
});
this.toast(__("Emails sent"));
}
catch(e)
{
this.error(__("Error sending mails: {0}", e.toString()), e);
}
}
},
"|",
{
name: __("TFIDF analyse"),
className: "fa fa-area-chart",
action: async (e: any) => {
try {
const q = await this.openDialog("PromptDialog",{
title: __("TFIDF Analyse"),
text: __("Max number of related posts to keep per post?"),
value: "5"
});
const data = {
path: `${this.meta().path}/api/ai/analyse.lua`,
parameters: {
dbpath: this.dbhandle.info.file.path,
top: parseInt(q)
}
};
const d = await this._api.apigateway(data, false);
if (d.error) {
throw new Error(d.error);
}
this.toast(d.result);
}
catch(e)
{
this.error(__("Error analysing posts: {0}", e.toString()), e);
}
}
}
]
});
this.bloglist.onlistselect = async (e: any) => {
const el = this.bloglist.selectedItem;
if (!el) { return; }
const sel = el.data;
if (!sel) { return; }
try {
const result=await this.blogdb.read({
where: {
id: Number(sel.id)
}
});
if(!result || result.length == 0)
{
throw new Error(__("No record found for ID {}", sel.id).__());
}
const r = result[0];
this.editor.value(r.content);
this.inputtags.value=r.tags;
return (this.find("blog-publish") as GUI.tag.SwitchTag).swon=Number(r.publish)? true:false;
}
catch(e_1) {
return this.error(__("Cannot fetch the entry content"),e_1);
}
};
this.bloglist.onitemclose = (e) => {
if (!e) { return; }
const el = e.data.item;
const data = el.data;
this.openDialog("YesNoDialog", {
title: __("Delete a post"),
iconclass: "fa fa-question-circle",
text: __("Do you really want to delete this post ?")
}).then(async (b: any) => {
if (!b) { return; }
const r = await this.blogdb.remove({
where: {
id: Number(data.id)
}
});
if(r.error)
{
throw new Error(r.error as string);
}
this.bloglist.delete(el);
this.bloglist.unselect();
return this.clearEditor();
});
return false;
};
this.bindKey("CTRL-S", () => {
const sel = this.tabcontainer.selectedTab;
if (!sel || ((sel.container as GUI.tag.TileLayoutTag).aid !== "blog-container")) { return; }
return this.saveBlog();
});
this.on("resize", () => {
return this.resizeContent();
});
this.resizeContent();
return this.init_db();
}
// @fetchData 0
// USER TAB
private fetchData(idx: any) {
switch (idx) {
case "user-container": //user info
return this.userdb.read()
.then((d) => {
if(!d || d.length == 0)
{
return;
}
this.user = d[0];
const inputs = this.select("[input-class='user-input']");
return inputs.map((i,v) => ($(v)).val(this.user[(v as HTMLInputElement).name]));
}).catch((e: any) => this.error(__("Cannot fetch user data"), e));
case "cv-container": // category
return this.refreshCVCat();
default:
this.last_ctime = 0;
this.bloglist.data = [];
return this.loadBlogs();
}
}
private async saveUser() {
try {
const inputs = this.select("[input-class='user-input']");
for (let v of inputs) { this.user[(v as HTMLInputElement).name] = ($(v)).val(); }
if (!this.user.fullname || (this.user.fullname === "")) { return this.toast(__("Full name must be entered")); }
//console.log @user
let fp = this.userdb;
if(this.user && this.user.id)
{
fp = `${this.userdb.path}@${this.user.id}`.asFileHandle();
}
fp.cache = this.user
const r = await fp.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
if(!this.user.id)
{
this.user.id = r.result;
}
this.toast(__("User data updated"));
}
catch(e)
{
this.error(__("Cannot save user data: {0}", e.toString()), e);
}
}
// PORFOLIO TAB
private refreshCVCat() {
return this.fetchCVCat().then((data: any) => {
this.cvlist.data = data;
return this.cvlist.expandAll();
}).catch((e: any) => this.error(__("Unable to load categories"), e));
}
private fetchCVCat() {
return new Promise(async (resolve, reject) => {
try {
const data = {
text: "Porfolio",
id: 0,
nodes: []
};
const filter = {
order: ["name$asc"]
};
const d = await this.cvcatdb.read(filter);
this.catListToTree(d, data, 0);
resolve(data);
}
catch(e)
{
reject(__e(e));
}
});
}
//it = (@cvlist.find "pid", "2")[0]
//@cvlist.set "selectedItem", it
private catListToTree(table: GenericObject<any>[], data: GenericObject<any>, id: number) {
const result = table.filter((e) => {
return e.pid == id
});
if (result.length === 0) {
return data.nodes = null;
}
for(let v of result)
{
v.nodes = [];
v.text = v.name;
this.catListToTree(table, v, v.id);
data.nodes.push(v);
}
}
private deleteCVCat(cat: GenericObject<any>): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
let v: any;
const ids = [];
var func = function (c: GenericObject<any>) {
ids.push(c.id);
if (c.nodes) {
c.nodes.map((v) => func(v));
}
};
func(cat);
// delete all content
let r = await this.cvsecdb.remove({
where: {
$or: {
cid: ids
}
}
});
if(r.error)
{
throw new Error(r.error as string);
}
r = await this.cvcatdb.remove({
where: {
$or: {
id: ids
}
}
});
if(r.error)
{
throw new Error(r.error as string);
}
await this.refreshCVCat();
this.seclist.data = [];
}
catch(e)
{
reject(__e(e));
}
});
}
private CVSectionByCID(cid: number): Promise<any> {
return new Promise( async (resolve, reject)=> {
try {
const d = await this.cvsecdb.read({
where: {cid},
order: [ "start$desc" ]
});
const items = [];
(this.find("cv-sec-status") as GUI.tag.LabelTag).text = __("Found {0} sections", d.length);
for (let v of d) {
v.closable = true;
v.tag = "afx-blogger-cvsection-item";
v.start = Number(v.start);
v.end = Number(v.end);
if (v.start < 1000) { v.start = undefined; }
if (v.end < 1000) { v.end = undefined; }
items.push(v);
}
this.seclist.data = items;
}
catch(e)
{
reject(__e(e));
}
});
}
// blog
private async saveBlog() {
try {
let sel = undefined;
const selel = this.bloglist.selectedItem;
if (selel) { sel = selel.data; }
const tags = this.inputtags.value;
const content = this.editor.value();
const title = (new RegExp("^#+(.*)\n", "g")).exec(content);
if (!title || (title.length !== 2)) { return this.toast(__("Please insert a title in the text: beginning with heading")); }
if (tags === "") { return this.toast(__("Please enter tags")); }
const d = new Date();
const data: GenericObject<any> = {
content,
title: title[1].trim(),
tags,
ctime: sel ? sel.ctime : d.timestamp(),
ctimestr: sel ? sel.ctimestr : d.toString(),
utime: d.timestamp(),
utimestr: d.toString(),
rendered: this.process(this.editor.options.previewRender(content)),
publish: (this.find("blog-publish") as GUI.tag.SwitchTag).swon ? 1 : 0
};
let handle = this.blogdb;
if (sel) {
data.id = sel.id;
handle = sel.$vfs;
}
//save the data
handle.cache = data;
const r = await handle.write(undefined);
if(r.error)
{
throw new Error(r.error as string);
}
if(!sel)
{
this.last_ctime = 0;
this.bloglist.data = [];
await this.loadBlogs();
}
else
{
//data.text = data.title;
selel.data.utime = data.utime;
selel.data.utimestr = data.utimestr;
selel.data = selel.data;
}
}
catch(e)
{
this.error(__("Cannot save blog: {0}", e.toString()), e);
}
}
private process(text: string) {
// find video tag and rendered it
let found: any;
const embed = (id: any) => `\
<iframe
class = "embeded-video"
width="560" height="315"
src="https://www.youtube.com/embed/${id}"
frameborder="0" allow="encrypted-media" allowfullscreen
></iframe>\
`;
const re = /\[\[youtube:([^\]]*)\]\]/g;
const replace = [];
while ((found = re.exec(text)) !== null) {
replace.push(found);
}
if (!(replace.length > 0)) { return text; }
let ret = "";
let begin = 0;
for (let it of replace) {
ret += text.substring(begin, it.index);
ret += embed(it[1]);
begin = it.index + it[0].length;
}
ret += text.substring(begin, text.length);
//console.log ret
return ret;
}
private clearEditor() {
this.editor.value("");
this.inputtags.value = "";
return (this.find("blog-publish") as GUI.tag.SwitchTag).swon = false;
}
// load blog
private loadBlogs(): Promise<any> {
return new Promise( async (ok, reject)=> {
try {
const filter: GenericObject<any> = {
order: ["ctime$desc"],
fields: [
"id",
"title",
"ctimestr",
"ctime",
"utime",
"utimestr"
],
limit: 10,
};
if(this.last_ctime)
{
filter.where = { ctime$lt: this.last_ctime};
}
const r = await this.blogdb.read(filter);
if(r.length == 0)
{
this.toast(__("No more record to load"));
return;
}
this.last_ctime = r[r.length - 1].ctime;
for (let v of r) {
v.tag = "afx-blogger-post-item";
this.bloglist.push(v);
}
this.clearEditor();
return this.bloglist.selected = -1;
}
catch(e)
{
reject(__e(e));
}
});
}
private resizeContent() {
const container = this.find("editor-container");
const children = ($(".EasyMDEContainer", container)).children();
const titlebar = (($(this.scheme)).find(".afx-window-top"))[0];
const toolbar = children[0];
const statusbar = children[3];
const cheight = ($(this.scheme)).height() - ($(titlebar)).height() - ($(toolbar)).height() - ($(statusbar)).height() - 90;
return ($(children[1])).css("height", cheight + "px");
}
}
Blogger.singleton = true;
Blogger.dependencies = [
"pkg://SimpleMDE/main.js",
"pkg://SimpleMDE/main.css",
"pkg://Katex/main.js",
"pkg://Katex/main.css",
"pkg://SQLiteDB/libsqlite.js",
];
}
}

View File

@ -1,14 +1,98 @@
{
"app":"Blogger",
"name":"Blogging application",
"description":"Backend manager for blogging",
"info":{
"app": "Blogger",
"name": "Blogging application",
"description": "Backend manager for blogging",
"info": {
"author": "Xuan Sang LE",
"email": "xsang.le@gmail.com"
},
"version":"0.2.9-a",
"category":"Internet",
"iconclass":"fa fa-book",
"dependencies": ["SimpleMDE@2.18.0-r","Katex@0.11.1-r"],
"mimes":["none"]
"version": "0.2.13-a",
"category": "Internet",
"iconclass": "fa fa-book",
"dependencies": [
"SimpleMDE@2.18.0-r",
"Katex@0.11.1-r",
"SQLiteDB@0.1.0-a"
],
"mimes": [
"none"
],
"locales": {
"en_GB": {
"Pick a parent": "Pick a parent",
"Category name": "Category name",
"Ok": "Ok",
"Cancel": "Cancel",
"Title": "Title",
"Subtitle": "Subtitle",
"Location": "Location",
"From": "From",
"Save": "Save",
"Content": "Content",
"IO Hub mail username/password": "IO Hub mail username/password",
"Send": "Send",
"Please select a parent category": "Please select a parent category",
"Please enter category name": "Please enter category name",
"Parent can not be the category itself": "Parent can not be the category itself",
"Title or content must not be blank": "Title or content must not be blank",
"No email selected": "No email selected",
"Unable to send mail to: {0}": "Unable to send mail to: {0}",
"Error sending mail: {0}": "Error sending mail: {0}",
"Open/Create database": "Open/Create database",
"Open/create new database": "Open/create new database",
"Unable to init database file: {0}": "Unable to init database file: {0}",
"Select image file": "Select image file",
"Unable to get file": "Unable to get file",
"Add category": "Add category",
"cv-cat-add: {0}": "cv-cat-add: {0}",
"Edit category": "Edit category",
"cv-cat-edit: {0}": "cv-cat-edit: {0}",
"Delete category": "Delete category",
"Do you really want to delete: {0}?": "Do you really want to delete: {0}?",
"cv-cat-del: {0}": "cv-cat-del: {0}",
"Please select a category": "Please select a category",
"New section entry for {0}": "New section entry for {0}",
"cv-sec-add: {0}": "cv-sec-add: {0}",
"Please select a section to move": "Please select a section to move",
"Move to": "Move to",
"cv-sec-move: {0}": "cv-sec-move: {0}",
"Please select a section to edit": "Please select a section to edit",
"Modify section entry": "Modify section entry",
"cv-sec-edit: {0}": "cv-sec-edit: {0}",
"Cannot delete the section: {0}": "Cannot delete the section: {0}",
"New": "New",
"Cannot export file for embedding to text": "Cannot export file for embedding to text",
"Preview": "Preview",
"Send mail": "Send mail",
"No post selected": "No post selected",
"Emails sent": "Emails sent",
"Error sending mails: {0}": "Error sending mails: {0}",
"No record found for ID {}": "No record found for ID {}",
"Cannot fetch the entry content": "Cannot fetch the entry content",
"Delete a post": "Delete a post",
"Do you really want to delete this post ?": "Do you really want to delete this post ?",
"Cannot fetch user data": "Cannot fetch user data",
"Full name must be entered": "Full name must be entered",
"User data updated": "User data updated",
"Cannot save user data: {0}": "Cannot save user data: {0}",
"Unable to load categories": "Unable to load categories",
"Found {0} sections": "Found {0} sections",
"Please insert a title in the text: beginning with heading": "Please insert a title in the text: beginning with heading",
"Please enter tags": "Please enter tags",
"Cannot save blog: {0}": "Cannot save blog: {0}",
"No more record to load": "No more record to load",
"Full name": "Full name",
"Address": "Address",
"Phone": "Phone",
"Email": "Email",
"Url": "Url",
"Photo": "Photo",
"Short biblio": "Short biblio",
"Categories": "Categories",
"Load more": "Load more",
"Tags": "Tags",
"Created: {0}": "Created: {0}",
"Updated: {0}": "Updated: {0}"
}
}
}

View File

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

View File

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

View File

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

104
Blogger/tags.ts Normal file
View File

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

Binary file not shown.

Binary file not shown.

View File

@ -3,5 +3,7 @@ A simple yet powerful code/text editor.
CodePad is a text editor based on the ACE editor.
## Change logs
- v0.1.8-a: fix resizer bug on new UI API
- v0.1.7-a: fix setting bug using new AntOS setting API
- v0.1.6-a: adapt to new AntOS v2.0.x
- v0.1.5-a: CodePad moved out of AntOS based system as regular AntOS package

View File

@ -19,7 +19,7 @@
<div data-id="right-editorarea"></div>
</afx-vbox>
</afx-hbox>
<afx-resizer data-height = "3" dir = "ve" attachnext = "true" ></afx-resizer>
<afx-resizer data-height = "3" attachnext = "true" ></afx-resizer>
<afx-tab-container data-id = "bottombar" data-height="150" min-height="150" tabbarheight= "35">
<afx-hbox tabname="__(Output)" iconclass = "fa fa-file-text" class = "bottom-tab-content">
<afx-button text = "" data-id="logger-clear" iconclass="fa fa-trash" data-width="21"></afx-button>

View File

@ -17,8 +17,10 @@
"data": ["build","build/debug","build/release"]
},
{
"name": "ts-import",
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",

View File

@ -3,5 +3,7 @@ A simple yet powerful code/text editor.
CodePad is a text editor based on the ACE editor.
## Change logs
- v0.1.8-a: fix resizer bug on new UI API
- v0.1.7-a: fix setting bug using new AntOS setting API
- v0.1.6-a: adapt to new AntOS v2.0.x
- v0.1.5-a: CodePad moved out of AntOS based system as regular AntOS package

File diff suppressed because one or more lines are too long

View File

@ -8,7 +8,7 @@
"email": "xsang.le@gmail.com",
"licences": "GPLv3"
},
"version":"0.1.6-b",
"version":"0.1.8-b",
"category":"Development",
"iconclass":"fa fa-pencil-square-o",
"dependencies": ["ACECore@1.4.12-r"],

View File

@ -19,7 +19,7 @@
<div data-id="right-editorarea"></div>
</afx-vbox>
</afx-hbox>
<afx-resizer data-height = "3" dir = "ve" attachnext = "true" ></afx-resizer>
<afx-resizer data-height = "3" attachnext = "true" ></afx-resizer>
<afx-tab-container data-id = "bottombar" data-height="150" min-height="150" tabbarheight= "35">
<afx-hbox tabname="__(Output)" iconclass = "fa fa-file-text" class = "bottom-tab-content">
<afx-button text = "" data-id="logger-clear" iconclass="fa fa-trash" data-width="21"></afx-button>

Binary file not shown.

View File

@ -8,7 +8,7 @@
"email": "xsang.le@gmail.com",
"licences": "GPLv3"
},
"version":"0.1.6-b",
"version":"0.1.8-b",
"category":"Development",
"iconclass":"fa fa-pencil-square-o",
"dependencies": ["ACECore@1.4.12-r"],

View File

@ -244,7 +244,7 @@ namespace OS {
{
text: __("Change theme"),
onmenuselect: async (
e: GUI.TagEventType<GUI.tag.MenuEventData>
e: GUI.TagEventType<GUI.tag.StackMenuEventData>
) => {
try{
const themes = this.eum.active.getThemes();
@ -263,7 +263,7 @@ namespace OS {
{
text: __("Change language mode"),
onmenuselect: async (
e: GUI.TagEventType<GUI.tag.MenuEventData>
e: GUI.TagEventType<GUI.tag.StackMenuEventData>
) => {
try{
const modes = this.eum.active.getModes().map(v => {
@ -284,7 +284,7 @@ namespace OS {
text: __("Build with AntOSDK"),
shortcut: " (CTRL-ALT-B)",
onmenuselect: async (
e: GUI.TagEventType<GUI.tag.MenuEventData>
e: GUI.TagEventType<GUI.tag.StackMenuEventData>
) => {
try{
this.build();
@ -364,7 +364,7 @@ namespace OS {
}
this.toggleSideBar();
this.toggleSplitMode();
this.applyAllSetting();
//this.applyAllSetting();
}
/**
@ -390,7 +390,7 @@ namespace OS {
this.sdk = new (API as any).AntOSDKBuilder(this.logger,"");
}
this.logger.clear();
this.showBottomBar(true);
this.setting.showBottomBar = true;
// check for meta file
const meta_file = `${this.currdir.path}/build.json`.asFileHandle();
const meta = await meta_file.read("json");
@ -454,7 +454,7 @@ namespace OS {
showOutput(toggle: boolean = false): void {
if (toggle)
this.showBottomBar(true);
this.setting.showBottomBar = true;
this.bottombar.selectedIndex = 0;
}
@ -479,7 +479,6 @@ namespace OS {
* @memberof CodePad
*/
public showBottomBar(v: boolean): void {
this.setting.showBottomBar = v;
if (v) {
$(this.bottombar).show();
}
@ -495,7 +494,7 @@ namespace OS {
* @memberof CodePad
*/
private toggleBottomBar(): void {
this.showBottomBar(!this.setting.showBottomBar);
this.setting.showBottomBar = !this.setting.showBottomBar;
}
private toggleSplitMode(): void {
@ -542,7 +541,7 @@ namespace OS {
dataid: "recent",
nodes: recent,
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>,
e: GUI.TagEventType<GUI.tag.StackMenuEventData>,
r: CodePad
) => {
const handle = e.data.item.data.text.asFileHandle();
@ -574,7 +573,7 @@ namespace OS {
},
],
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>,
e: GUI.TagEventType<GUI.tag.StackMenuEventData>,
r: CodePad
) => {
return this.menuAction(e.data.item.data.dataid, r);
@ -591,9 +590,9 @@ namespace OS {
* @memberof CodePad
*/
private ctxFileMenuHandle(
e: GUI.TagEventType<GUI.tag.MenuEventData>
e: GUI.TagEventType<GUI.tag.StackMenuEventData>
): void {
const el = e.data.item as GUI.tag.MenuEntryTag;
const el = e.data.item;
if (!el) {
return;
}
@ -845,7 +844,7 @@ namespace OS {
}
],
onchildselect: (
e: GUI.TagEventType<GUI.tag.MenuEventData>,
e: GUI.TagEventType<GUI.tag.StackMenuEventData>,
r: EditorFileHandle
) => {
switch (e.data.item.data.dataid) {

View File

@ -2,6 +2,8 @@
Simple PDF document manager
## Change logs
- v0.1.1-b: move PDF merge and document thumbnail generation to client side, remove server side lua script
- 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,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

@ -20,12 +20,12 @@
<afx-vbox>
<div data-id = "preview-container">
<canvas data-id="preview-canvas"></canvas>
<canvas data-id="tmp-canvas" style="display:none;"></canvas>
</div>
<afx-grid-view data-id="docgrid"></afx-grid-view>
<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,37 @@
}
]
},
"coffee": {
"locale": {
"require": ["locale"],
"jobs": [
{
"name":"locale-gen",
"data": {
"src": "",
"exclude": ["build/", "css/", "coffees/"],
"locale": "en_GB",
"dest": "package.json"
}
}
]
},
"ts": {
"require": [
"coffee"
"ts"
],
"jobs": [
{
"name": "coffee-compile",
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",
"data": {
"src": [
"coffees/dialogs.coffee",
"coffees/main.coffee"
"ts/dialogs.ts",
"ts/main.ts"
],
"dest": "build/debug/main.js"
}
@ -50,8 +70,8 @@
"data": {
"src": [
"assets/scheme.html",
"api/api.lua",
"package.json",
"css/main.css",
"README.md"
],
"dest": "build/debug"
@ -65,7 +85,7 @@
],
"depend": [
"init",
"coffee",
"ts",
"uglify",
"copy"
],
@ -78,6 +98,14 @@
}
}
]
},
"debug": {
"depend": [
"init",
"ts",
"copy"
]
}
}
}

View File

@ -2,6 +2,8 @@
Simple PDF document manager
## Change logs
- v0.1.1-b: move PDF merge and document thumbnail generation to client side, remove server side lua script
- 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,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,4 +1,3 @@
afx-app-window[data-id = "Docify"] .header .label-text
{
font-weight: bold;

File diff suppressed because one or more lines are too long

View File

@ -1,15 +1,70 @@
{
"pkgname": "Docify",
"app":"Docify",
"name":"Docify",
"description":"Docify",
"info":{
"author": "",
"email": ""
"app": "Docify",
"name": "Docify",
"description": "Simple document manager",
"info": {
"author": "Dany LE",
"email": "mrsang@iohub.dev"
},
"version":"0.0.9-b",
"category":"Office",
"iconclass":"bi bi-collection-fill",
"mimes":["none"],
"locale": {}
"version": "0.1.1-b",
"category": "Office",
"iconclass": "bi bi-collection-fill",
"mimes": [
"none"
],
"dependencies": [
"SQLiteDB@0.1.0-a",
"libpdfjs@2.6.347-r",
"PDFLib@1.17.1"
],
"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

@ -20,12 +20,12 @@
<afx-vbox>
<div data-id = "preview-container">
<canvas data-id="preview-canvas"></canvas>
<canvas data-id="tmp-canvas" style="display:none;"></canvas>
</div>
<afx-grid-view data-id="docgrid"></afx-grid-view>
<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

@ -1,15 +1,70 @@
{
"pkgname": "Docify",
"app":"Docify",
"name":"Docify",
"description":"Docify",
"info":{
"author": "",
"email": ""
"app": "Docify",
"name": "Docify",
"description": "Simple document manager",
"info": {
"author": "Dany LE",
"email": "mrsang@iohub.dev"
},
"version":"0.0.9-b",
"category":"Office",
"iconclass":"bi bi-collection-fill",
"mimes":["none"],
"locale": {}
"version": "0.1.1-b",
"category": "Office",
"iconclass": "bi bi-collection-fill",
"mimes": [
"none"
],
"dependencies": [
"SQLiteDB@0.1.0-a",
"libpdfjs@2.6.347-r",
"PDFLib@1.17.1"
],
"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>\
`;
}
}
}

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

@ -0,0 +1,701 @@
namespace OS {
export namespace application {
declare var pdfjsLib;
declare var PDFLib;
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 = ""; }
pdfjsLib.GlobalWorkerOptions.workerSrc = "pkg://libpdfjs/pdf.worker.js".asFileHandle().getlink();
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.merge_files(data);
data.file = r.path;
/*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());
}
// delete file
try {
await item.data.file.asFileHandle().remove();
const thumb = await this.get_thumb_path(item.data.file);
await thumb.asFileHandle().remove();
}
catch(e)
{
console.log(e);
}
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;
if(data.file.includes(item.data.file) && data.file.length == 1)
{
// nothing changes
data.file = item.data.file;
}
else
{
if(!data.file.includes(item.data.file))
{
// remove old file
try {
console.log("remove old file", item.data.file);
await item.data.file.asFileHandle().remove();
const thumb = await this.get_thumb_path(item.data.file);
await thumb.asFileHandle().remove();
}
catch(e)
{
console.log(e);
}
}
// merge all PDF file
const merged_file = await this.merge_files(data);
data.file = merged_file.path;
}
data.mtime = Math.floor(Date.now() / 1000);
const handle = item.data.$vfs;
handle.cache = data;
const d = await handle.write(undefined);
if(d.error)
{
throw new Error(d.error);
}
this.toast(__("Document updated"));
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 get_thumb_path(filepath: string| API.VFS.BaseFileHandle)
{
const path = filepath.asFileHandle().path;
let thumb_name = await this.sha1(path.replace(this.setting.docpath,""));
return `${this.setting.docpath}/cache/${thumb_name}.png`;
}
private async merge_files(data) {
const paths: string[] = data.file;
const cat = data.cid.toFixed(1).toString();
const dir = `${this.setting.docpath}/${cat}`.asFileHandle();
try{
await dir.onready();
}
catch(_)
{
const ret = await dir.parent().mk(cat);
if(ret.error)
{
throw new Error(ret.error.toString());
}
await dir.onready();
}
const des_file = `${dir.path}/${new Date(Date.now()).getTime().toString()}.pdf`.asFileHandle();
// concat the file
const pdfdoc = await PDFLib.PDFDocument.create();
let pages = [];
for(const path of paths)
{
const arr = await path.asFileHandle().read("binary");
const doc = await PDFLib.PDFDocument.load(arr, { ignoreEncryption: true });
const copiedpages = await pdfdoc.copyPages(doc, doc.getPageIndices());
pages = pages.concat(copiedpages);
}
for (let i = 0; i < pages.length; i++) {
await pdfdoc.insertPage(i, pages[i]);
}
const buffer = await pdfdoc.save();
des_file.cache = new Blob([buffer]);
const ret = await des_file.write("binary");
if(ret.error)
{
throw new Error(ret.error.toString());
}
// move thumb file
let src_tfile = await this.get_thumb_path(paths[0]);
const dest_tfile = await this.get_thumb_path(des_file);
try {
console.log("Move", src_tfile, "to", dest_tfile);
const ret = await src_tfile.asFileHandle().move(dest_tfile);
if(ret.error)
{
console.log(ret.error);
}
}
catch(e)
{
console.log(e);
}
// remove other file and thumb file
for(const path of paths)
{
try{
src_tfile = await this.get_thumb_path(path);
console.log("'Remove file", path, src_tfile);
let ret = await path.asFileHandle().remove();
if(ret.error)
{
console.log(ret);
}
ret = await src_tfile.asFileHandle().remove();
if(ret.error)
{
console.log(ret);
}
}
catch(e)
{
console.log(e);
}
}
return des_file;
}
private async update_doclist(cid: any) {
try
{
const d = await this.docdb.read({
where: {
cid: cid
},
order: ["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 = [];
}
private async sha1(str) {
const enc = new TextEncoder();
const hash = await crypto.subtle.digest('SHA-1', enc.encode(str));
return Array.from(new Uint8Array(hash))
.map(v => (v.toString(16) as any).padStart(2, '0'))
.join('');
}
private async genthumb(path: string)
{
/** try to search if the thumb file exists,
* if it does not exist, generate it using
* pdfjs library and an hidden canvas
*/
const tpath = await this.get_thumb_path(path);
const file = tpath.asFileHandle();
try {
await file.onready();
}
catch(e)
{
// generate thumb file
//data = await file.read("binary");
console.log("Try to generate thumb file for", path);
const canvas = this.find("tmp-canvas") as HTMLCanvasElement;
const context = canvas.getContext('2d');
const pdf = await pdfjsLib.getDocument(path.asFileHandle().getlink()).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 0.33});
// Support HiDPI-screens.
const outputScale = window.devicePixelRatio || 1;
canvas.width = Math.floor(viewport.width * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
canvas.style.width = Math.floor(viewport.width) + "px";
canvas.style.height = Math.floor(viewport.height) + "px";
const transform = outputScale !== 1
? [outputScale, 0, 0, outputScale, 0, 0]
: null;
const renderContext = {
canvasContext: context,
transform: transform,
viewport: viewport
};
await page.render(renderContext).promise;
const url = canvas.toDataURL('image/png');
file.cache = url;
const ret = await file.write("base64");
if(ret.error)
{
throw new Error(ret.error.toString());
}
await file.onready();
}
return file;
}
async preview(path: any, canvas: HTMLCanvasElement) {
try {
const file = await this.genthumb(path);
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;
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);
}
}
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",
"pkg://libpdfjs/pdf.js",
"pkg://PDFLib/main.js"
];
}
}

View File

@ -1,3 +1,6 @@
# GPClient
Simple Multi purpose client wrapper
## change logs
- allow clipboard read/write in iframe

View File

@ -1,5 +1,5 @@
<afx-app-window apptitle="__(Unknow client)" width="700" height="500" data-id="GPClient" blur-overlay="true">
<afx-hbox >
<iframe frameborder="0" data-id="container" src=""></iframe>
<iframe frameborder="0" data-id="container" src="" allow="clipboard-read; clipboard-write"></iframe>
</afx-hbox>
</afx-app-window>

View File

@ -3,6 +3,10 @@
"targets": {
"init": {
"jobs": [
{
"name": "vfs-rm_no_error",
"data": ["build/debug","build/release"]
},
{
"name": "vfs-mkdir",
"data": [

View File

@ -1,3 +1,6 @@
# GPClient
Simple Multi purpose client wrapper
## change logs
- allow clipboard read/write in iframe

View File

@ -1 +1 @@
(function(){var t,i,e;(t=class t extends this.OS.GUI.BasicDialog{constructor(){super("ClientDialog",t.scheme)}main(){var t,i,e,n;if(super.main(),e=$(this.scheme).find("input[type=text]"),this.data)for(i=0,n=e.length;i<n;i++)t=e[i],this.data[t.name]&&(t.value=this.data[t.name]);return this.find("btncancel").onbtclick=()=>this.quit(),this.find("btnok").onbtclick=i=>{var n,a,s;for(n={},a=0,s=e.length;a<s;a++){if(""===(t=e[a]).value)return this.notify(__("Please enter all the fields"));n[t.name]=t.value}return this.handle&&this.handle(n),this.quit()}}}).scheme='<afx-app-window width=\'400\' height=\'300\'>\n <afx-vbox padding="5">\n <afx-label data-height="25" text = "__(Client name)"></afx-label>\n <input type="text" name="text" data-height="30" ></input>\n <div data-height="5"></div>\n <afx-label data-height="25" text = "__(URL)"></afx-label>\n <input type="text" name="url" data-height="30" ></input>\n <div></div>\n <div data-height="35" style="text-align: right;">\n <afx-button data-id="btnok" text="__(Ok)"></afx-button>\n <afx-button data-id="btncancel" text="__(Cancel)"></afx-button>\n </div>\n </afx-vbox>\n</afx-app-window>',(i=class i extends this.OS.GUI.BasicDialog{constructor(){super("ClientListDialog",i.scheme)}main(){return super.main(),this.clist=this.find("client-list"),this.clist.buttons=[{text:"",iconclass:"fa fa-plus-circle",onbtclick:i=>this.openDialog(new t,{title:__("Add new client")}).then(t=>(this.parent.setting.clients.push(t),this.clist.data=this.parent.setting.clients))},{text:"",iconclass:"fa fa-minus-circle",onbtclick:t=>{var i,e;if(e=this.clist.selectedItem,i=this.clist.selected,e)return this.ask({text:__("Do you realy want to delete: `{0}`",e.data.text)}).then(t=>{if(t)return this.parent.setting.clients.splice(i,1),this.clist.data=this.parent.setting.clients})}},{text:"",iconclass:"fa fa-pencil-square-o",onbtclick:i=>{var e;if(e=this.clist.selectedItem)return this.openDialog(new t,{title:__("Add new client"),text:e.data.text,url:e.data.url}).then(t=>{if(t)return e.data.text=t.text,e.data.url=t.url,this.clist.data=this.parent.setting.clients})}}],this.find("btnswitch").onbtclick=t=>{var i;if(i=this.clist.selectedItem)return this.parent.setting.curl=i.data.url,this.parent.setting.cname=i.data.text,this.parent.switchClient(),this.quit()},this.clist.data=this.parent.setting.clients}}).scheme='<afx-app-window width=\'400\' height=\'300\'>\n <afx-vbox padding="5">\n <afx-list-view data-id="client-list"></afx-list-view>\n <div data-height="35" style="text-align: right;">\n <afx-button text="__(Switch client)" data-id="btnswitch"></afx-button>\n <div>\n </afx-vbox>\n</afx-app-window>',(e=class extends this.OS.application.BaseApplication{constructor(t){super("GPClient",t)}main(){return this.setting.clients||(this.setting.clients=[]),this.container=this.find("container"),this.bindKey("CTRL-M",()=>this.openDialog(new i,{title:__("Client Manager")})),this.switchClient()}switchClient(){return this.setting.curl?(this.container.src=this.setting.curl,this.scheme.apptitle=this.setting.cname):this.notify(__("No client selected, manager client in menu Options > Client manager"))}menu(){return[{text:"__(Options)",nodes:[{text:"__(Client manager)",shortcut:"C-M"}],onchildselect:t=>this.openDialog(new i,{title:__("Client Manager")})}]}}).singleton=!0,this.OS.register("GPClient",e)}).call(this);
(function(){var t,e,i;(t=class t extends this.OS.GUI.BasicDialog{constructor(){super("ClientDialog",t.scheme)}main(){var t,e,i,n;if(super.main(),e=$(this.scheme).find("input[type=text]"),this.data)for(i=0,n=e.length;i<n;i++)t=e[i],this.data[t.name]&&(t.value=this.data[t.name]);return this.find("btncancel").onbtclick=()=>this.quit(),this.find("btnok").onbtclick=i=>{var n,s,a;for(n={},s=0,a=e.length;s<a;s++){if(""===(t=e[s]).value)return this.notify(__("Please enter all the fields"));n[t.name]=t.value}return this.handle&&this.handle(n),this.quit()}}}).scheme='<afx-app-window width=\'400\' height=\'300\'>\n <afx-vbox padding="5">\n <afx-label data-height="25" text = "__(Client name)"></afx-label>\n <input type="text" name="text" data-height="30" ></input>\n <div data-height="5"></div>\n <afx-label data-height="25" text = "__(URL)"></afx-label>\n <input type="text" name="url" data-height="30" ></input>\n <div></div>\n <div data-height="35" style="text-align: right;">\n <afx-button data-id="btnok" text="__(Ok)"></afx-button>\n <afx-button data-id="btncancel" text="__(Cancel)"></afx-button>\n </div>\n </afx-vbox>\n</afx-app-window>',(e=class e extends this.OS.GUI.BasicDialog{constructor(){super("ClientListDialog",e.scheme)}refresh_list(){return this.clist.data=this.parent.setting.clients.map(t=>({text:t.text,url:t.url}))}main(){return super.main(),this.clist=this.find("client-list"),this.clist.buttons=[{text:"",iconclass:"fa fa-plus-circle",onbtclick:e=>this.openDialog(new t,{title:__("Add new client")}).then(t=>(console.log(t),this.parent.setting.clients.push(t),this.clist.data=this.parent.setting.clients.map((t,e)=>({text:t.text,url:t.url}))))},{text:"",iconclass:"fa fa-minus-circle",onbtclick:t=>{var e,i;if(i=this.clist.selectedItem,e=this.clist.selected,i)return this.ask({text:__("Do you realy want to delete: `{0}`",i.data.text)}).then(t=>{if(t)return this.parent.setting.clients.splice(e,1),this.refresh_list()})}},{text:"",iconclass:"fa fa-pencil-square-o",onbtclick:e=>{var i,n;if(n=this.clist.selectedItem,i=this.clist.selected,n)return this.openDialog(new t,{title:__("Edit client"),text:n.data.text,url:n.data.url}).then(t=>{if(t)return this.parent.setting.clients[i].text=t.text,this.parent.setting.clients[i].url=t.url,this.refresh_list()})}}],this.find("btnswitch").onbtclick=t=>{var e;if(e=this.clist.selectedItem)return this.parent.setting.curl=e.data.url,this.parent.setting.cname=e.data.text,this.parent.switchClient(),this.quit()},this.refresh_list()}}).scheme='<afx-app-window width=\'400\' height=\'300\'>\n <afx-vbox padding="5">\n <afx-list-view data-id="client-list"></afx-list-view>\n <div data-height="35" style="text-align: right;">\n <afx-button text="__(Switch client)" data-id="btnswitch"></afx-button>\n <div>\n </afx-vbox>\n</afx-app-window>',(i=class extends this.OS.application.BaseApplication{constructor(t){super("GPClient",t)}main(){return this.setting.clients||(this.setting.clients=[]),this.container=this.find("container"),this.bindKey("CTRL-M",()=>this.openDialog(new e,{title:__("Client Manager")})),this.switchClient()}switchClient(){return this.setting.curl?(this.container.src=this.setting.curl,this.scheme.apptitle=this.setting.cname):this.notify(__("No client selected, manager client in menu Options > Client manager"))}menu(){return[{text:"__(Options)",nodes:[{text:"__(Client manager)",shortcut:"C-M"}],onchildselect:t=>this.openDialog(new e,{title:__("Client Manager")})}]}}).singleton=!0,this.OS.register("GPClient",i)}).call(this);

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@iohub.dev"
},
"version":"0.1.4-a",
"version":"0.1.5-a",
"category":"Internet",
"iconclass":"bi bi-hurricane",
"mimes":["none"],

View File

@ -1,5 +1,5 @@
<afx-app-window apptitle="__(Unknow client)" width="700" height="500" data-id="GPClient" blur-overlay="true">
<afx-hbox >
<iframe frameborder="0" data-id="container" src=""></iframe>
<iframe frameborder="0" data-id="container" src="" allow="clipboard-read; clipboard-write"></iframe>
</afx-hbox>
</afx-app-window>

Binary file not shown.

View File

@ -43,7 +43,14 @@ ClientDialog.scheme = """
class ClientListDialog extends this.OS.GUI.BasicDialog
constructor: () ->
super "ClientListDialog", ClientListDialog.scheme
refresh_list: () ->
@clist.data = @parent.setting.clients.map (e) =>
{
text: e.text,
url: e.url
}
main: () ->
super.main()
@clist = @find("client-list")
@ -56,9 +63,13 @@ class ClientListDialog extends this.OS.GUI.BasicDialog
title: __("Add new client")
})
.then (data) =>
#console.log(data)
console.log(data)
@parent.setting.clients.push(data)
@clist.data = @parent.setting.clients
@clist.data = @parent.setting.clients.map (e,i) =>
{
text: e.text,
url: e.url
}
},
{
@ -72,25 +83,25 @@ class ClientListDialog extends this.OS.GUI.BasicDialog
.then (d) =>
return unless d
@parent.setting.clients.splice(index,1)
@clist.data = @parent.setting.clients
@refresh_list()
},
{
text: "",
iconclass: "fa fa-pencil-square-o",
onbtclick: (e) =>
item = @clist.selectedItem
index = @clist.selected
return unless item
@openDialog(new ClientDialog(), {
title: __("Add new client"),
title: __("Edit client"),
text: item.data.text,
url: item.data.url
})
.then (data) =>
#console.log(data)
return unless data
item.data.text = data.text
item.data.url = data.url
@clist.data = @parent.setting.clients
@parent.setting.clients[index].text = data.text
@parent.setting.clients[index].url = data.url
@refresh_list()
}
]
@find("btnswitch").onbtclick = (e) =>
@ -100,7 +111,7 @@ class ClientListDialog extends this.OS.GUI.BasicDialog
@parent.setting.cname = item.data.text
@parent.switchClient()
@quit()
@clist.data = @parent.setting.clients
@refresh_list()
ClientListDialog.scheme = """
<afx-app-window width='400' height='300'>

View File

@ -7,7 +7,7 @@
"author": "Xuan Sang LE",
"email": "mrsang@iohub.dev"
},
"version":"0.1.4-a",
"version":"0.1.5-a",
"category":"Internet",
"iconclass":"bi bi-hurricane",
"mimes":["none"],

View File

@ -17,8 +17,10 @@
"data": ["build","build/debug","build/release"]
},
{
"name": "ts-import",
"data": ["sdk://core/ts/core.d.ts", "sdk://core/ts/jquery.d.ts","sdk://core/ts/antos.d.ts"]
"name": "ts-antos-sdk",
"data": {
"version": "2.0.x"
}
},
{
"name": "ts-compile",

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More