mirror of
https://github.com/antos-rde/antosdk-apps.git
synced 2025-07-19 15:29:54 +02:00
Compare commits
42 Commits
d1984d89b8
...
2.0.x
Author | SHA1 | Date | |
---|---|---|---|
5594516697 | |||
4a62c4d1cf | |||
3a3f55e7c8 | |||
761c2556cb | |||
cb6b76d067 | |||
d9726d6041 | |||
675f245b3f | |||
5618965d3c | |||
d8d38f5b05 | |||
3b14ff5b9f | |||
4d03eff031 | |||
7174dfb8c9 | |||
f8435cd87c | |||
e8aa62735a | |||
b3ea32f4bc | |||
4c86a315dd | |||
d70391c7ba | |||
8153ca6f9d | |||
a628b57109 | |||
e22e8c8e8c | |||
4a0a611acc | |||
0227f02ad6 | |||
7cbd4ce979 | |||
79ddff1cbd | |||
402f0edb49 | |||
77258bd48b | |||
d011b004ec | |||
f9f27321ab | |||
545f630b0e | |||
7292d2ef21 | |||
6354c48680 | |||
5dec0a2b56 | |||
c8ddd5ec6e | |||
04050f124f | |||
cf21ef60e0 | |||
cd5b0f66cc | |||
e3deffe907 | |||
11df616319 | |||
9afa2f5d1c | |||
a2a602f5b2 | |||
b3db4861ef | |||
fc515ff012 |
@ -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.
@ -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.
Binary file not shown.
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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
@ -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": [
|
||||
|
@ -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.
@ -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": [
|
||||
|
@ -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({
|
||||
|
@ -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.
@ -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.
@ -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
|
||||
|
@ -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
@ -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.
@ -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)
|
||||
|
@ -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"],
|
||||
|
@ -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
|
||||
|
83
Blogger/api/ai/analyse.lua
Normal file
83
Blogger/api/ai/analyse.lua
Normal 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
346
Blogger/api/ai/cluster.lua
Normal 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
|
151
Blogger/api/ai/stopwords.txt
Normal file
151
Blogger/api/ai/stopwords.txt
Normal 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
50
Blogger/api/ai/test.lua
Normal 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
|
@ -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
|
||||
|
@ -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"
|
||||
],
|
||||
|
@ -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
|
||||
|
83
Blogger/build/debug/api/ai/analyse.lua
Normal file
83
Blogger/build/debug/api/ai/analyse.lua
Normal 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/build/debug/api/ai/cluster.lua
Normal file
346
Blogger/build/debug/api/ai/cluster.lua
Normal 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
|
151
Blogger/build/debug/api/ai/stopwords.txt
Normal file
151
Blogger/build/debug/api/ai/stopwords.txt
Normal 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/build/debug/api/ai/test.lua
Normal file
50
Blogger/build/debug/api/ai/test.lua
Normal 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
|
72
Blogger/build/debug/api/sendmail.lua
Normal file
72
Blogger/build/debug/api/sendmail.lua
Normal 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
|
@ -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
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
@ -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.
@ -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>
|
@ -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
282
Blogger/dialogs.ts
Normal 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\
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ""
|
||||
.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
925
Blogger/main.ts
Normal 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(``);
|
||||
}).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",
|
||||
];
|
||||
}
|
||||
}
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
@ -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
104
Blogger/tags.ts
Normal 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.
@ -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
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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
@ -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"],
|
||||
|
@ -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.
@ -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"],
|
||||
|
@ -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) {
|
||||
|
Binary file not shown.
@ -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
|
||||
|
@ -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
|
@ -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>
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
@ -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
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
@ -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>
|
||||
"""
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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
354
Docify/ts/dialogs.ts
Normal 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
701
Docify/ts/main.ts
Normal 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"
|
||||
];
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
# GPClient
|
||||
|
||||
Simple Multi purpose client wrapper
|
||||
|
||||
## change logs
|
||||
- allow clipboard read/write in iframe
|
||||
|
@ -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>
|
@ -3,6 +3,10 @@
|
||||
"targets": {
|
||||
"init": {
|
||||
"jobs": [
|
||||
{
|
||||
"name": "vfs-rm_no_error",
|
||||
"data": ["build/debug","build/release"]
|
||||
},
|
||||
{
|
||||
"name": "vfs-mkdir",
|
||||
"data": [
|
||||
|
@ -1,3 +1,6 @@
|
||||
# GPClient
|
||||
|
||||
Simple Multi purpose client wrapper
|
||||
|
||||
## change logs
|
||||
- allow clipboard read/write in iframe
|
||||
|
@ -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);
|
@ -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"],
|
||||
|
@ -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.
@ -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'>
|
||||
|
@ -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"],
|
||||
|
@ -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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user