1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import arsd
.simpledisplay
;
33 private import std
.path
: buildPath
;
36 // ////////////////////////////////////////////////////////////////////////// //
37 __gshared string RcDir
= "~/.xreader";
38 //__gshared string stateFileName;
39 __gshared
int currentFileId
= -1;
40 __gshared Database filedb
;
42 private enum SQLPragmas
=
43 //"PRAGMA page_size=512;\n"~
44 //"PRAGMA threads=3;\n"~
45 "PRAGMA case_sensitive_like=OFF;\n"~
46 "PRAGMA foreign_keys=OFF;\n"~
47 "PRAGMA secure_delete=OFF;\n"~
48 "PRAGMA trusted_schema=OFF;\n"~
49 "PRAGMA writable_schema=OFF;\n"~
50 "PRAGMA auto_vacuum=NONE;\n"~
51 "PRAGMA encoding='UTF-8';\n"~
52 "PRAGMA synchronous=OFF;\n"~
53 "PRAGMA journal_mode=WAL;\n";
55 private enum SQLSchema
=
56 "CREATE TABLE IF NOT EXISTS known_files (\n"~
57 " id INTEGER PRIMARY KEY /* file id */\n"~
58 " , lastread INTEGER NOT NULL DEFAULT 0 /* unixtime; 0 means 'hidden' */\n"~
59 " , wordindex INTEGER NOT NULL DEFAULT 0 /* last file position */\n"~
60 " , filename TEXT NOT NULL UNIQUE /* filename without a path */\n"~
61 " , fullpath TEXT /* disk file name, purely informational, may be absent */\n"~
62 " , title TEXT /* book title */\n"~
63 " , author TEXT /* book author */\n"~
64 " , seqname TEXT /* sequence name */\n"~
65 " , seqnum INTEGER NOT NULL DEFAULT 0 /* sequence index */\n"~
68 "CREATE INDEX IF NOT EXISTS files_by_lastread ON known_files (lastread);\n"~
71 "CREATE TABLE IF NOT EXISTS flubusta_cache (\n"~
72 " id INTEGER PRIMARY KEY /* record id */\n"~
73 " , flibusta_id TEXT NOT NULL UNIQUE\n"~
74 " , filename TEXT NOT NULL\n"~
78 shared static this () {
80 RcDir
= RcDir
.expandTilde
.absolutePath
;
81 try { import std
.file
; mkdirRecurse(RcDir
); } catch (Exception
) {}
82 filedb
.openEx(buildPath(RcDir
, "xreader.db"), Database
.Mode
.ReadWriteCreate
,
83 SQLPragmas
, SQLSchema
);
87 shared static ~this () {
91 __gshared
float shipAngle
= 0;
92 __gshared
bool showShip
= false;
94 __gshared EliteModel shipModel
;
97 // ////////////////////////////////////////////////////////////////////////// //
98 __gshared string textFontName
= "~/ttf/ms/arial.ttf:noaa";
99 __gshared string textiFontName
= "~/ttf/ms/ariali.ttf:noaa";
100 __gshared string textbFontName
= "~/ttf/ms/arialbd.ttf:noaa";
101 __gshared string textzFontName
= "~/ttf/ms/arialbi.ttf:noaa";
102 __gshared string monoFontName
= "~/ttf/ms/cour.ttf:noaa";
103 __gshared string monoiFontName
= "~/ttf/ms/couri.ttf:noaa";
104 __gshared string monobFontName
= "~/ttf/ms/courbd.ttf:noaa";
105 __gshared string monozFontName
= "~/ttf/ms/courbi.ttf:noaa";
106 __gshared string uiFontName
= "~/ttf/ms/verdana.ttf:noaa";
107 __gshared string galmapFontName
= "~/ttf/ms/verdana.ttf:noaa";
108 __gshared
int GWidth
= 900;
109 __gshared
int GHeight
= 1024;
110 __gshared
int fsizeText
= 24;
111 __gshared
int fsizeUI
= 16;
112 __gshared
int fGalMapSize
= 16;
113 __gshared
bool sbLeft
= false;
114 __gshared
bool interAllowed
= false;
116 __gshared
bool flagNanoAA
= false;
117 __gshared
bool flagNanoSS
= false;
118 __gshared
bool flagNanoFAA
= false;
120 __gshared NVGColor colorDim
= NVGColor(0, 0, 0, 0);
121 __gshared NVGColor colorBack
= NVGColor(0x2a, 0x2a, 0x2a);
122 __gshared NVGColor colorText
= NVGColor(0xff, 0x7f, 0x00);
123 __gshared NVGColor colorTextHi
= NVGColor(0xff, 0xff, 0x00);
124 __gshared NVGColor colorTextHref
= NVGColor(0, 0, 0x80);
126 __gshared
int uiFont
, galmapFont
;
127 __gshared
int textFont
, textiFont
, textbFont
, textzFont
;
128 __gshared
int monoFont
, monoiFont
, monobFont
, monozFont
;
130 __gshared
bool optJustify
= false;
134 enum MinWinWidth
= 320;
135 enum MinWinHeight
= 240;
138 // ////////////////////////////////////////////////////////////////////////// //
139 int updateFileLastReadTime (string fname
, string author
, string title
,
140 string sequence
, uint seqnum
)
142 if (fname
.length
== 0) return -1;
144 import std
.path
: baseName
, expandTilde
, absolutePath
;
145 string filename
= fname
.baseName
;
146 string fullname
= fname
.expandTilde
.absolutePath
;
147 long epoch
= cast(long)Clock
.currTime
.toUnixTime();
148 if (author
.length
== 0) author
= "";
149 if (title
.length
== 0) title
= "";
150 auto stmt
= filedb
.statement(`
151 INSERT INTO known_files
152 ( lastread, filename, fullpath, author, title, seqname, seqnum)
153 VALUES(:lastread,:filename,:fullpath,:author,:title,:seqname,:seqnum)
154 ON CONFLICT(filename)
156 SET lastread=:lastread, fullpath=:fullpath
157 , author=:author, title=:title
158 , seqname=:seqname, seqnum=:seqnum
162 .bind(":lastread", epoch
)
163 .bindText(":filename", filename
)
164 .bindText(":fullpath", fullname
)
165 .bindText(":author", author
)
166 .bindText(":title", title
)
167 .bindText(":seqname", sequence
)
168 .bind(":seqnum", seqnum
);
170 foreach (auto row
; stmt
.range
) {
177 string
getLatestFileName () {
178 auto stmt
= filedb
.statement(`
179 SELECT MAX(lastread) AS lrd, fullpath AS fullpath FROM known_files
180 WHERE lastread>0 AND fullpath<>''
183 foreach (auto row
; stmt
.range
) {
184 res
= row
.fullpath
!string
;
190 int getCurrentFileWordIndex () {
191 if (currentFileId
<= 0) return -1;
192 auto stmt
= filedb
.statement(`
193 SELECT wordindex AS wordindex FROM known_files
198 foreach (auto row
; stmt
.bind(":id", currentFileId
).range
) {
199 widx
= row
.wordindex
!int;
205 void updateCurrentFileWordIndex (int widx
) {
206 if (currentFileId
<= 0 || widx
< 0) return;
207 auto stmt
= filedb
.statement(`
208 UPDATE known_files SET wordindex=:widx
212 .bind(":id", currentFileId
)
218 // ////////////////////////////////////////////////////////////////////////// //
219 BookInfo
[] loadDetailedHistory () {
222 scope(exit
) delete hideFileIds
;
223 bool hasUpdates
= false;
224 filedb
.beginTransaction();
225 scope(exit
) filedb
.commitTransaction();
227 import std
.path
, std
.file
: exists
;
228 auto stmt
= filedb
.statement(`
229 SELECT id AS id, fullpath AS fullpath, author AS author, title AS title
230 , seqname AS seqname, seqnum AS seqnum
232 WHERE lastread>0 AND fullpath<>''
235 foreach (auto row
; stmt
.range
) {
236 import std
.file
: exists
, isFile
;
237 string diskfile
= row
.fullpath
!string
;
242 writeln(diskfile
, "; exists=", diskfile
.exists
, "; isFile=", diskfile
.isFile
,
243 "; author=", row
.author
!string
,
244 "; title=", row
.title
!string
,
245 "; seqname=", row
.seqname
!string
,
246 "; seqnum=", row
.seqnum
!uint,
247 "; id=", row
.id
!int);
249 if (diskfile
.exists
&& diskfile
.isFile
) {
251 bi
.author
= row
.author
!string
;
252 bi
.title
= row
.title
!string
;
253 bi
.seqname
= row
.seqname
!string
;
254 bi
.seqnum
= row
.seqnum
!uint;
255 bi
.diskfile
= diskfile
;
257 if (bi
.author
.length
== 0 && bi
.title
.length
== 0) {
259 bi
= loadBookInfo(diskfile
);
260 bi
.diskfile
= diskfile
;
262 bi
.needupdate
= true;
268 } catch (Exception
) {}
269 if (!ok
) hideFileIds
~= row
.id
!int;
271 // hide missing files
272 if (hideFileIds
.length
) {
273 stmt
= filedb
.statement(`
278 foreach (int fid
; hideFileIds
) {
279 stmt
.bind(":fid", fid
).doAll();
283 stmt
= filedb
.statement(`
285 SET author=:author, title=:title, seqname=:seqname, seqnum=:seqnum
288 foreach (const ref BookInfo bi
; res
) {
289 if (!bi
.needupdate
) continue;
292 .bindText(":author", bi
.author
)
293 .bindText(":title", bi
.title
)
294 .bindText(":seqname", bi
.seqname
)
295 .bind(":seqnum", bi
.seqnum
)
299 } catch (Exception
) {}
304 void removeFileFromHistory (const(char)[] filename
) {
305 auto stmt
= filedb
.statement(`
308 WHERE fullpath=:filename
311 .bindText(":filename", filename
)
316 // ////////////////////////////////////////////////////////////////////////// //
317 __gshared string
[] eliteShipFiles
;
320 void loadEliteShips () {
322 import std
.path
: extension
;
323 vfsAddPak(buildPath(RcDir
, "eliteships.zip"), "oxm:");
324 //scope(exit) vfsRemovePak(buildPath(RcDir, "eliteships.zip"), "oxm:");
325 foreach (immutable idx
, ref de; vfsFileList
) {
326 //writeln("<", de.name, ">");
327 if (de.name
.length
> 4 && de.name
[0..4] == "oxm:" && strEquCI(de.name
.extension
, ".oxm")) {
329 import core.memory : GC;
331 auto mdl = new EliteModel(de.name);
335 eliteShipFiles ~= de.name;
336 } catch (Exception e) {}
339 eliteShipFiles
~= de.name
;
345 // ////////////////////////////////////////////////////////////////////////// //
346 VFile
fwritef(A
...) (VFile fl
, string fmt
, /*lazy*/ A args
) {
347 import std
.string
: format
;
348 auto s
= format(fmt
, args
);
349 if (s
.length
) fl
.rawWriteExact(s
[]);
354 // ////////////////////////////////////////////////////////////////////////// //
359 fl
= VFile(buildPath(RcDir
, "config.ui"));
360 } catch (Exception
) {
362 try { import std
.file
; mkdirRecurse(RcDir
); } catch (Exception
) {}
363 fl
= VFile(buildPath(RcDir
, "config.ui"), "w");
364 fl
.fwritef("width=%s\n", GWidth
);
365 fl
.fwritef("height=%s\n", GHeight
);
366 fl
.fwritef("font-text=%s\n", textFontName
);
367 fl
.fwritef("font-text-italic=%s\n", textiFontName
);
368 fl
.fwritef("font-text-bold=%s\n", textbFontName
);
369 fl
.fwritef("font-text-bold-italic=%s\n", textzFontName
);
370 fl
.fwritef("font-mono=%s\n", monoFontName
);
371 fl
.fwritef("font-mono-italic=%s\n", monoiFontName
);
372 fl
.fwritef("font-mono-bold=%s\n", monobFontName
);
373 fl
.fwritef("font-mono-bold-italic=%s\n", monozFontName
);
374 fl
.fwritef("font-ui=%s\n", uiFontName
);
375 fl
.fwritef("font-galmap=%s\n", galmapFontName
);
376 fl
.fwritef("text-size=%s\n", fsizeText
);
377 fl
.fwritef("ui-text-size=%s\n", fsizeUI
);
378 fl
.fwritef("galmap-text-size=%s\n", fGalMapSize
);
379 fl
.fwritef("scrollbar-on-left=%s\n", sbLeft
);
380 fl
.fwritef("interference=%s\n", interAllowed
);
381 fl
.fwritef("color-dim=0x%08x\n", colorDim
.asUintARGB
);
382 fl
.fwritef("color-back=0x%08x\n", colorBack
.asUintARGB
);
383 fl
.fwritef("color-text=0x%08x\n", colorText
.asUintARGB
);
384 fl
.fwritef("color-text-hi=0x%08x\n", colorTextHi
.asUintARGB
);
385 fl
.fwritef("color-text-href=0x%08x\n", colorTextHref
.asUintARGB
);
386 fl
.fwritef("nano-aa=%s\n", flagNanoAA
);
387 fl
.fwritef("nano-ss=%s\n", flagNanoSS
);
388 fl
.fwritef("nano-faa=%s\n", flagNanoFAA
);
389 fl
.fwritef("justify-text=%s\n", optJustify
);
390 } catch (Exception
) {}
394 "font-text", &textFontName
,
395 "font-text-italic", &textiFontName
,
396 "font-text-bold", &textbFontName
,
397 "font-text-bold-italic", &textzFontName
,
398 "font-text-italic-bold", &textzFontName
,
399 "font-mono", &monoFontName
,
400 "font-mono-italic", &monoiFontName
,
401 "font-mono-bold", &monobFontName
,
402 "font-mono-bold-italic", &monozFontName
,
403 "font-mono-italic-bold", &monozFontName
,
404 "font-ui", &uiFontName
,
405 "font-galmap", &galmapFontName
,
408 "text-size", &fsizeText
,
409 "ui-text-size", &fsizeUI
,
410 "galmap-text-size", &fGalMapSize
,
411 "scrollbar-on-left", &sbLeft
,
412 "interference", &interAllowed
,
413 "color-dim", &colorDim
,
414 "color-back", &colorBack
,
415 "color-text", &colorText
,
416 "color-text-hi", &colorTextHi
,
417 "color-text-href", &colorTextHref
,
418 "nano-aa", &flagNanoAA
,
419 "nano-ss", &flagNanoSS
,
420 "nano-faa", &flagNanoFAA
,
421 "justify-text", &optJustify
,
426 // ////////////////////////////////////////////////////////////////////////// //
427 bool isComment (const(char)[] s
) {
428 while (s
.length
&& s
.ptr
[0] <= ' ') s
= s
[1..$];
429 return (s
.length
> 0 && s
.ptr
[0] == '#');