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
;
21 import std
.concurrency
;
25 import arsd
.simpledisplay
;
29 import iv
.nanovega
.textlayouter
;
30 import iv
.nanovega
.blendish
;
31 import iv
.nanovega
.perf
;
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 // ////////////////////////////////////////////////////////////////////////// //
122 // all visible hrefs on a page
123 __gshared HrefInfo
[] hreflist
;
124 __gshared
int hrefcurr
= -1;
127 // ////////////////////////////////////////////////////////////////////////// //
130 float PulseScale
= 8; // ratio of "tail" to "acceleration"
133 float PulseNormalize
= 1;
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
;
146 // the previous animation ended here:
147 float start
= exp(-1.0f);
148 // simple viscous drag
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);
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; }
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;
202 shipModelIndex
= idx
;
203 if (shipModel
!is null) {
204 shipModel
.glUnload();
205 shipModel
.freeData();
212 shipModel
= new EliteModel(eliteShipFiles
[shipModelIndex
]);
213 shipModel
.glUpload();
214 shipModel
.freeImages();
215 } catch (Exception e
) {}
216 //shipModel = eliteShips[shipModelIndex];
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();
242 int widx
= getCurrentFileWordIndex();
244 xiniParse(VFile(stateFileName),
248 if (widx
>= 0) goTo(widx
);
249 } catch (Exception
) {}
252 void doSaveState (bool forced
=false) {
254 if (formatWorks
!= 0) return;
255 if (!doSaveCheck
) return;
256 auto ct
= MonoTime
.currTime
;
257 if (ct
< nextSaveTime
) return;
259 if (laytext
is null || laytext
.lineCount
== 0 || formatWorks
!= 0) return;
262 //auto fo = VFile(stateFileName, "w");
263 if (laytext
!is null && laytext
.lineCount
) {
264 auto lnum
= laytext
.findLineAtY(topY
);
266 //fo.writeln("wordindex=", laytext.line(lnum).wstart);
267 updateCurrentFileWordIndex(laytext
.line(lnum
).wstart
);
270 } catch (Exception
) {}
274 void stateChanged () {
277 nextSaveTime
= MonoTime
.currTime
+10.seconds
;
282 void hardScrollBy (int delta
) {
283 if (delta
== 0 || laytext
is null || laytext
.lineCount
== 0) return;
286 if (topY
>= laytext
.textHeight
-textHeight
) topY
= cast(int)laytext
.textHeight
-textHeight
;
287 if (topY
< 0) topY
= 0;
294 void scrollBy (int delta
) {
295 if (delta
== 0 || laytext
is null || laytext
.lineCount
== 0) return;
300 // scrolling up, mark top line
301 newYLine
= laytext
.findLineAtY(topY
);
303 // scrolling down, mark bottom line
304 newYLine
= laytext
.findLineAtY(topY
+textHeight
-2);
307 conwriteln("scrollBy: delta=", delta
, "; newYLine=", newYLine
, "; topLine=", laytext
.findLineAtY(topY
));
309 if (newYLine
< 0) newYFade
= false;
314 if (laytext
is null) return;
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;
336 void goTo (uint widx
) {
337 if (laytext
is null) return;
338 auto lidx
= laytext
.findLineWithWord(widx
);
340 assert(lidx
< laytext
.lineCount
);
342 if (topY
!= laytext
.line(lidx
).y
) {
343 topY
= laytext
.line(lidx
).y
;
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
;
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
);
376 auto newY
= laytext
.line(lidx
).y
;
386 void relayout (bool forced
=false) {
387 if (laytext
!is null) {
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;
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
);
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;
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;
433 if (bookmeta
.sections
.length
> 1) {
434 auto lidx
= laytext
.findLineAtY(topY
);
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
;
444 //if (scidx > 0 && scidx < bookmeta.sections.length) sc = bookmeta.sections[scidx];
445 if (force || scidx
!= currentSection
) {
446 currentSection
= scidx
;
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 () {
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
, () {
463 foreach (const ref sc
; bookmeta
.sections
) items
~= sc
.name
;
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
) {
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 () {
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
;
498 foreach (const ref BookInfo bi
; recentFiles
) {
500 if (bi
.seqname
.length
) {
502 if (bi
.seqnum
) { s
~= to
!string(bi
.seqnum
); s
~= ": "; }
503 //conwriteln(bi.seqname);
507 if (bi
.author
.length
) { s
~= " \xe2\x80\x94 "; s
~= bi
.author
; }
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
) {
545 } else if (res
>= 0) {
546 if (onPopupSelect
!is null) onPopupSelect(res
);
548 if (popupNoShipKill
) popupNoShipKill
= false; else freeShipModel();
550 } else if (res
== PopupMenu
.NotMine
) {
551 if (onPopupKeyEvent
!is null && onPopupKeyEvent(event
)) {
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
) {
571 } else if (res
>= 0) {
572 if (onPopupSelect
!is null) onPopupSelect(res
);
574 if (popupNoShipKill
) popupNoShipKill
= false; else freeShipModel();
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
) {
591 } else if (res
>= 0) {
592 if (onPopupSelect
!is null) onPopupSelect(res
);
594 if (popupNoShipKill
) popupNoShipKill
= false; else freeShipModel();
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] == '#') {
607 foreach (const ref id
; bookmeta
.ids
) if (id
.name
== href
) return id
.wordidx
;
614 bool readerKey (KeyEvent event
) {
615 if (formatWorks
!= 0) return false;
616 if (!event
.pressed
) {
618 case Key
.Up
: arrowDir
= 0; return true;
619 case Key
.Down
: arrowDir
= 0; return true;
626 if (event
.modifierState
&ModifierState
.shift
) {
627 //goto case Key.PageUp;
628 hardScrollBy(toMove
); toMove
= 0;
629 scrollBy(-textHeight
/3*2);
631 //goto case Key.PageDown;
632 hardScrollBy(toMove
); toMove
= 0;
633 scrollBy(textHeight
/3*2);
640 hardScrollBy(toMove
); toMove
= 0;
641 hardScrollBy(-(textHeight
> 32 ? textHeight
-32 : textHeight
));
644 hardScrollBy(toMove
); toMove
= 0;
645 hardScrollBy(textHeight
> 32 ? textHeight
-32 : textHeight
);
656 if (laytext
!is null) {
657 if (event
== "C-H") goEnd(); else goHome();
661 if (hreflist
.length
) {
662 int dir
= (event
== "Tab" ?
1 : event
== "S-Tab" ?
-1 : 0);
665 hrefcurr
= (dir
> 0 ?
0 : cast(int)hreflist
.length
-1);
667 hrefcurr
= (hrefcurr
+cast(int)hreflist
.length
+dir
)%cast(int)hreflist
.length
;
674 if (hrefcurr
>= 0 && event
== "Enter") {
675 auto wid
= getCurrHrefIdx();
688 bool controlKey (KeyEvent event
) {
689 if (!event
.pressed
) return false;
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; }
699 createQuitMenu(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;
708 if (event
.modifierState
== ModifierState
.ctrl
&& eliteShipFiles
.length
> 0) {
710 showShip
= !showShip
;
711 if (showShip
) ensureShipModel(); else freeShipModel();
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;
720 if (formatWorks
== 0 && !showShip
&& !inMenu
&& !inGalaxyMap
) {
727 if (formatWorks
== 0 && event
.modifierState
== ModifierState
.alt
) {
734 if (!inMenu
&& !showShip
) {
735 inGalaxyMap
= !inGalaxyMap
;
740 if (formatWorks
== 0 && event
.modifierState
== ModifierState
.ctrl
) relayout(true);
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;
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 // ////////////////////////////////////////////////////////////////////////// //
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
;
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);
792 //formatWorks = -1; //FIXME
800 //sdwindow.redrawOpenGlSceneNow();
801 //bookText = loadBook(newBookFileName);
802 //newBookFileName = null;
804 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
806 //conwriteln("*** loading new book: '", filename, "'");
807 hreflist
.unsafeArrayClear();
809 childTid
.send(ReformatWork(null, filename
, GWidth
, GHeight
));
814 if (formatWorks
< 0) formatWorks
= 1; else ++formatWorks
;
815 childTid
.send(ReformatWork(cast(shared)bookText
, null, GWidth
, GHeight
));
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();
826 BookMetadata meta
= cast(BookMetadata
)w
.meta
;
829 BookText
bt = cast(BookText
)w
.booktext
;
831 if (bt !is bookText
) {
835 currentSection
= -666;
836 setWindowTitle(true);
837 //sdwindow.title = bookText.title~" \xe2\x80\x94 "~bookText.authorFirst~" "~bookText.authorLast;
838 } else if (bookmeta
is null) {
842 if (isQuitRequested || formatWorks
<= 0) return;
846 if (formatWorks
== 0) reformat();
850 if (formatWorks
!= 0) return;
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();
872 void closeWindow () {
873 doSaveState(true); // forced state save
874 if (!sdwindow
.closed
&& vg
!is null) {
884 sdwindow
.visibleForTheFirstTime
= delegate () {
885 sdwindow
.setAsCurrentOpenGlContext(); // make this window active
886 sdwindow
.vsync
= false;
887 //sdwindow.useGLFinish = false;
888 //glbindLoadFunctions();
891 NVGContextFlag
[4] flagList
;
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
]);
898 conwriteln("Could not init nanovg.");
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
);
915 sdwindow
.redrawOpenGlScene();
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
);
926 textHeight
= GHeight
-8;
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;
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
);
954 conwriteln("cnt=", cnt++);
957 // update window title
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
);
966 needRedrawFlag
= (fps
!is null && fpsVisible
);
967 if (vg
is null) return;
969 scope(exit
) vg
.releaseImages();
970 vg
.beginFrame(GWidth
, GHeight
, 1);
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
);
984 if (laytext
is null) {
985 if (shipModel
is null) {
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");
996 } else if (hrefcurr
>= 0) {
997 owidx
= hreflist
[hrefcurr
].widx
;
1002 hreflist
.unsafeArrayClear();
1004 if (laytext
!is null && laytext
.lineCount
) {
1006 vg
.fillColor(colorText
);
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
);
1025 //vg.strokeColor(c);
1026 lastStyle
= w
.style
;
1029 // line highlighting
1030 if (newYFade
&& newYLine
== lidx
) vg
.fillColor(nvgLerpRGBA(colorText
, colorTextHi
, newYAlpha
));
1031 auto oid
= w
.objectIdx
;
1034 laytext
.objectAtIndex(oid
).draw(vg
, startx
+w
.x
, drawY
+ln
.h
+ln
.desc
);
1037 if (laytext
.isMarkedWord(w
.wordNum
)) {
1039 vg
.fillColor(nvgRGB(0, 0, 255));
1041 vg
.rect(startx
+w
.x
, drawY
, (laytext
.isMarkedWord(w
.wordNum
+1) ? w
.fullwidth
: w
.width
), w
.h
);
1044 vg
.fillColor(nvgRGB(255, 255, 255));
1047 vg
.text(startx
+w
.x
, drawY
+ln
.h
+ln
.desc
, laytext
.wordText(w
));
1050 if (drawY
>= 0 && drawY
+ln
.h
<= GHeight
&& isWordHref(w
)) {
1052 hif
.widx
= cast(int)w
.wordNum
;
1053 hif
.x0
= startx
+w
.x
;
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
) {
1067 vg
.strokeColor(nvgRGB(0, 0, 255));
1068 vg
.rect(startx
+w
.x
, drawY
, w
.w
, w
.h
);
1072 if (newYFade
&& newYLine
== lidx
) vg
.fillColor(NVGColor(lastStyle
.color
));
1074 if (newYFade
&& newYLine
== lidx
) markerY
= drawY
;
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; }
1086 if (hrefcurr
>= 0) {
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
);
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
);
1102 // draw scroll marker
1103 if (markerY
!= -666) {
1105 vg
.fillColor(NVGColor(0.3f, 0.3f, 0.3f, newYAlpha
));
1106 vg
.rect(startX
, markerY
, GWidth
, 2);
1112 if (colorDim
.a
!= 1) {
1113 //vg.scissor(0, 0, GWidth, GHeight);
1116 vg
.fillColor(colorDim
);
1117 vg
.rect(0, 0, GWidth
, GHeight
);
1122 // dim more if menu is active
1123 if (inMenu || showShip || formatWorks
!= 0) {
1124 //vg.scissor(0, 0, GWidth, GHeight);
1127 //vg.globalAlpha(0.5);
1128 vg
.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks
!= 0 ?
127 : 64)));
1129 vg
.rect(0, 0, GWidth
, GHeight
);
1133 if (shipModel
!is null) {
1135 float zz
= shipModel
.bbox
[1].z
-shipModel
.bbox
[0].z
;
1149 drawModel(shipAngle
, shipModel
);
1150 vg
.beginFrame(GWidth
, GHeight
, 1);
1155 if (formatWorks
== 0) {
1157 //vg.beginFrame(GWidth, GHeight, 1);
1158 //vg.scissor(0, 0, GWidth, GHeight);
1165 if (fps
!is null && fpsVisible
) {
1166 //vg.beginFrame(GWidth, GHeight, 1);
1167 //vg.scissor(0, 0, GWidth, GHeight);
1169 fps
.render(vg
, GWidth
-fps
.width
-4, GHeight
-fps
.height
-4);
1173 if (inGalaxyMap
) drawGalaxy(vg
);
1176 if (curImg
.valid
&& !mouseHidden
) {
1178 //vg.beginFrame(GWidth, GHeight, 1);
1180 //vg.scissor(0, 0, GWidth, GHeight);
1182 vg
.imageSize(curImg
, w
, h
);
1183 if (mouseFadingAway
) {
1185 if (mouseAlpha
<= 0) { mouseFadingAway
= false; mouseHidden
= true; }
1189 vg
.globalAlpha(mouseAlpha
);
1191 vg
.fillPaint(vg
.imagePattern(mouseX
, mouseY
, w
, h
, 0, curImg
, 1));
1193 vg
.fillPaint(vg
.imagePattern(mouseX
, mouseY
, w
, h
, 0, curImgWhite
, 1));
1195 vg
.rect(mouseX
, mouseY
, w
, h
);
1200 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
1201 vg.rect(mouseX, mouseY, w, h);
1211 void processThreads () {
1212 ReformatWorkComplete wd
;
1214 bool workDone
= false;
1215 auto res
= receiveTimeout(Duration
.zero
,
1219 (ReformatWorkComplete w
) {
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;
1248 if (showShip
) ensureShipModel(); else freeShipModel();
1254 if (currPopup
!is null) {
1260 onPopupSelect
= null;
1261 })("menu_close", "close current popup menu");
1264 if (formatWorks
== 0 && !showShip
&& !inGalaxyMap
) {
1268 createSectionMenu();
1271 })("menu_section", "show section menu");
1274 if (formatWorks
== 0 && !showShip
&& !inGalaxyMap
) {
1281 })("menu_recent", "show recent menu");
1283 sdwindow
.eventLoop(1000/34,
1286 if (sdwindow
.closed
) return;
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
;
1298 //curt = MonoTime.currTime;
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);
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
;
1316 arrowSpeed
-= 0.025; //*(lastArrowDir ? 4 : 1);
1317 if (arrowSpeed
<= 0) {
1320 lastArrowDir
= arrowDir
;
1325 import std
.math
: abs
;
1327 immutable int sign
= (toMove
< 0 ?
-1 : 1);
1329 static int arrowSpeed
= 0;
1330 if (arrowSpeed
== 0) arrowSpeed
= 16;
1331 if (abs(toMove
) <= arrowSpeed
) {
1333 if (arrowSpeed
< 4) arrowSpeed
= 4;
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
);
1343 if (toMove
== 0) arrowSpeed
= 0;
1344 nextFadeTime
= ctt
+500.msecs
;
1348 int toMove
= cast(int)(pulse
.pulse(arrowSpeed
)*64);
1349 //if (arrowSpeed) { import std.stdio; writeln("lastdir=", lastArrowDir, "; dir=", arrowDir, "; as=", arrowSpeed, "; toMove=", toMove); }
1351 toMove
*= lastArrowDir
;
1352 hardScrollBy(toMove
);
1353 nextFadeTime
= ctt
+500.msecs
;
1360 import std.math : abs;
1361 immutable int sign = (toMove < 0 ? -1 : 1);
1363 if (arrowSpeed == 0) arrowSpeed = 16;
1364 if (abs(toMove) <= arrowSpeed) {
1366 if (arrowSpeed < 4) arrowSpeed = 4;
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);
1376 if (toMove == 0) arrowSpeed = 0;
1377 nextFadeTime = ctt+500.msecs;
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);
1385 } else if (arrowSpeed != 0) {
1386 if (arrowSpeed < 0) {
1387 if ((arrowSpeed += 4) > 0) arrowSpeed = 0;
1389 if ((arrowSpeed -= 4) < 0) arrowSpeed = 0;
1392 hardScrollBy(arrowSpeed);
1399 if (ctt
>= nextFadeTime
) {
1400 if ((newYAlpha
-= 0.1) <= 0) {
1403 nextFadeTime
= ctt
+25.msecs
;
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();
1417 if (shipModel
!is null) {
1419 if (shipAngle
< 359) shipAngle
+= 360;
1423 if (!mouseFadingAway
) {
1424 if (!mouseHidden
&& !mouseHigh
) {
1425 if ((ctt
-lastMMove
).total
!"msecs" > 2500) {
1426 //mouseHidden = true;
1427 mouseFadingAway
= true;
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();
1441 if (newBookFileName
.length
&& formatWorks
== 0 && vg
!is null) {
1442 doSaveState(true); // forced state save
1445 loadAndFormat(newBookFileName
);
1446 newBookFileName
= null;
1447 sdwindow
.redrawOpenGlSceneNow();
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;
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();
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
);
1494 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1495 auto w
= laytext
.wordByIndex(widx
);
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] == '#') {
1503 foreach (const ref id
; bookmeta
.ids
) {
1504 if (id
.name
== href
) {
1510 //conwriteln("id '", hr.name, "' not found!");
1523 lastMMove
= MonoTime
.currTime
;
1524 if (mouseHidden || mouseFadingAway
) {
1525 mouseHidden
= false;
1526 mouseFadingAway
= false;
1530 if (mouseX
!= event
.x || mouseY
!= event
.y
) {
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
);
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
);
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
);
1563 case MouseButton
.right
:
1568 } else if (event
.type
== MouseEventType
.buttonReleased
) {
1569 if (event
.button
== MouseButton
.left
) {
1570 mouseStartMarking
= false;
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; }
1587 childTid
.send(QuitWork());
1588 while (formatWorks
>= 0) processThreads();
1592 // ////////////////////////////////////////////////////////////////////////// //
1593 struct FlibustaUrl
{
1594 string fullUrl
; // onion
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
);
1604 fullUrl
= "http://flibustaongezhld6dibs2dps6vm4nvqg2kp7vgowbu76tzopgnhazqd.onion/b/%s/fb2".format(ct
[1]);
1606 host
= "flibustaongezhld6dibs2dps6vm4nvqg2kp7vgowbu76tzopgnhazqd.onion";
1609 auto protoRE
= regex(`^([^:/]+):`);
1610 auto protoMt
= aurl
.matchFirst(protoRE
);
1611 if (protoMt
.empty
) fullUrl
= "http:%s%s".format((aurl
[0] == '/' ?
"" : "//"), aurl
);
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
1632 WHERE flibusta_id=:flid
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
;
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();
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)
1668 SET filename=:filename
1671 .bindText(":flibusta_id", furl
.id
)
1672 .bindText(":filename", fname
)
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;
1697 bool alreadyDowned
= false;
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
);
1706 auto fnp
= ct
[1].xstrip
;
1707 auto lslpos
= fnp
.lastIndexOf('/');
1708 if (lslpos
> 0) fnp
= fnp
[lslpos
+1..$];
1709 if (fnp
.length
== 0) {
1712 import std
.file
: exists
, mkdirRecurse
;
1714 string xfn
= buildPath(RcDir
, "cache");
1717 xxname
.reserve(fnp
.length
);
1718 foreach (char ch
; fnp
) {
1719 if (ch
<= ' ' || ch
== 127) ch
= '_';
1722 fnps
= cast(string
)xxname
; // it is safe to cast here
1723 fname
= buildPath(xfn
, fnps
);
1724 tmpfname
= fname
~".down.part";
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
;
1740 auto fnp
= mt
[1].xstrip
;
1741 string xfn
= buildPath(RcDir
, "cache");
1744 xxname
.reserve(fnp
.length
);
1745 foreach (char ch
; fnp
) {
1746 if (ch
<= ' ' || ch
== 127) ch
= '_';
1749 fnps
= cast(string
)xxname
; // it is safe to cast here
1750 fname
= buildPath(xfn
, fnps
);
1751 tmpfname
= fname
~".down.part";
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
) {
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
);
1774 MonoTime lastProgTime
= MonoTime
.zero
;
1775 enum BarLength
= 68;
1776 bool doProgUpdate
= true;
1777 char[1024] buf
= void;
1778 int oldDots
= -1, oldPrc
= -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;
1804 put(intWithCommas(buf
[], n
));
1806 auto len
= intWithCommas(buf
[], max
).length
;
1807 auto pt
= intWithCommas(buf
[], n
);
1808 while (len
-- > pt
.length
) put(" ");
1816 auto barpos
= bufpos
;
1817 foreach (immutable _
; 0..BarLength
) put(" ");
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;
1830 putCommaNum(dlNow
, dlTotal
);
1832 putCommaNum(dlTotal
);
1836 foreach (immutable dp
; 0..dots
) if (barpos
+dp
< buf
.length
) buf
[barpos
+dp
] = '.';
1839 if (oldDots
!= -1 || oldPrc
!= -1) doProgUpdate
= true;
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
;
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);
1867 http
.proxyType
= HTTP
.CurlProxy
.socks5_hostname
;
1868 http
.proxy
= "127.0.0.1";
1869 http
.proxyPort
= 9050;
1873 //write("downloading from [", host, "]: ", realUrl, " ... ");
1874 write("downloading from Flibusta: ", furl
.fullUrl
, " ... ", stickStr
[0]);
1877 buildPBar(cast(uint)fo
.size
, cast(uint)fo
.size
);
1881 writeln(buf
[0..bufpos
], "\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
);
1897 // something was downloaded, rename it
1898 import std
.file
: rename
;
1900 rename(tmpfname
, fname
);
1901 dbPutToCache(furl
, fname
);
1909 // ////////////////////////////////////////////////////////////////////////// //
1910 void main (string
[] args
) {
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
;
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");
1934 if (args
.length
== 1) assert(0, "no filename");
1938 bookFileName
= args
[1];