fixed flibusta loader
[xreader.git] / xreader.d
blob45700d638ec28bf778d4f128dadcdc722a73c6ee
1 /* Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>f
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/>.
17 module xreader is aliced;
19 import core.time;
21 import std.concurrency;
22 import std.net.curl;
23 import std.regex;
25 import arsd.simpledisplay;
26 import arsd.image;
28 import iv.nanovega;
29 import iv.nanovega.textlayouter;
30 import iv.nanovega.blendish;
31 import iv.nanovega.perf;
33 import iv.strex;
35 import iv.cmdcongl;
37 import iv.unarray;
39 import iv.vfs;
40 import iv.vfs.io;
42 import xmodel;
43 import xiniz;
45 import booktext;
47 import xreadercfg;
48 import xreaderui;
49 import xreaderifs;
50 import xreaderfmt;
52 import eworld;
53 import ewbillboard;
55 //version = debug_draw;
58 // ////////////////////////////////////////////////////////////////////////// //
59 __gshared int oglConScale = 1;
62 // ////////////////////////////////////////////////////////////////////////// //
63 __gshared string bookFileName;
65 __gshared int formatWorks = -1;
66 __gshared NVGContext vg = null;
67 __gshared PerfGraph fps;
68 __gshared bool fpsVisible = false;
69 __gshared BookText bookText;
70 __gshared string newBookFileName;
71 __gshared uint[] posstack;
72 __gshared SimpleWindow sdwindow;
73 __gshared LayTextC laytext;
74 __gshared BookInfo[] recentFiles;
75 __gshared BookMetadata bookmeta;
76 __gshared int currentSection = -666;
77 __gshared bool mouseStartMarking = false;
79 __gshared int textHeight;
81 __gshared int toMove = 0; // for smooth scroller
82 __gshared int topY = 0;
83 __gshared int arrowDir = 0;
84 __gshared int lastArrowDir = 0;
85 __gshared float arrowSpeed = 0;
87 __gshared int newYLine = -1; // index
88 __gshared float newYAlpha;
89 __gshared bool newYFade = false;
90 __gshared MonoTime nextFadeTime;
92 __gshared NVGImage curImg, curImgWhite;
93 __gshared int mouseX = -666, mouseY = -666;
94 __gshared bool mouseHigh = false;
95 __gshared bool mouseHidden = false;
96 __gshared MonoTime lastMMove;
97 __gshared float mouseAlpha = 1.0f;
98 __gshared int mouseFadingAway = false;
100 __gshared bool needRedrawFlag = true;
102 __gshared bool firstFormat = true;
104 __gshared bool inGalaxyMap = false;
105 __gshared PopupMenu currPopup;
106 __gshared void delegate (int item) onPopupSelect;
107 __gshared bool delegate (KeyEvent event) onPopupKeyEvent; // return `true` to close menu
108 __gshared int shipModelIndex = 0;
109 __gshared bool popupNoShipKill = false;
111 __gshared bool doSaveCheck = false;
112 __gshared MonoTime nextSaveTime;
115 // ////////////////////////////////////////////////////////////////////////// //
116 struct HrefInfo {
117 int widx;
118 int x0, y0;
119 int width, height;
122 // all visible hrefs on a page
123 __gshared HrefInfo[] hreflist;
124 __gshared int hrefcurr = -1;
127 // ////////////////////////////////////////////////////////////////////////// //
128 struct Pulse {
129 public:
130 float PulseScale = 8; // ratio of "tail" to "acceleration"
132 private:
133 float PulseNormalize = 1;
135 private:
136 // viscous fluid with a pulse for part and decay for the rest
137 float pulseInternal (float x) nothrow @safe @nogc {
138 import std.math : exp;
139 float val;
141 // test
142 x = x*PulseScale;
143 if (x < 1) {
144 val = x-(1-exp(-x));
145 } else {
146 // the previous animation ended here:
147 float start = exp(-1.0f);
148 // simple viscous drag
149 x -= 1;
150 float expx = 1-exp(-x);
151 val = start+(expx*(1.0-start));
154 return val*PulseNormalize;
158 void ComputePulseScale () nothrow @safe @nogc {
159 PulseNormalize = 1.0f/pulseInternal(1);
162 public:
163 this (float ascale) nothrow @safe @nogc { PulseScale = ascale; }
165 void setScale (float ascale) nothrow @safe @nogc { PulseScale = ascale; PulseNormalize = 1; }
167 // viscous fluid with a pulse for part and decay for the rest
168 float pulse (float x) nothrow @safe @nogc {
169 if (x >= 1) return 1;
170 if (x <= 0) return 0;
172 if (PulseNormalize == 1) ComputePulseScale();
174 return pulseInternal(x);
179 __gshared Pulse pulse;
182 // ////////////////////////////////////////////////////////////////////////// //
183 void refresh () { needRedrawFlag = true; }
184 void refreshed () { needRedrawFlag = false; }
186 bool needRedraw () {
187 return (needRedrawFlag || (currPopup !is null && currPopup.needRedraw));
191 @property bool inMenu () { return (currPopup !is null); }
192 void closeMenu () { if (currPopup !is null) currPopup.destroy; currPopup = null; onPopupSelect = null; onPopupKeyEvent = null; }
195 void setShip (int idx) {
196 if (eliteShipFiles.length) {
197 import core.memory : GC;
198 if (idx >= eliteShipFiles.length) idx = cast(int)eliteShipFiles.length-1;
199 if (idx < 0) idx = 0;
200 if (shipModelIndex == idx && shipModel !is null) return;
201 // remove old ship
202 shipModelIndex = idx;
203 if (shipModel !is null) {
204 shipModel.glUnload();
205 shipModel.freeData();
206 shipModel.destroy;
207 shipModel = null;
209 GC.collect();
210 // load new ship
211 try {
212 shipModel = new EliteModel(eliteShipFiles[shipModelIndex]);
213 shipModel.glUpload();
214 shipModel.freeImages();
215 } catch (Exception e) {}
216 //shipModel = eliteShips[shipModelIndex];
217 GC.collect();
221 void ensureShipModel () {
222 if (eliteShipFiles.length && shipModel is null) {
223 import std.random : uniform;
224 setShip(uniform!"[)"(0, eliteShipFiles.length));
228 void freeShipModel () {
229 if (!showShip && !inMenu && shipModel !is null) {
230 import core.memory : GC;
231 shipModel.glUnload();
232 shipModel.freeData();
233 shipModel.destroy;
234 shipModel = null;
235 GC.collect();
240 void loadState () {
241 try {
242 int widx = getCurrentFileWordIndex();
244 xiniParse(VFile(stateFileName),
245 "wordindex", &widx,
248 if (widx >= 0) goTo(widx);
249 } catch (Exception) {}
252 void doSaveState (bool forced=false) {
253 if (!forced) {
254 if (formatWorks != 0) return;
255 if (!doSaveCheck) return;
256 auto ct = MonoTime.currTime;
257 if (ct < nextSaveTime) return;
258 } else {
259 if (laytext is null || laytext.lineCount == 0 || formatWorks != 0) return;
261 try {
262 //auto fo = VFile(stateFileName, "w");
263 if (laytext !is null && laytext.lineCount) {
264 auto lnum = laytext.findLineAtY(topY);
265 if (lnum >= 0) {
266 //fo.writeln("wordindex=", laytext.line(lnum).wstart);
267 updateCurrentFileWordIndex(laytext.line(lnum).wstart);
270 } catch (Exception) {}
271 doSaveCheck = false;
274 void stateChanged () {
275 if (!doSaveCheck) {
276 doSaveCheck = true;
277 nextSaveTime = MonoTime.currTime+10.seconds;
282 void hardScrollBy (int delta) {
283 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
284 int oldY = topY;
285 topY += delta;
286 if (topY >= laytext.textHeight-textHeight) topY = cast(int)laytext.textHeight-textHeight;
287 if (topY < 0) topY = 0;
288 if (topY != oldY) {
289 stateChanged();
290 refresh();
294 void scrollBy (int delta) {
295 if (delta == 0 || laytext is null || laytext.lineCount == 0) return;
296 toMove += delta;
297 newYFade = true;
298 newYAlpha = 1;
299 if (delta < 0) {
300 // scrolling up, mark top line
301 newYLine = laytext.findLineAtY(topY);
302 } else {
303 // scrolling down, mark bottom line
304 newYLine = laytext.findLineAtY(topY+textHeight-2);
306 version(none) {
307 conwriteln("scrollBy: delta=", delta, "; newYLine=", newYLine, "; topLine=", laytext.findLineAtY(topY));
309 if (newYLine < 0) newYFade = false;
313 void goHome () {
314 if (laytext is null) return;
315 if (topY != 0) {
316 topY = 0;
317 stateChanged();
318 refresh();
323 void goEnd () {
324 if (laytext is null || laytext.lineCount < 2) return;
325 auto newY = laytext.line(laytext.lineCount-1).y;
326 if (newY >= laytext.textHeight-textHeight) newY = cast(int)laytext.textHeight-textHeight;
327 if (newY < 0) newY = 0;
328 if (newY != topY) {
329 topY = newY;
330 stateChanged();
331 refresh();
336 void goTo (uint widx) {
337 if (laytext is null) return;
338 auto lidx = laytext.findLineWithWord(widx);
339 if (lidx != -1) {
340 assert(lidx < laytext.lineCount);
341 toMove = 0;
342 if (topY != laytext.line(lidx).y) {
343 topY = laytext.line(lidx).y;
344 stateChanged();
345 refresh();
347 version(none) {
348 conwriteln("goto: widx=", widx, "; lidx=", lidx, "; fwn=", laytext.line(lidx).wstart, "; topY=", topY);
349 auto lnum = laytext.findLineAtY(topY);
350 conwriteln(" newlnum=", lnum, "; lidx.y=", laytext.line(lidx).y, "; lnum.y=", laytext.line(lnum).y);
356 void pushPosition () {
357 if (laytext is null || laytext.lineCount == 0) return;
358 auto lidx = laytext.findLineAtY(topY);
359 if (lidx >= 0) posstack ~= laytext.line(lidx).wstart;
362 void popPosition () {
363 if (posstack.length == 0) return;
364 auto widx = posstack[$-1];
365 posstack.length -= 1;
366 posstack.assumeSafeAppend;
367 goTo(widx);
371 void gotoSection (int sn) {
372 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) return;
373 if (sn < 0 || sn >= bookmeta.sections.length) return;
374 auto lidx = laytext.findLineWithWord(bookmeta.sections[sn].wordidx);
375 if (lidx >= 0) {
376 auto newY = laytext.line(lidx).y;
377 if (newY != topY) {
378 topY = newY;
379 stateChanged();
380 refresh();
386 void relayout (bool forced=false) {
387 if (laytext !is null) {
388 uint widx;
389 auto lidx = laytext.findLineAtY(topY);
390 if (lidx >= 0) widx = laytext.line(lidx).wstart;
391 int maxWidth = GWidth-4-2-BND_SCROLLBAR_WIDTH-2;
392 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
394 import core.time;
395 auto stt = MonoTime.currTime;
396 laytext.relayout(maxWidth, forced);
397 auto ett = MonoTime.currTime-stt;
398 conwriteln("relayouted in ", ett.total!"msecs", " milliseconds; lines:", laytext.lineCount, "; words:", laytext.nextWordIndex);
400 goTo(widx);
401 refresh();
406 void drawShipName () {
407 if (shipModel is null || shipModel.name.length == 0) return;
408 vg.fontFaceId(uiFont);
409 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
410 vg.fontSize(fsizeUI);
411 auto w = vg.bndLabelWidth(-1, shipModel.name)+8;
412 float h = BND_WIDGET_HEIGHT+8;
413 float mx = (GWidth-w)/2.0;
414 float my = (GHeight-h)-8;
415 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
416 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.name);
417 if (shipModel.dispName && shipModel.dispName != shipModel.name) {
418 my -= BND_WIDGET_HEIGHT+16;
419 w = vg.bndLabelWidth(-1, shipModel.dispName)+8;
420 mx = (GWidth-w)/2.0;
421 vg.bndMenuBackground(mx, my, w, h, BND_CORNER_NONE);
422 vg.bndMenuItem(mx+4, my+4, w-8, BND_WIDGET_HEIGHT, BND_DEFAULT, -1, shipModel.dispName);
427 void setWindowTitle (bool force=false) {
428 if (bookmeta is null || laytext is null) {
429 currentSection = -666;
430 return;
432 int scidx = -1;
433 if (bookmeta.sections.length > 1) {
434 auto lidx = laytext.findLineAtY(topY);
435 if (lidx >= 0) {
436 foreach (auto idx, ref cc; bookmeta.sections) {
437 if (idx == 0) continue;
438 auto sline = laytext.findLineWithWord(cc.wordidx);
439 if (sline >= 0 && lidx >= sline) scidx = cast(int)idx;
443 //Anc sc = null;
444 //if (scidx > 0 && scidx < bookmeta.sections.length) sc = bookmeta.sections[scidx];
445 if (force || scidx != currentSection) {
446 currentSection = scidx;
447 string tt;
448 if (scidx > 0 && scidx < bookmeta.sections.length) {
449 import std.conv : to;
450 tt = bookmeta.sections[scidx].name.to!string~" \xe2\x80\x94 ";
452 sdwindow.title = tt~bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
457 void createSectionMenu () {
458 closeMenu();
459 //conwriteln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
460 if (laytext is null || laytext.lineCount == 0 || bookmeta is null) { freeShipModel(); return; }
461 currPopup = new PopupMenu(vg, "Sections"d, () {
462 dstring[] items;
463 foreach (const ref sc; bookmeta.sections) items ~= sc.name;
464 return items;
466 currPopup.allowFiltering = true;
467 //conwriteln(currPopup.items.length);
468 // find current section
469 currPopup.itemIndex = 0;
470 auto lidx = laytext.findLineAtY(topY);
471 if (lidx >= 0 && bookmeta.sections.length > 0) {
472 foreach (immutable sidx, const ref sc; bookmeta.sections) {
473 auto sline = laytext.findLineWithWord(sc.wordidx);
474 if (sline >= 0 && lidx >= sline) currPopup.itemIndex = cast(int)sidx;
477 onPopupSelect = (int item) { gotoSection(item); };
481 void createQuitMenu (bool wantYes) {
482 closeMenu();
483 currPopup = new PopupMenu(vg, "Quit?"d, () {
484 return ["Yes"d, "No"d];
486 currPopup.itemIndex = (wantYes ? 0 : 1);
487 onPopupSelect = (int item) { if (item == 0) concmd("quit"); };
491 void createRecentMenu () {
492 closeMenu();
493 if (recentFiles.length == 0) recentFiles = loadDetailedHistory();
494 if (recentFiles.length == 0) { freeShipModel(); return; }
495 currPopup = new PopupMenu(vg, "Recent files"d, () {
496 import std.conv : to;
497 dstring[] res;
498 foreach (const ref BookInfo bi; recentFiles) {
499 string s = bi.title;
500 if (bi.seqname.length) {
501 s ~= " (";
502 if (bi.seqnum) { s ~= to!string(bi.seqnum); s ~= ": "; }
503 //conwriteln(bi.seqname);
504 s ~= bi.seqname;
505 s ~= ")";
507 if (bi.author.length) { s ~= " \xe2\x80\x94 "; s ~= bi.author; }
508 res ~= s.to!dstring;
510 return res;
512 currPopup.allowFiltering = true;
513 currPopup.itemIndex = cast(int)recentFiles.length-1;
514 onPopupSelect = (int item) {
515 newBookFileName = recentFiles[item].diskfile;
516 popupNoShipKill = true;
518 onPopupKeyEvent = (KeyEvent event) {
519 if (event == "Delete" && currPopup.isCurValid) {
520 auto idx = currPopup.itemIndex;
521 if (idx >= 0 && idx < recentFiles.length) {
522 removeFileFromHistory(recentFiles[idx].diskfile);
523 currPopup.removeItem(idx);
524 foreach (immutable c; idx+1..recentFiles.length) recentFiles[c-1] = recentFiles[c];
525 recentFiles.length -= 1;
526 recentFiles.assumeSafeAppend;
529 return false; // don't close menu
534 bool menuKey (KeyEvent event) {
535 if (formatWorks != 0) return false;
536 if (!inMenu) return false;
537 if (inGalaxyMap) return false;
538 if (!event.pressed) return false;
539 if (currPopup is null) return false;
540 auto res = currPopup.onKey(event);
541 if (res == PopupMenu.Close) {
542 closeMenu();
543 freeShipModel();
544 refresh();
545 } else if (res >= 0) {
546 if (onPopupSelect !is null) onPopupSelect(res);
547 closeMenu();
548 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
549 refresh();
550 } else if (res == PopupMenu.NotMine) {
551 if (onPopupKeyEvent !is null && onPopupKeyEvent(event)) {
552 closeMenu();
553 freeShipModel();
554 refresh();
557 return true;
561 bool menuChar (dchar dch) {
562 if (formatWorks != 0) return false;
563 if (!inMenu) return false;
564 if (inGalaxyMap) return false;
565 if (currPopup is null) return false;
566 auto res = currPopup.onChar(dch);
567 if (res == PopupMenu.Close) {
568 closeMenu();
569 freeShipModel();
570 refresh();
571 } else if (res >= 0) {
572 if (onPopupSelect !is null) onPopupSelect(res);
573 closeMenu();
574 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
575 refresh();
577 return true;
581 bool menuMouse (MouseEvent event) {
582 if (formatWorks != 0) return false;
583 if (!inMenu) return false;
584 if (inGalaxyMap) return false;
585 if (currPopup is null) return false;
586 auto res = currPopup.onMouse(event);
587 if (res == PopupMenu.Close) {
588 closeMenu();
589 freeShipModel();
590 refresh();
591 } else if (res >= 0) {
592 if (onPopupSelect !is null) onPopupSelect(res);
593 closeMenu();
594 if (popupNoShipKill) popupNoShipKill = false; else freeShipModel();
595 refresh();
597 return true;
601 int getCurrHrefIdx () {
602 if (hrefcurr < 0 || hrefcurr >= hreflist.length) return -1;
603 if (auto hr = hreflist[hrefcurr].widx in bookmeta.hrefs) {
604 dstring href = hr.name;
605 if (href.length > 1 && href[0] == '#') {
606 href = href[1..$];
607 foreach (const ref id; bookmeta.ids) if (id.name == href) return id.wordidx;
610 return -1;
614 bool readerKey (KeyEvent event) {
615 if (formatWorks != 0) return false;
616 if (!event.pressed) {
617 switch (event.key) {
618 case Key.Up: arrowDir = 0; return true;
619 case Key.Down: arrowDir = 0; return true;
620 default:
622 return false;
624 switch (event.key) {
625 case Key.Space:
626 if (event.modifierState&ModifierState.shift) {
627 //goto case Key.PageUp;
628 hardScrollBy(toMove); toMove = 0;
629 scrollBy(-textHeight/3*2);
630 } else {
631 //goto case Key.PageDown;
632 hardScrollBy(toMove); toMove = 0;
633 scrollBy(textHeight/3*2);
635 break;
636 case Key.Backspace:
637 popPosition();
638 break;
639 case Key.PageUp:
640 hardScrollBy(toMove); toMove = 0;
641 hardScrollBy(-(textHeight > 32 ? textHeight-32 : textHeight));
642 break;
643 case Key.PageDown:
644 hardScrollBy(toMove); toMove = 0;
645 hardScrollBy(textHeight > 32 ? textHeight-32 : textHeight);
646 break;
647 case Key.Up:
648 //scrollBy(-8);
649 arrowDir = -1;
650 break;
651 case Key.Down:
652 //scrollBy(8);
653 arrowDir = 1;
654 break;
655 case Key.H:
656 if (laytext !is null) {
657 if (event == "C-H") goEnd(); else goHome();
659 break;
660 case Key.Tab:
661 if (hreflist.length) {
662 int dir = (event == "Tab" ? 1 : event == "S-Tab" ? -1 : 0);
663 if (dir) {
664 if (hrefcurr < 0) {
665 hrefcurr = (dir > 0 ? 0 : cast(int)hreflist.length-1);
666 } else {
667 hrefcurr = (hrefcurr+cast(int)hreflist.length+dir)%cast(int)hreflist.length;
670 refresh();
672 break;
673 case Key.Enter:
674 if (hrefcurr >= 0 && event == "Enter") {
675 auto wid = getCurrHrefIdx();
676 if (wid >= 0) {
677 pushPosition();
678 goTo(wid);
681 break;
682 default:
684 return true;
688 bool controlKey (KeyEvent event) {
689 if (!event.pressed) return false;
690 switch (event.key) {
691 case Key.Escape:
692 if (inGalaxyMap) { inGalaxyMap = false; refresh(); return true; }
693 if (inMenu) { closeMenu(); freeShipModel(); refresh(); return true; }
694 if (showShip) { showShip = false; freeShipModel(); refresh(); return true; }
695 if (hrefcurr >= 0) {
696 hrefcurr = -1;
697 } else {
698 ensureShipModel();
699 createQuitMenu(true);
701 refresh();
702 return true;
703 case Key.P: if (event.modifierState == ModifierState.ctrl) { concmd("r_fps toggle"); return true; } break;
704 case Key.I: if (event.modifierState == ModifierState.ctrl) { concmd("r_interference toggle"); return true; } break;
705 case Key.N: if (event.modifierState == ModifierState.ctrl) { concmd("r_sbleft toggle"); return true; } break;
706 case Key.C: if (event.modifierState == ModifierState.ctrl) { if (addIf()) refresh(); return true; } break;
707 case Key.E:
708 if (event.modifierState == ModifierState.ctrl && eliteShipFiles.length > 0) {
709 if (!inMenu) {
710 showShip = !showShip;
711 if (showShip) ensureShipModel(); else freeShipModel();
712 refresh();
714 return true;
716 break;
717 case Key.Q: if (event.modifierState == ModifierState.ctrl) { concmd("quit"); return true; } break;
718 case Key.B: if (formatWorks == 0 && !inMenu && !inGalaxyMap && event.modifierState == ModifierState.ctrl) { pushPosition(); return true; } break;
719 case Key.S:
720 if (formatWorks == 0 && !showShip && !inMenu && !inGalaxyMap) {
721 ensureShipModel();
722 createSectionMenu();
723 refresh();
725 break;
726 case Key.L:
727 if (formatWorks == 0 && event.modifierState == ModifierState.alt) {
728 ensureShipModel();
729 createRecentMenu();
730 refresh();
732 break;
733 case Key.M:
734 if (!inMenu && !showShip) {
735 inGalaxyMap = !inGalaxyMap;
736 refresh();
738 break;
739 case Key.R:
740 if (formatWorks == 0 && event.modifierState == ModifierState.ctrl) relayout(true);
741 break;
742 case Key.Home: if (showShip) { setShip(0); return true; } break;
743 case Key.End: if (showShip) { setShip(cast(int)eliteShipFiles.length); return true; } break;
744 case Key.Up: case Key.Left: if (showShip) { setShip(shipModelIndex-1); return true; } break;
745 case Key.Down: case Key.Right: if (showShip) { setShip(shipModelIndex+1); return true; } break;
746 default:
748 return showShip;
751 int startX () { pragma(inline, true); return (sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4); }
752 int endX () { pragma(inline, true); return startX+GWidth-4-2-BND_SCROLLBAR_WIDTH-2-1; }
754 int startY () { pragma(inline, true); return (GHeight-textHeight)/2; }
755 int endY () { pragma(inline, true); return startY+textHeight-1; }
758 // ////////////////////////////////////////////////////////////////////////// //
759 void run () {
760 if (GWidth < MinWinWidth) GWidth = MinWinWidth;
761 if (GHeight < MinWinHeight) GHeight = MinWinHeight;
763 bookText = loadBook(bookFileName);
765 setOpenGLContextVersion(3, 0); // it's enough
766 //openGLContextCompatible = false;
768 sdwindow = new SimpleWindow(GWidth, GHeight, bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast, OpenGlOptions.yes, Resizability.allowResizing);
769 sdwindow.hideCursor(); // we will do our own
770 sdwindow.setMinSize(MinWinWidth, MinWinHeight);
772 version(X11) sdwindow.closeQuery = delegate () { concmd("quit"); };
774 auto stt = MonoTime.currTime;
775 auto prevt = MonoTime.currTime;
776 auto curt = prevt;
777 textHeight = GHeight-8;
779 MonoTime nextIfTime = MonoTime.currTime;
781 lastMMove = MonoTime.currTime;
783 auto childTid = spawn(&reformatThreadFn, thisTid);
784 childTid.setMaxMailboxSize(128, OnCrowding.block);
785 thisTid.setMaxMailboxSize(128, OnCrowding.block);
787 void loadAndFormat (string filename) {
788 assert(formatWorks <= 0);
789 bookText = null;
790 laytext = null;
791 newYLine = -1;
792 //formatWorks = -1; //FIXME
793 firstFormat = true;
794 newYFade = false;
795 toMove = 0;
796 recentFiles = null;
797 arrowDir = 0;
798 lastArrowDir = 0;
799 arrowSpeed = 0;
800 //sdwindow.redrawOpenGlSceneNow();
801 //bookText = loadBook(newBookFileName);
802 //newBookFileName = null;
803 //reformat();
804 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
805 formatWorks = 1;
806 //conwriteln("*** loading new book: '", filename, "'");
807 hreflist.unsafeArrayClear();
808 hrefcurr = -1;
809 childTid.send(ReformatWork(null, filename, GWidth, GHeight));
810 refresh();
813 void reformat () {
814 if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
815 childTid.send(ReformatWork(cast(shared)bookText, null, GWidth, GHeight));
816 refresh();
819 void formatComplete (ref ReformatWorkComplete w) {
820 scope(exit) { import core.memory : GC; GC.collect(); GC.minimize(); }
822 auto lt = cast(LayTextC)w.laytext;
823 scope(exit) if (lt) lt.freeMemory();
824 w.laytext = null;
826 BookMetadata meta = cast(BookMetadata)w.meta;
827 w.meta = null;
829 BookText bt = cast(BookText)w.booktext;
830 w.booktext = null;
831 if (bt !is bookText) {
832 bookText = bt;
833 bookmeta = meta;
834 firstFormat = true;
835 currentSection = -666;
836 setWindowTitle(true);
837 //sdwindow.title = bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
838 } else if (bookmeta is null) {
839 bookmeta = meta;
842 if (isQuitRequested || formatWorks <= 0) return;
843 --formatWorks;
845 if (w.w != GWidth) {
846 if (formatWorks == 0) reformat();
847 return;
850 if (formatWorks != 0) return;
851 freeShipModel();
853 uint widx = 0;
854 if (!firstFormat && laytext !is null && laytext.lineCount) {
855 auto lidx = laytext.findLineAtY(topY);
856 if (lidx >= 0) widx = laytext.line(lidx).wstart;
858 if (laytext !is null) laytext.freeMemory();
859 laytext = lt;
860 lt = null;
861 if (firstFormat) {
862 loadState();
863 firstFormat = false;
864 doSaveCheck = false;
865 stateChanged();
866 } else {
867 goTo(widx);
869 refresh();
872 void closeWindow () {
873 doSaveState(true); // forced state save
874 if (!sdwindow.closed && vg !is null) {
875 curImg.clear();
876 curImgWhite.clear();
877 freeShipModel();
878 vg.kill();
879 vg = null;
880 sdwindow.close();
884 sdwindow.visibleForTheFirstTime = delegate () {
885 sdwindow.setAsCurrentOpenGlContext(); // make this window active
886 sdwindow.vsync = false;
887 //sdwindow.useGLFinish = false;
888 //glbindLoadFunctions();
890 try {
891 NVGContextFlag[4] flagList;
892 uint flagCount = 0;
893 if (flagNanoAA) flagList[flagCount++] = NVGContextFlag.Antialias;
894 if (flagNanoSS) flagList[flagCount++] = NVGContextFlag.StencilStrokes;
895 if (flagNanoFAA) flagList[flagCount++] = NVGContextFlag.FontAA; else flagList[flagCount++] = NVGContextFlag.FontNoAA;
896 vg = nvgCreateContext(flagList[0..flagCount]);
897 if (vg is null) {
898 conwriteln("Could not init nanovg.");
899 assert(0);
900 //sdwindow.close();
902 loadFonts(vg);
903 curImg = createCursorImage(vg);
904 curImgWhite = createCursorImage(vg, true);
905 fps = new PerfGraph("Frame Time", PerfGraph.Style.FPS, "ui");
906 } catch (Exception e) {
907 conwriteln("ERROR: ", e.msg);
908 concmd("quit");
909 return;
912 reformat();
914 refresh();
915 sdwindow.redrawOpenGlScene();
916 refresh();
919 sdwindow.windowResized = delegate (int w, int h) {
920 //conwriteln("w=", w, "; h=", h);
921 //if (w < MinWinWidth) w = MinWinWidth;
922 //if (h < MinWinHeight) h = MinWinHeight;
923 glViewport(0, 0, w, h);
924 GWidth = w;
925 GHeight = h;
926 textHeight = GHeight-8;
927 //reformat();
928 relayout();
929 refresh();
932 static bool isWordHref (const ref LayWord w) {
933 if (!w.style.href) return false;
934 if (auto hr = w.wordNum in bookmeta.hrefs) {
935 dstring href = hr.name;
936 if (href.length > 1 && href[0] == '#') return true;
938 return false;
941 static bool isWordHrefByIdx (int widx) {
942 if (widx < 0) return false;
943 auto w = laytext.wordByIndex(widx);
944 if (!w) return false;
945 return isWordHref(*w);
948 sdwindow.redrawOpenGlScene = delegate () {
949 if (isQuitRequested) return;
951 glconResize(GWidth/oglConScale, GHeight/oglConScale, oglConScale);
953 __gshared int cnt;
954 conwriteln("cnt=", cnt++);
957 // update window title
958 setWindowTitle();
960 //glClearColor(0, 0, 0, 0);
961 //glClearColor(0.18, 0.18, 0.18, 0);
962 glClearColor(colorBack.r, colorBack.g, colorBack.b, 0);
963 glClear(glNVGClearFlags|EliteModel.glClearFlags);
965 refreshed();
966 needRedrawFlag = (fps !is null && fpsVisible);
967 if (vg is null) return;
969 scope(exit) vg.releaseImages();
970 vg.beginFrame(GWidth, GHeight, 1);
971 drawIfs(vg);
972 // draw scrollbar
974 float curHeight = (laytext !is null ? topY : 0);
975 float th = (laytext !is null ? laytext.textHeight-textHeight : 0);
976 if (th <= 0) { curHeight = 0; th = 1; }
977 float sz = cast(float)(GHeight-4)/th;
978 if (sz > 1) sz = 1; else if (sz < 0.05) sz = 0.05;
979 int sx = (sbLeft ? 2 : GWidth-BND_SCROLLBAR_WIDTH-2);
980 vg.bndScrollSlider(sx, 2, BND_SCROLLBAR_WIDTH, GHeight-4, BND_DEFAULT, curHeight/th, sz);
983 int owidx = -1;
984 if (laytext is null) {
985 if (shipModel is null) {
986 vg.beginPath();
987 vg.fillColor(colorText);
988 int drawY = (GHeight-textHeight)/2;
989 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
990 vg.fontFaceId(textFont);
991 vg.fontSize(fsizeText);
992 vg.textAlign(NVGTextAlign.H.Center, NVGTextAlign.V.Middle);
993 vg.text(GWidth/2, GHeight/2, "REFORMATTING");
994 vg.fill();
996 } else if (hrefcurr >= 0) {
997 owidx = hreflist[hrefcurr].widx;
1000 // draw text page
1001 int markerY = -666;
1002 hreflist.unsafeArrayClear();
1003 hrefcurr = -1;
1004 if (laytext !is null && laytext.lineCount) {
1005 vg.beginPath();
1006 vg.fillColor(colorText);
1007 int drawY = startY;
1008 vg.intersectScissor((sbLeft ? 2+BND_SCROLLBAR_WIDTH+2 : 4), drawY, GWidth-4-2-BND_SCROLLBAR_WIDTH-2, textHeight);
1009 //FIXME: not GHeight!
1010 int lidx = laytext.findLineAtY(topY);
1011 if (lidx >= 0 && lidx < laytext.lineCount) {
1012 drawY -= topY-laytext.line(lidx).y;
1013 vg.textAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
1014 LayFontStyle lastStyle;
1015 int startx = startX;
1016 bool setColor = true;
1017 while (lidx < laytext.lineCount && drawY < GHeight) {
1018 auto ln = laytext.line(lidx);
1019 foreach (ref LayWord w; laytext.lineWords(lidx)) {
1020 if (lastStyle != w.style || setColor) {
1021 if (w.style.fontface != lastStyle.fontface) vg.fontFace(laytext.fontFace(w.style.fontface));
1022 vg.fontSize(w.style.fontsize);
1023 auto c = NVGColor(w.style.color);
1024 vg.fillColor(c);
1025 //vg.strokeColor(c);
1026 lastStyle = w.style;
1027 setColor = false;
1029 // line highlighting
1030 if (newYFade && newYLine == lidx) vg.fillColor(nvgLerpRGBA(colorText, colorTextHi, newYAlpha));
1031 auto oid = w.objectIdx;
1032 if (oid >= 0) {
1033 vg.fill();
1034 laytext.objectAtIndex(oid).draw(vg, startx+w.x, drawY+ln.h+ln.desc);
1035 vg.beginPath();
1036 } else {
1037 if (laytext.isMarkedWord(w.wordNum)) {
1038 vg.fill();
1039 vg.fillColor(nvgRGB(0, 0, 255));
1040 vg.beginPath();
1041 vg.rect(startx+w.x, drawY, (laytext.isMarkedWord(w.wordNum+1) ? w.fullwidth : w.width), w.h);
1042 vg.fill();
1043 vg.beginPath();
1044 vg.fillColor(nvgRGB(255, 255, 255));
1045 setColor = true;
1047 vg.text(startx+w.x, drawY+ln.h+ln.desc, laytext.wordText(w));
1049 // register href
1050 if (drawY >= 0 && drawY+ln.h <= GHeight && isWordHref(w)) {
1051 HrefInfo hif;
1052 hif.widx = cast(int)w.wordNum;
1053 hif.x0 = startx+w.x;
1054 hif.y0 = drawY;
1055 hif.width = w.w;
1056 hif.height = ln.h;
1057 hreflist.unsafeArrayAppend(hif);
1059 //TODO: draw lines over whitespace
1060 if (lastStyle.underline) vg.rect(startx+w.x, drawY+ln.h+ln.desc+1, w.w, 1);
1061 if (lastStyle.strike) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc/3, w.w, 2);
1062 if (lastStyle.overline) vg.rect(startx+w.x, drawY+ln.h+ln.desc-w.asc-1, w.w, 1);
1063 version(debug_draw) {
1064 vg.fill();
1065 vg.beginPath();
1066 vg.strokeWidth(1);
1067 vg.strokeColor(nvgRGB(0, 0, 255));
1068 vg.rect(startx+w.x, drawY, w.w, w.h);
1069 vg.stroke();
1070 vg.beginPath();
1072 if (newYFade && newYLine == lidx) vg.fillColor(NVGColor(lastStyle.color));
1074 if (newYFade && newYLine == lidx) markerY = drawY;
1075 drawY += ln.h;
1076 ++lidx;
1079 vg.fill();
1082 // restore current href (if it is possible)
1083 foreach (auto idx, const ref HrefInfo hif; hreflist) if (hif.widx == owidx) { hrefcurr = cast(int)idx; break; }
1085 // draw href
1086 if (hrefcurr >= 0) {
1087 // background
1088 vg.beginPath();
1089 vg.fillColor(nvgRGBA(0, 0, 0, 42));
1090 vg.rect(hreflist[hrefcurr].x0+0.5f, hreflist[hrefcurr].y0+0.5f, hreflist[hrefcurr].width, hreflist[hrefcurr].height);
1091 vg.fill();
1092 // frame
1093 vg.beginPath();
1094 vg.strokeWidth(1);
1095 vg.strokeColor(nvgRGB(0, 0, 255));
1096 vg.setLineDash([4.0f, 4.0f]);
1097 vg.rect(hreflist[hrefcurr].x0+0.5f, hreflist[hrefcurr].y0+0.5f, hreflist[hrefcurr].width, hreflist[hrefcurr].height);
1098 vg.stroke();
1099 vg.beginPath();
1102 // draw scroll marker
1103 if (markerY != -666) {
1104 vg.beginPath();
1105 vg.fillColor(NVGColor(0.3f, 0.3f, 0.3f, newYAlpha));
1106 vg.rect(startX, markerY, GWidth, 2);
1107 vg.fill();
1110 // dim text
1111 if (!showShip) {
1112 if (colorDim.a != 1) {
1113 //vg.scissor(0, 0, GWidth, GHeight);
1114 vg.resetScissor;
1115 vg.beginPath();
1116 vg.fillColor(colorDim);
1117 vg.rect(0, 0, GWidth, GHeight);
1118 vg.fill();
1122 // dim more if menu is active
1123 if (inMenu || showShip || formatWorks != 0) {
1124 //vg.scissor(0, 0, GWidth, GHeight);
1125 vg.resetScissor;
1126 vg.beginPath();
1127 //vg.globalAlpha(0.5);
1128 vg.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks != 0 ? 127 : 64)));
1129 vg.rect(0, 0, GWidth, GHeight);
1130 vg.fill();
1133 if (shipModel !is null) {
1134 vg.endFrame();
1135 float zz = shipModel.bbox[1].z-shipModel.bbox[0].z;
1136 zz += 10;
1137 lightsClear();
1139 lightAdd(
1140 0, 60, 60,
1141 1.0, 0.0, 0.0
1144 lightAdd(
1145 //0, 0, -60,
1146 0, 0, -zz,
1147 1.0, 1.0, 1.0
1149 drawModel(shipAngle, shipModel);
1150 vg.beginFrame(GWidth, GHeight, 1);
1151 drawShipName();
1152 //vg.endFrame();
1155 if (formatWorks == 0) {
1156 if (inMenu) {
1157 //vg.beginFrame(GWidth, GHeight, 1);
1158 //vg.scissor(0, 0, GWidth, GHeight);
1159 vg.resetScissor;
1160 currPopup.draw();
1161 //vg.endFrame();
1165 if (fps !is null && fpsVisible) {
1166 //vg.beginFrame(GWidth, GHeight, 1);
1167 //vg.scissor(0, 0, GWidth, GHeight);
1168 vg.resetScissor;
1169 fps.render(vg, GWidth-fps.width-4, GHeight-fps.height-4);
1170 //vg.endFrame();
1173 if (inGalaxyMap) drawGalaxy(vg);
1175 // mouse cursor
1176 if (curImg.valid && !mouseHidden) {
1177 int w, h;
1178 //vg.beginFrame(GWidth, GHeight, 1);
1179 vg.beginPath();
1180 //vg.scissor(0, 0, GWidth, GHeight);
1181 vg.resetScissor;
1182 vg.imageSize(curImg, w, h);
1183 if (mouseFadingAway) {
1184 mouseAlpha -= 0.1;
1185 if (mouseAlpha <= 0) { mouseFadingAway = false; mouseHidden = true; }
1186 } else {
1187 mouseAlpha = 1.0f;
1189 vg.globalAlpha(mouseAlpha);
1190 if (!mouseHigh) {
1191 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImg, 1));
1192 } else {
1193 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 1));
1195 vg.rect(mouseX, mouseY, w, h);
1196 vg.fill();
1198 if (mouseHigh) {
1199 vg.beginPath();
1200 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
1201 vg.rect(mouseX, mouseY, w, h);
1202 vg.fill();
1205 //vg.endFrame();
1207 vg.endFrame();
1208 glconDraw();
1211 void processThreads () {
1212 ReformatWorkComplete wd;
1213 for (;;) {
1214 bool workDone = false;
1215 auto res = receiveTimeout(Duration.zero,
1216 (QuitWork w) {
1217 formatWorks = -1;
1219 (ReformatWorkComplete w) {
1220 wd = w;
1221 workDone = true;
1224 if (!res) { assert(!workDone); break; }
1225 if (workDone) { workDone = false; formatComplete(wd); }
1229 auto lastTimerEventTime = MonoTime.currTime;
1230 bool somethingVisible = true;
1232 sdwindow.visibilityChanged = delegate (bool vis) {
1233 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
1234 somethingVisible = vis;
1237 conRegVar!fpsVisible("r_fps", "show fps indicator", (self, valstr) { refresh(); });
1238 conRegVar!inGalaxyMap("r_galaxymap", "show Elite galaxy map", (self, valstr) { refresh(); });
1240 conRegVar!interAllowed("r_interference", "show interference", (self, valstr) { refresh(); });
1241 conRegVar!sbLeft("r_sbleft", "show scrollbar at the left side", (self, valstr) { refresh(); });
1243 conRegVar!showShip("r_showship", "show Elite ship", (self, valstr) {
1244 if (eliteShipFiles.length == 0) return false;
1245 return true;
1247 (self, valstr) {
1248 if (showShip) ensureShipModel(); else freeShipModel();
1249 refresh();
1253 conRegFunc!(() {
1254 if (currPopup !is null) {
1255 currPopup.destroy;
1256 currPopup = null;
1257 freeShipModel();
1258 refresh();
1260 onPopupSelect = null;
1261 })("menu_close", "close current popup menu");
1263 conRegFunc!(() {
1264 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1265 currPopup.destroy;
1266 currPopup = null;
1267 ensureShipModel();
1268 createSectionMenu();
1269 refresh();
1271 })("menu_section", "show section menu");
1273 conRegFunc!(() {
1274 if (formatWorks == 0 && !showShip && !inGalaxyMap) {
1275 currPopup.destroy;
1276 currPopup = null;
1277 ensureShipModel();
1278 createRecentMenu();
1279 refresh();
1281 })("menu_recent", "show recent menu");
1283 sdwindow.eventLoop(1000/34,
1284 delegate () {
1285 processThreads();
1286 if (sdwindow.closed) return;
1287 conProcessQueue();
1288 if (isQuitRequested) { closeWindow(); return; }
1289 auto ctt = MonoTime.currTime;
1292 auto spass = (ctt-lastTimerEventTime).total!"msecs";
1293 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
1294 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
1295 lastTimerEventTime = ctt;
1296 // update FPS timer
1297 prevt = curt;
1298 //curt = MonoTime.currTime;
1299 curt = ctt;
1300 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
1301 auto dt = cast(double)((curt-prevt).total!"msecs")/1000.0;
1302 if (fps !is null) fps.update(dt);
1305 // smooth scrolling
1306 if (formatWorks == 0) {
1307 if (!lastArrowDir) lastArrowDir = arrowDir;
1308 if (arrowDir && arrowDir == lastArrowDir) {
1309 arrowSpeed += 0.015;
1310 if (arrowSpeed >= 1) arrowSpeed = 1;
1311 } else if (arrowDir) {
1312 assert(lastArrowDir != arrowDir);
1313 lastArrowDir = arrowDir;
1314 arrowSpeed *= 0.6;
1315 } else {
1316 arrowSpeed -= 0.025; //*(lastArrowDir ? 4 : 1);
1317 if (arrowSpeed <= 0) {
1318 arrowSpeed = 0;
1319 if (arrowDir) {
1320 lastArrowDir = arrowDir;
1324 if (toMove != 0) {
1325 import std.math : abs;
1326 enum Delta = 92*2;
1327 immutable int sign = (toMove < 0 ? -1 : 1);
1328 // change speed
1329 static int arrowSpeed = 0;
1330 if (arrowSpeed == 0) arrowSpeed = 16;
1331 if (abs(toMove) <= arrowSpeed) {
1332 arrowSpeed /= 2;
1333 if (arrowSpeed < 4) arrowSpeed = 4;
1334 } else {
1335 arrowSpeed *= 2;
1336 if (arrowSpeed > Delta) arrowSpeed = Delta;
1338 // calc move distance
1339 int sc = arrowSpeed;
1340 if (sc > abs(toMove)) sc = abs(toMove);
1341 hardScrollBy(sc*sign);
1342 toMove -= sc*sign;
1343 if (toMove == 0) arrowSpeed = 0;
1344 nextFadeTime = ctt+500.msecs;
1345 refresh();
1348 int toMove = cast(int)(pulse.pulse(arrowSpeed)*64);
1349 //if (arrowSpeed) { import std.stdio; writeln("lastdir=", lastArrowDir, "; dir=", arrowDir, "; as=", arrowSpeed, "; toMove=", toMove); }
1350 if (toMove > 1) {
1351 toMove *= lastArrowDir;
1352 hardScrollBy(toMove);
1353 nextFadeTime = ctt+500.msecs;
1354 refresh();
1358 enum Delta = 92*2;
1359 if (toMove != 0) {
1360 import std.math : abs;
1361 immutable int sign = (toMove < 0 ? -1 : 1);
1362 // change speed
1363 if (arrowSpeed == 0) arrowSpeed = 16;
1364 if (abs(toMove) <= arrowSpeed) {
1365 arrowSpeed /= 2;
1366 if (arrowSpeed < 4) arrowSpeed = 4;
1367 } else {
1368 arrowSpeed *= 2;
1369 if (arrowSpeed > Delta) arrowSpeed = Delta;
1371 // calc move distance
1372 int sc = arrowSpeed;
1373 if (sc > abs(toMove)) sc = abs(toMove);
1374 hardScrollBy(sc*sign);
1375 toMove -= sc*sign;
1376 if (toMove == 0) arrowSpeed = 0;
1377 nextFadeTime = ctt+500.msecs;
1378 refresh();
1379 } else if (arrowDir) {
1380 if ((arrowDir < 0 && arrowSpeed > 0) || (arrowDir > 0 && arrowSpeed < 0)) arrowSpeed += arrowDir*4;
1381 arrowSpeed += arrowDir*2;
1382 if (arrowSpeed < -64) arrowSpeed = -64; else if (arrowSpeed > 64) arrowSpeed = 64;
1383 hardScrollBy(arrowSpeed);
1384 refresh();
1385 } else if (arrowSpeed != 0) {
1386 if (arrowSpeed < 0) {
1387 if ((arrowSpeed += 4) > 0) arrowSpeed = 0;
1388 } else {
1389 if ((arrowSpeed -= 4) < 0) arrowSpeed = 0;
1391 if (arrowSpeed) {
1392 hardScrollBy(arrowSpeed);
1393 refresh();
1397 // highlight fading
1398 if (newYFade) {
1399 if (ctt >= nextFadeTime) {
1400 if ((newYAlpha -= 0.1) <= 0) {
1401 newYFade = false;
1402 } else {
1403 nextFadeTime = ctt+25.msecs;
1405 refresh();
1409 // interference processing
1410 if (ctt >= nextIfTime) {
1411 import std.random : uniform;
1412 if (uniform!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
1413 nextIfTime += (uniform!"[]"(50, 1500)).msecs;
1415 if (processIfs()) refresh();
1416 // ship rotation
1417 if (shipModel !is null) {
1418 shipAngle -= 1;
1419 if (shipAngle < 359) shipAngle += 360;
1420 refresh();
1422 // mouse autohide
1423 if (!mouseFadingAway) {
1424 if (!mouseHidden && !mouseHigh) {
1425 if ((ctt-lastMMove).total!"msecs" > 2500) {
1426 //mouseHidden = true;
1427 mouseFadingAway = true;
1428 mouseAlpha = 1.0f;
1429 refresh();
1433 if (somethingVisible) {
1434 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
1435 if (/*needRedraw*/true) sdwindow.redrawOpenGlSceneNow();
1436 } else {
1437 refresh();
1439 doSaveState();
1440 // load new book?
1441 if (newBookFileName.length && formatWorks == 0 && vg !is null) {
1442 doSaveState(true); // forced state save
1443 closeMenu();
1444 ensureShipModel();
1445 loadAndFormat(newBookFileName);
1446 newBookFileName = null;
1447 sdwindow.redrawOpenGlSceneNow();
1448 //refresh();
1451 delegate (KeyEvent event) {
1452 if (sdwindow.closed) return;
1453 if (glconKeyEvent(event)) return;
1454 if (event.key == Key.PadEnter) event.key = Key.Enter;
1455 if ((event.modifierState&ModifierState.numLock) == 0) {
1456 switch (event.key) {
1457 case Key.Pad0: event.key = Key.Insert; break;
1458 case Key.PadDot: event.key = Key.Delete; break;
1459 case Key.Pad1: event.key = Key.End; break;
1460 case Key.Pad2: event.key = Key.Down; break;
1461 case Key.Pad3: event.key = Key.PageDown; break;
1462 case Key.Pad4: event.key = Key.Left; break;
1463 case Key.Pad6: event.key = Key.Right; break;
1464 case Key.Pad7: event.key = Key.Home; break;
1465 case Key.Pad8: event.key = Key.Up; break;
1466 case Key.Pad9: event.key = Key.PageUp; break;
1467 //case Key.PadEnter: event.key = Key.Enter; break;
1468 default:
1471 if (controlKey(event)) return;
1472 if (menuKey(event)) return;
1473 if (readerKey(event)) return;
1474 if (((event.modifierState&ModifierState.ctrl) != 0 && event.key == Key.C) ||
1475 ((event.modifierState&ModifierState.ctrl) != 0 && event.key == Key.Insert))
1477 string ltext = laytext.getMarkedText();
1478 if (ltext.length) {
1479 setClipboardText(sdwindow, ltext);
1480 setPrimarySelection(sdwindow, ltext);
1481 setSecondarySelection(sdwindow, ltext);
1486 delegate (MouseEvent event) {
1487 if (sdwindow.closed) return;
1489 int linkAt (int msx, int msy) {
1490 if (laytext !is null && bookmeta !is null) {
1491 if (msx >= startX && msx <= endX && msy >= startY && msy <= endY) {
1492 auto widx = laytext.wordAtXY(msx-startX, topY+msy-startY);
1493 if (widx >= 0) {
1494 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1495 auto w = laytext.wordByIndex(widx);
1496 while (widx >= 0) {
1497 //conwriteln("word #", widx, "; href=", w.style.href);
1498 if (!w.style.href) break;
1499 if (auto hr = w.wordNum in bookmeta.hrefs) {
1500 dstring href = hr.name;
1501 if (href.length > 1 && href[0] == '#') {
1502 href = href[1..$];
1503 foreach (const ref id; bookmeta.ids) {
1504 if (id.name == href) {
1505 //pushPosition();
1506 //goTo(id.wordidx);
1507 return id.wordidx;
1510 //conwriteln("id '", hr.name, "' not found!");
1511 return -1;
1514 --widx;
1515 --w;
1520 return -1;
1523 lastMMove = MonoTime.currTime;
1524 if (mouseHidden || mouseFadingAway) {
1525 mouseHidden = false;
1526 mouseFadingAway = false;
1527 mouseAlpha = 1.0f;
1528 refresh();
1530 if (mouseX != event.x || mouseY != event.y) {
1531 mouseX = event.x;
1532 mouseY = event.y;
1533 refresh();
1535 if (!menuMouse(event) && !showShip) {
1536 if (mouseStartMarking && event.type == MouseEventType.motion) {
1537 if (laytext && event.x >= startX && event.x <= endX &&
1538 event.y >= startY && event.y <= endY)
1540 laytext.setMark(laytext.MarkType.End, event.x-startX, topY+event.y-startY);
1541 refresh();
1544 if (event.type == MouseEventType.buttonPressed) {
1545 switch (event.button) {
1546 case MouseButton.wheelUp: hardScrollBy(-42); break;
1547 case MouseButton.wheelDown: hardScrollBy(42); break;
1548 case MouseButton.left:
1549 auto wid = linkAt(event.x, event.y);
1550 if (wid >= 0) {
1551 pushPosition();
1552 goTo(wid);
1553 } else {
1554 if (laytext && event.x >= startX && event.x <= endX &&
1555 event.y >= startY && event.y <= endY)
1557 mouseStartMarking = true;
1558 laytext.setMark(laytext.MarkType.Both, event.x-startX, topY+event.y-startY);
1559 refresh();
1562 break;
1563 case MouseButton.right:
1564 popPosition();
1565 break;
1566 default:
1568 } else if (event.type == MouseEventType.buttonReleased) {
1569 if (event.button == MouseButton.left) {
1570 mouseStartMarking = false;
1573 } else {
1574 mouseStartMarking = false;
1576 mouseHigh = (linkAt(event.x, event.y) >= 0);
1578 delegate (dchar ch) {
1579 if (sdwindow.closed) return;
1580 if (glconCharEvent(ch)) return;
1581 if (menuChar(ch)) return;
1582 //if (ch == '`') { concmd("r_console tan"); return; }
1585 closeWindow();
1587 childTid.send(QuitWork());
1588 while (formatWorks >= 0) processThreads();
1592 // ////////////////////////////////////////////////////////////////////////// //
1593 struct FlibustaUrl {
1594 string fullUrl; // onion
1595 string host;
1596 string id;
1598 this (const(char)[] aurl) {
1599 import std.format : format;
1600 aurl = aurl.xstrip();
1601 auto flibustaRE = regex(`^(?:https?://)?(?:(?:(?:www\.)?flibusta\.[^/]+)|(?:flibusta[^.]*.onion))/b/(\d+)`);
1602 auto ct = aurl.matchFirst(flibustaRE);
1603 if (!ct.empty) {
1604 fullUrl = "http://flibustaongezhld6dibs2dps6vm4nvqg2kp7vgowbu76tzopgnhazqd.onion/b/%s/fb2".format(ct[1]);
1605 id = ct[1].idup;
1606 host = "flibustaongezhld6dibs2dps6vm4nvqg2kp7vgowbu76tzopgnhazqd.onion";
1607 } else {
1608 // add protocol
1609 auto protoRE = regex(`^([^:/]+):`);
1610 auto protoMt = aurl.matchFirst(protoRE);
1611 if (protoMt.empty) fullUrl = "http:%s%s".format((aurl[0] == '/' ? "" : "//"), aurl);
1612 // add host
1613 auto hostRE = regex(`^(?:[^:/]+)://([^/]+)`);
1614 auto hostMt = fullUrl.matchFirst(hostRE);
1615 if (hostMt.empty) { fullUrl = null; return; }
1616 host = hostMt[1].idup;
1620 @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (fullUrl.length > 0); }
1621 @property bool isFlibusta () const pure nothrow @safe @nogc { pragma(inline, true); return (id.length > 0); }
1622 @property bool isOnion () const pure nothrow @safe @nogc { pragma(inline, true); return host.endsWithCI(".onion"); }
1626 // ////////////////////////////////////////////////////////////////////////// //
1627 string dbFindInCache() (in auto ref FlibustaUrl furl) {
1628 if (!furl.valid) return null;
1629 auto stmt = filedb.statement(`
1630 SELECT filename AS filename
1631 FROM flubusta_cache
1632 WHERE flibusta_id=:flid
1633 LIMIT 1
1636 string fname;
1637 foreach (auto row; stmt.bindText(":flid", furl.id).range) {
1638 fname = row.filename!string;
1641 if (fname.length == 0) return null;
1643 import std.file : exists;
1644 try {
1645 if (fname.exists) return fname;
1646 } catch (Exception e) {}
1648 // no disk file, delete from cache
1649 stmt = filedb.statement(`
1650 DELETE FROM flubusta_cache
1651 WHERE flibusta_id=:flid
1653 stmt.bindText(":flid", furl.id).doAll();
1655 return null;
1659 void dbPutToCache() (in auto ref FlibustaUrl furl, const(char)[] fname) {
1660 if (!furl.valid || fname.length == 0 || furl.id.length == 0) return;
1662 auto stmt = filedb.statement(`
1663 INSERT INTO flubusta_cache
1664 ( flibusta_id, filename)
1665 VALUES(:flibusta_id,:filename)
1666 ON CONFLICT(flibusta_id)
1667 DO UPDATE
1668 SET filename=:filename
1670 stmt
1671 .bindText(":flibusta_id", furl.id)
1672 .bindText(":filename", fname)
1673 .doAll();
1677 // ////////////////////////////////////////////////////////////////////////// //
1678 // returns file name
1679 string fileDown() (in auto ref FlibustaUrl furl) {
1680 if (!furl.valid || !furl.isFlibusta) return null;
1682 string cachedFName = dbFindInCache(furl);
1683 if (cachedFName.length) return cachedFName;
1685 // content-disposition: attachment; filename="Divov_Sled-zombi.1lzb6Q.96382.fb2.zip"
1686 auto cdRE0 = regex(`^\s*attachment\s*;\s*filename="(.+?)"`, "i");
1687 auto cdRE1 = regex(`^\s*attachment\s*;\s*filename=([^;]+?)`, "i");
1688 auto cdLoc0 = regex(`.*/b\.fb2/([^/]+?\.fb2\.zip)$`, "i");
1690 auto http = HTTP(furl.host);
1691 http.method = HTTP.Method.get;
1692 http.url = furl.fullUrl;
1694 string fname = null;
1695 string tmpfname = null;
1696 string fnps = null;
1697 bool alreadyDowned = false;
1698 VFile fo;
1700 http.onReceiveHeader = delegate (in char[] key, in char[] value) {
1701 //writeln(key ~ ": " ~ value);
1702 if (key.strEquCI("content-disposition")) {
1703 auto ct = value.matchFirst(cdRE0);
1704 if (ct.empty) ct = value.matchFirst(cdRE1);
1705 if (ct[1].length) {
1706 auto fnp = ct[1].xstrip;
1707 auto lslpos = fnp.lastIndexOf('/');
1708 if (lslpos > 0) fnp = fnp[lslpos+1..$];
1709 if (fnp.length == 0) {
1710 fname = null;
1711 } else {
1712 import std.file : exists, mkdirRecurse;
1713 import std.path;
1714 string xfn = buildPath(RcDir, "cache");
1715 xfn.mkdirRecurse();
1716 char[] xxname;
1717 xxname.reserve(fnp.length);
1718 foreach (char ch; fnp) {
1719 if (ch <= ' ' || ch == 127) ch = '_';
1720 xxname ~= ch;
1722 fnps = cast(string)xxname; // it is safe to cast here
1723 fname = buildPath(xfn, fnps);
1724 tmpfname = fname~".down.part";
1726 if (fname.exists) {
1727 alreadyDowned = true;
1728 throw new Exception("already here");
1729 //throw new FileAlreadyDowned("already here");
1732 //write("\r", fnp, " [", furl.fullUrl, "]\e[K");
1735 } else if (key.strEquCI("location")) {
1736 auto mt = value.matchFirst(cdLoc0);
1737 if (!mt.empty && mt[1].length > 0) {
1738 import std.file : exists, mkdirRecurse;
1739 import std.path;
1740 auto fnp = mt[1].xstrip;
1741 string xfn = buildPath(RcDir, "cache");
1742 xfn.mkdirRecurse();
1743 char[] xxname;
1744 xxname.reserve(fnp.length);
1745 foreach (char ch; fnp) {
1746 if (ch <= ' ' || ch == 127) ch = '_';
1747 xxname ~= ch;
1749 fnps = cast(string)xxname; // it is safe to cast here
1750 fname = buildPath(xfn, fnps);
1751 tmpfname = fname~".down.part";
1753 if (fname.exists) {
1754 alreadyDowned = true;
1755 throw new Exception("already here");
1756 //throw new FileAlreadyDowned("already here");
1759 //write("\r", fnp, " [", furl.fullUrl, "]\e[K");
1764 http.onReceive = delegate (ubyte[] data) {
1765 if (!fo.isOpen) {
1766 if (fname.length == 0) throw new Exception("no file name found in headers");
1767 //writeln(" downloading to ", fname);
1768 fo = VFile(tmpfname, "w");
1770 fo.rawWriteExact(data);
1771 return data.length;
1774 MonoTime lastProgTime = MonoTime.zero;
1775 enum BarLength = 68;
1776 bool doProgUpdate = true;
1777 char[1024] buf = void;
1778 int oldDots = -1, oldPrc = -1;
1779 uint bufpos = 0;
1780 int stickPos = 1;
1781 immutable string stickStr = `|/-\`;
1783 // will set `doProgUpdate`, and update `oldXXX`
1784 void buildPBar (usize dlTotal, usize dlNow) {
1785 void put (const(char)[] s...) nothrow {
1786 if (s.length == 0) return;
1787 if (bufpos >= buf.length) return;
1788 int left = cast(int)buf.length-bufpos;
1789 if (s.length > left) s = s[0..left];
1790 assert(s.length > 0);
1791 import core.stdc.string : memcpy;
1792 memcpy(buf.ptr+bufpos, s.ptr, s.length);
1793 bufpos += cast(int)s.length;
1795 void putprc (int prc) {
1796 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1797 if (bufpos >= buf.length || buf.length-bufpos < 5) return; // oops
1798 import core.stdc.stdio;
1799 bufpos += cast(int)snprintf(buf.ptr+bufpos, 5, "%3d%%", prc);
1801 void putCommaNum (usize n, usize max=0) {
1802 char[128] buf = void;
1803 if (max < n) {
1804 put(intWithCommas(buf[], n));
1805 } else {
1806 auto len = intWithCommas(buf[], max).length;
1807 auto pt = intWithCommas(buf[], n);
1808 while (len-- > pt.length) put(" ");
1809 put(pt);
1812 bufpos = 0;
1813 put("\r");
1814 put(fnps);
1815 put(" [");
1816 auto barpos = bufpos;
1817 foreach (immutable _; 0..BarLength) put(" ");
1818 put("]");
1819 if (dlTotal > 0) {
1820 int prc = cast(int)(cast(ulong)100*dlNow/dlTotal);
1821 if (prc < 0) prc = 0; else if (prc > 100) prc = 100;
1822 int dots = cast(int)(cast(ulong)BarLength*dlNow/dlTotal);
1823 if (dots < 0) dots = 0; else if (dots > BarLength) dots = BarLength;
1824 if (prc != oldPrc || dots != oldDots) {
1825 doProgUpdate = true;
1826 oldPrc = prc;
1827 oldDots = dots;
1829 put(" [");
1830 putCommaNum(dlNow, dlTotal);
1831 put("/");
1832 putCommaNum(dlTotal);
1833 put("] ");
1834 putprc(prc);
1835 // dots
1836 foreach (immutable dp; 0..dots) if (barpos+dp < buf.length) buf[barpos+dp] = '.';
1837 } else {
1838 put("?\e[K");
1839 if (oldDots != -1 || oldPrc != -1) doProgUpdate = true;
1840 oldDots = -1;
1841 oldPrc = -1;
1845 http.onProgress = delegate (usize dltotal, usize dlnow, usize ultotal, usize ulnow) {
1846 //writeln("Progress ", dltotal, ", ", dlnow, ", ", ultotal, ", ", ulnow);
1847 if (fname.length == 0) {
1848 auto ct = MonoTime.currTime;
1849 if ((ct-lastProgTime).total!"msecs" >= 100) {
1850 write("\x08", stickStr[stickPos]);
1851 stickPos = (stickPos+1)%cast(int)stickStr.length;
1852 lastProgTime = ct;
1854 return 0;
1856 buildPBar(dltotal, dlnow);
1857 if (doProgUpdate) { write("\e[?7l", buf[0..bufpos], "\e[K\e[?7h"); doProgUpdate = false; }
1858 //if (dltotal == 0) return 0;
1859 //auto ct = MonoTime.currTime;
1860 //if ((ct-lastProgTime).total!"msecs" < 1000) return 0;
1861 //lastProgTime = ct;
1862 //writef("\r%s [%s] -- [%s/%s] %3u%%\e[K", fnps, host, intWithCommas(dlnow), intWithCommas(dltotal), 100UL*dlnow/dltotal);
1863 return 0;
1866 if (furl.isOnion) {
1867 http.proxyType = HTTP.CurlProxy.socks5_hostname;
1868 http.proxy = "127.0.0.1";
1869 http.proxyPort = 9050;
1872 try {
1873 //write("downloading from [", host, "]: ", realUrl, " ... ");
1874 write("downloading from Flibusta: ", furl.fullUrl, " ... ", stickStr[0]);
1875 http.perform();
1876 if (fo.isOpen) {
1877 buildPBar(cast(uint)fo.size, cast(uint)fo.size);
1878 } else {
1879 buildPBar(1, 1);
1881 writeln(buf[0..bufpos], "\e[K");
1882 //write("\r\e[K");
1883 } catch (Exception e) {
1884 if (/*cast(FileAlreadyDowned)e*/alreadyDowned) {
1885 write("\r", fname, " already downloaded.\e[K");
1886 return fname; // already here
1888 if (tmpfname.length) {
1889 import std.exception : collectException;
1890 import std.file : remove;
1891 collectException(tmpfname.remove);
1893 throw e;
1896 if (fo.isOpen) {
1897 // something was downloaded, rename it
1898 import std.file : rename;
1899 fo.close();
1900 rename(tmpfname, fname);
1901 dbPutToCache(furl, fname);
1902 return fname;
1905 return null;
1909 // ////////////////////////////////////////////////////////////////////////// //
1910 void main (string[] args) {
1911 import std.path;
1913 conRegVar!oglConScale(1, 4, "r_conscale", "console scale");
1915 conProcessQueue(256*1024); // load config
1916 conProcessArgs!true(args);
1917 conProcessQueue(256*1024);
1919 universe = Galaxy(0);
1921 if (args.length == 1) {
1922 string lnn = getLatestFileName();
1923 if (lnn.length) args ~= lnn;
1924 } else {
1925 if (args.length != 2) assert(0, "invalid number of arguments");
1926 auto furl = FlibustaUrl(args[1]);
1927 if (furl.valid && furl.isFlibusta) {
1928 string fn = fileDown(furl);
1929 if (fn.length == 0) assert(0, "can't download file");
1930 args[1] = fn;
1934 if (args.length == 1) assert(0, "no filename");
1936 readConfig();
1938 bookFileName = args[1];
1939 run();