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
;
54 //version = debug_draw;
57 // ////////////////////////////////////////////////////////////////////////// //
58 __gshared
int oglConScale
= 1;
61 // ////////////////////////////////////////////////////////////////////////// //
62 __gshared string bookFileName
;
64 __gshared
int formatWorks
= -1;
65 __gshared NVGContext vg
= null;
66 __gshared PerfGraph fps
;
67 __gshared
bool fpsVisible
= false;
68 __gshared BookText bookText
;
69 __gshared string newBookFileName
;
70 __gshared
uint[] posstack
;
71 __gshared SimpleWindow sdwindow
;
72 __gshared LayTextC laytext
;
73 __gshared BookInfo
[] recentFiles
;
74 __gshared BookMetadata bookmeta
;
76 __gshared
int textHeight
;
78 __gshared
int toMove
= 0; // for smooth scroller
79 __gshared
int topY
= 0;
80 __gshared
int arrowDir
= 0;
81 __gshared
int arrowSpeed
= 0;
83 __gshared
int newYLine
= -1; // index
84 __gshared
float newYAlpha
;
85 __gshared
bool newYFade
= false;
86 __gshared MonoTime nextFadeTime
;
88 __gshared NVGImage curImg
, curImgWhite
;
89 __gshared
int mouseX
= -666, mouseY
= -666;
90 __gshared
bool mouseHigh
= false;
91 __gshared
bool mouseHidden
= false;
92 __gshared MonoTime lastMMove
;
93 __gshared
float mouseAlpha
= 1.0f;
94 __gshared
int mouseFadingAway
= false;
96 __gshared
bool needRedrawFlag
= true;
98 __gshared
bool firstFormat
= true;
100 __gshared
bool inGalaxyMap
= false;
101 __gshared PopupMenu currPopup
;
102 __gshared
void delegate (int item
) onPopupSelect
;
103 __gshared
bool delegate (KeyEvent event
) onPopupKeyEvent
; // return `true` to close menu
104 __gshared
int shipModelIndex
= 0;
105 __gshared
bool popupNoShipKill
= false;
107 __gshared
bool doSaveCheck
= false;
108 __gshared MonoTime nextSaveTime
;
111 // ////////////////////////////////////////////////////////////////////////// //
112 void refresh () { needRedrawFlag
= true; }
113 void refreshed () { needRedrawFlag
= false; }
116 return (needRedrawFlag ||
(currPopup
!is null && currPopup
.needRedraw
));
120 @property bool inMenu () { return (currPopup
!is null); }
121 void closeMenu () { if (currPopup
!is null) currPopup
.destroy
; currPopup
= null; onPopupSelect
= null; onPopupKeyEvent
= null; }
124 void setShip (int idx
) {
125 if (eliteShipFiles
.length
) {
126 import core
.memory
: GC
;
127 if (idx
>= eliteShipFiles
.length
) idx
= cast(int)eliteShipFiles
.length
-1;
128 if (idx
< 0) idx
= 0;
129 if (shipModelIndex
== idx
&& shipModel
!is null) return;
131 shipModelIndex
= idx
;
132 if (shipModel
!is null) {
133 shipModel
.glUnload();
134 shipModel
.freeData();
141 shipModel
= new EliteModel(eliteShipFiles
[shipModelIndex
]);
142 shipModel
.glUpload();
143 shipModel
.freeImages();
144 } catch (Exception e
) {}
145 //shipModel = eliteShips[shipModelIndex];
150 void ensureShipModel () {
151 if (eliteShipFiles
.length
&& shipModel
is null) {
152 import std
.random
: uniform
;
153 setShip(uniform
!"[)"(0, eliteShipFiles
.length
));
157 void freeShipModel () {
158 if (!showShip
&& !inMenu
&& shipModel
!is null) {
159 import core
.memory
: GC
;
160 shipModel
.glUnload();
161 shipModel
.freeData();
172 xiniParse(VFile(stateFileName
),
175 if (widx
>= 0) goTo(widx
);
176 } catch (Exception
) {}
179 void doSaveState (bool forced
=false) {
181 if (formatWorks
!= 0) return;
182 if (!doSaveCheck
) return;
183 auto ct
= MonoTime
.currTime
;
184 if (ct
< nextSaveTime
) return;
186 if (laytext
is null || laytext
.lineCount
== 0 || formatWorks
!= 0) return;
189 auto fo
= VFile(stateFileName
, "w");
190 if (laytext
!is null && laytext
.lineCount
) {
191 auto lnum
= laytext
.findLineAtY(topY
);
192 if (lnum
>= 0) fo
.writeln("wordindex=", laytext
.line(lnum
).wstart
);
194 } catch (Exception
) {}
198 void stateChanged () {
201 nextSaveTime
= MonoTime
.currTime
+10.seconds
;
206 void hardScrollBy (int delta
) {
207 if (delta
== 0 || laytext
is null || laytext
.lineCount
== 0) return;
210 if (topY
>= laytext
.textHeight
-textHeight
) topY
= cast(int)laytext
.textHeight
-textHeight
;
211 if (topY
< 0) topY
= 0;
218 void scrollBy (int delta
) {
219 if (delta
== 0 || laytext
is null || laytext
.lineCount
== 0) return;
224 // scrolling up, mark top line
225 newYLine
= laytext
.findLineAtY(topY
);
227 // scrolling down, mark bottom line
228 newYLine
= laytext
.findLineAtY(topY
+textHeight
-2);
231 conwriteln("scrollBy: delta=", delta
, "; newYLine=", newYLine
, "; topLine=", laytext
.findLineAtY(topY
));
233 if (newYLine
< 0) newYFade
= false;
238 if (laytext
is null) return;
248 if (laytext
is null || laytext
.lineCount
< 2) return;
249 auto newY
= laytext
.line(laytext
.lineCount
-1).y
;
250 if (newY
>= laytext
.textHeight
-textHeight
) newY
= cast(int)laytext
.textHeight
-textHeight
;
251 if (newY
< 0) newY
= 0;
260 void goTo (uint widx
) {
261 if (laytext
is null) return;
262 auto lidx
= laytext
.findLineWithWord(widx
);
264 assert(lidx
< laytext
.lineCount
);
266 if (topY
!= laytext
.line(lidx
).y
) {
267 topY
= laytext
.line(lidx
).y
;
272 conwriteln("goto: widx=", widx
, "; lidx=", lidx
, "; fwn=", laytext
.line(lidx
).wstart
, "; topY=", topY
);
273 auto lnum
= laytext
.findLineAtY(topY
);
274 conwriteln(" newlnum=", lnum
, "; lidx.y=", laytext
.line(lidx
).y
, "; lnum.y=", laytext
.line(lnum
).y
);
280 void pushPosition () {
281 if (laytext
is null || laytext
.lineCount
== 0) return;
282 auto lidx
= laytext
.findLineAtY(topY
);
283 if (lidx
>= 0) posstack
~= laytext
.line(lidx
).wstart
;
286 void popPosition () {
287 if (posstack
.length
== 0) return;
288 auto widx
= posstack
[$-1];
289 posstack
.length
-= 1;
290 posstack
.assumeSafeAppend
;
295 void gotoSection (int sn
) {
296 if (laytext
is null || laytext
.lineCount
== 0 || bookmeta
is null) return;
297 if (sn
< 0 || sn
>= bookmeta
.sections
.length
) return;
298 auto lidx
= laytext
.findLineWithWord(bookmeta
.sections
[sn
].wordidx
);
300 auto newY
= laytext
.line(lidx
).y
;
310 void relayout (bool forced
=false) {
311 if (laytext
!is null) {
313 auto lidx
= laytext
.findLineAtY(topY
);
314 if (lidx
>= 0) widx
= laytext
.line(lidx
).wstart
;
315 int maxWidth
= GWidth
-4-2-BND_SCROLLBAR_WIDTH
-2;
316 //if (maxWidth < MinWinWidth) maxWidth = MinWinWidth;
319 auto stt
= MonoTime
.currTime
;
320 laytext
.relayout(maxWidth
, forced
);
321 auto ett
= MonoTime
.currTime
-stt
;
322 conwriteln("relayouted in ", ett
.total
!"msecs", " milliseconds; lines:", laytext
.lineCount
, "; words:", laytext
.nextWordIndex
);
330 void drawShipName () {
331 if (shipModel
is null || shipModel
.name
.length
== 0) return;
332 vg
.fontFaceId(uiFont
);
333 vg
.textAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
);
334 vg
.fontSize(fsizeUI
);
335 auto w
= vg
.bndLabelWidth(-1, shipModel
.name
)+8;
336 float h
= BND_WIDGET_HEIGHT
+8;
337 float mx
= (GWidth
-w
)/2.0;
338 float my
= (GHeight
-h
)-8;
339 vg
.bndMenuBackground(mx
, my
, w
, h
, BND_CORNER_NONE
);
340 vg
.bndMenuItem(mx
+4, my
+4, w
-8, BND_WIDGET_HEIGHT
, BND_DEFAULT
, -1, shipModel
.name
);
341 if (shipModel
.dispName
&& shipModel
.dispName
!= shipModel
.name
) {
342 my
-= BND_WIDGET_HEIGHT
+16;
343 w
= vg
.bndLabelWidth(-1, shipModel
.dispName
)+8;
345 vg
.bndMenuBackground(mx
, my
, w
, h
, BND_CORNER_NONE
);
346 vg
.bndMenuItem(mx
+4, my
+4, w
-8, BND_WIDGET_HEIGHT
, BND_DEFAULT
, -1, shipModel
.dispName
);
351 void createSectionMenu () {
353 //conwriteln("lc=", laytext.lineCount, "; bm: ", (bookmeta !is null));
354 if (laytext
is null || laytext
.lineCount
== 0 || bookmeta
is null) { freeShipModel(); return; }
355 currPopup
= new PopupMenu(vg
, "Sections"d
, () {
357 foreach (const ref sc
; bookmeta
.sections
) items
~= sc
.name
;
360 currPopup
.allowFiltering
= true;
361 //conwriteln(currPopup.items.length);
362 // find current section
363 currPopup
.itemIndex
= 0;
364 auto lidx
= laytext
.findLineAtY(topY
);
365 if (lidx
>= 0 && bookmeta
.sections
.length
> 0) {
366 foreach (immutable sidx
, const ref sc
; bookmeta
.sections
) {
367 auto sline
= laytext
.findLineWithWord(sc
.wordidx
);
368 if (sline
>= 0 && lidx
>= sline
) currPopup
.itemIndex
= cast(int)sidx
;
371 onPopupSelect
= (int item
) { gotoSection(item
); };
375 void createQuitMenu (bool wantYes
) {
377 currPopup
= new PopupMenu(vg
, "Quit?"d
, () {
378 return ["Yes"d
, "No"d
];
380 currPopup
.itemIndex
= (wantYes ?
0 : 1);
381 onPopupSelect
= (int item
) { if (item
== 0) concmd("quit"); };
385 void createRecentMenu () {
387 if (recentFiles
.length
== 0) recentFiles
= loadDetailedHistory();
388 if (recentFiles
.length
== 0) { freeShipModel(); return; }
389 currPopup
= new PopupMenu(vg
, "Recent files"d
, () {
390 import std
.conv
: to
;
392 foreach (const ref BookInfo bi
; recentFiles
) {
394 if (bi
.seqname
.length
) {
396 if (bi
.seqnum
) { s
~= to
!string(bi
.seqnum
); s
~= ": "; }
397 //conwriteln(bi.seqname);
401 if (bi
.author
.length
) { s
~= " \xe2\x80\x94 "; s
~= bi
.author
; }
406 currPopup
.allowFiltering
= true;
407 currPopup
.itemIndex
= cast(int)recentFiles
.length
-1;
408 onPopupSelect
= (int item
) {
409 newBookFileName
= recentFiles
[item
].diskfile
;
410 popupNoShipKill
= true;
412 onPopupKeyEvent
= (KeyEvent event
) {
413 if (event
== "Delete" && currPopup
.isCurValid
) {
414 auto idx
= currPopup
.itemIndex
;
415 if (idx
>= 0 && idx
< recentFiles
.length
) {
416 removeFileFromHistory(recentFiles
[idx
].diskfile
);
417 currPopup
.removeItem(idx
);
418 foreach (immutable c
; idx
+1..recentFiles
.length
) recentFiles
[c
-1] = recentFiles
[c
];
419 recentFiles
.length
-= 1;
420 recentFiles
.assumeSafeAppend
;
423 return false; // don't close menu
428 bool menuKey (KeyEvent event
) {
429 if (formatWorks
!= 0) return false;
430 if (!inMenu
) return false;
431 if (inGalaxyMap
) return false;
432 if (!event
.pressed
) return false;
433 if (currPopup
is null) return false;
434 auto res
= currPopup
.onKey(event
);
435 if (res
== PopupMenu
.Close
) {
439 } else if (res
>= 0) {
440 if (onPopupSelect
!is null) onPopupSelect(res
);
442 if (popupNoShipKill
) popupNoShipKill
= false; else freeShipModel();
444 } else if (res
== PopupMenu
.NotMine
) {
445 if (onPopupKeyEvent
!is null && onPopupKeyEvent(event
)) {
455 bool menuChar (dchar dch
) {
456 if (formatWorks
!= 0) return false;
457 if (!inMenu
) return false;
458 if (inGalaxyMap
) return false;
459 if (currPopup
is null) return false;
460 auto res
= currPopup
.onChar(dch
);
461 if (res
== PopupMenu
.Close
) {
465 } else if (res
>= 0) {
466 if (onPopupSelect
!is null) onPopupSelect(res
);
468 if (popupNoShipKill
) popupNoShipKill
= false; else freeShipModel();
475 bool menuMouse (MouseEvent event
) {
476 if (formatWorks
!= 0) return false;
477 if (!inMenu
) return false;
478 if (inGalaxyMap
) return false;
479 if (currPopup
is null) return false;
480 auto res
= currPopup
.onMouse(event
);
481 if (res
== PopupMenu
.Close
) {
485 } else if (res
>= 0) {
486 if (onPopupSelect
!is null) onPopupSelect(res
);
488 if (popupNoShipKill
) popupNoShipKill
= false; else freeShipModel();
495 bool readerKey (KeyEvent event
) {
496 if (formatWorks
!= 0) return false;
497 if (!event
.pressed
) {
499 case Key
.Up
: arrowDir
= 0; return true;
500 case Key
.Down
: arrowDir
= 0; return true;
507 if (event
.modifierState
&ModifierState
.shift
) {
508 //goto case Key.PageUp;
509 hardScrollBy(toMove
); toMove
= 0;
510 scrollBy(-textHeight
/3*2);
512 //goto case Key.PageDown;
513 hardScrollBy(toMove
); toMove
= 0;
514 scrollBy(textHeight
/3*2);
521 hardScrollBy(toMove
); toMove
= 0;
522 hardScrollBy(-(textHeight
> 32 ? textHeight
-32 : textHeight
));
525 hardScrollBy(toMove
); toMove
= 0;
526 hardScrollBy(textHeight
> 32 ? textHeight
-32 : textHeight
);
537 if (laytext
!is null) {
538 if (event
== "C-H") goEnd(); else goHome();
547 bool controlKey (KeyEvent event
) {
548 if (!event
.pressed
) return false;
551 if (inGalaxyMap
) { inGalaxyMap
= false; refresh(); return true; }
552 if (inMenu
) { closeMenu(); freeShipModel(); refresh(); return true; }
553 if (showShip
) { showShip
= false; freeShipModel(); refresh(); return true; }
555 createQuitMenu(true);
558 case Key
.P
: if (event
.modifierState
== ModifierState
.ctrl
) { concmd("r_fps toggle"); return true; } break;
559 case Key
.I
: if (event
.modifierState
== ModifierState
.ctrl
) { concmd("r_interference toggle"); return true; } break;
560 case Key
.N
: if (event
.modifierState
== ModifierState
.ctrl
) { concmd("r_sbleft toggle"); return true; } break;
561 case Key
.C
: if (event
.modifierState
== ModifierState
.ctrl
) { if (addIf()) refresh(); return true; } break;
563 if (event
.modifierState
== ModifierState
.ctrl
&& eliteShipFiles
.length
> 0) {
565 showShip
= !showShip
;
566 if (showShip
) ensureShipModel(); else freeShipModel();
572 case Key
.Q
: if (event
.modifierState
== ModifierState
.ctrl
) { concmd("quit"); return true; } break;
573 case Key
.B
: if (formatWorks
== 0 && !inMenu
&& !inGalaxyMap
&& event
.modifierState
== ModifierState
.ctrl
) { pushPosition(); return true; } break;
575 if (formatWorks
== 0 && !showShip
&& !inMenu
&& !inGalaxyMap
) {
582 if (formatWorks
== 0 && event
.modifierState
== ModifierState
.alt
) {
589 if (!inMenu
&& !showShip
) {
590 inGalaxyMap
= !inGalaxyMap
;
595 if (formatWorks
== 0 && event
.modifierState
== ModifierState
.ctrl
) relayout(true);
597 case Key
.Home
: if (showShip
) { setShip(0); return true; } break;
598 case Key
.End
: if (showShip
) { setShip(cast(int)eliteShipFiles
.length
); return true; } break;
599 case Key
.Up
: case Key
.Left
: if (showShip
) { setShip(shipModelIndex
-1); return true; } break;
600 case Key
.Down
: case Key
.Right
: if (showShip
) { setShip(shipModelIndex
+1); return true; } break;
606 int startX () { pragma(inline
, true); return (sbLeft ?
2+BND_SCROLLBAR_WIDTH
+2 : 4); }
607 int endX () { pragma(inline
, true); return startX
+GWidth
-4-2-BND_SCROLLBAR_WIDTH
-2-1; }
609 int startY () { pragma(inline
, true); return (GHeight
-textHeight
)/2; }
610 int endY () { pragma(inline
, true); return startY
+textHeight
-1; }
613 // ////////////////////////////////////////////////////////////////////////// //
615 if (GWidth
< MinWinWidth
) GWidth
= MinWinWidth
;
616 if (GHeight
< MinWinHeight
) GHeight
= MinWinHeight
;
618 bookText
= loadBook(bookFileName
);
620 //setOpenGLContextVersion(3, 2); // up to GLSL 150
621 setOpenGLContextVersion(2, 0); // it's enough
622 //openGLContextCompatible = false;
624 sdwindow
= new SimpleWindow(GWidth
, GHeight
, bookText
.title
~" \xe2\x80\x94 "~bookText
.authorFirst
~" "~bookText
.authorLast
, OpenGlOptions
.yes
, Resizability
.allowResizing
);
625 sdwindow
.hideCursor(); // we will do our own
626 sdwindow
.setMinSize(MinWinWidth
, MinWinHeight
);
628 version(X11
) sdwindow
.closeQuery
= delegate () { concmd("quit"); };
630 auto stt
= MonoTime
.currTime
;
631 auto prevt
= MonoTime
.currTime
;
633 textHeight
= GHeight
-8;
635 MonoTime nextIfTime
= MonoTime
.currTime
;
637 lastMMove
= MonoTime
.currTime
;
639 auto childTid
= spawn(&reformatThreadFn
, thisTid
);
640 childTid
.setMaxMailboxSize(128, OnCrowding
.block
);
641 thisTid
.setMaxMailboxSize(128, OnCrowding
.block
);
643 void loadAndFormat (string filename
) {
644 assert(formatWorks
<= 0);
648 //formatWorks = -1; //FIXME
655 //sdwindow.redrawOpenGlSceneNow();
656 //bookText = loadBook(newBookFileName);
657 //newBookFileName = null;
659 //if (formatWorks < 0) formatWorks = 1; else ++formatWorks;
661 //conwriteln("*** loading new book: '", filename, "'");
662 childTid
.send(ReformatWork(null, filename
, GWidth
, GHeight
));
667 if (formatWorks
< 0) formatWorks
= 1; else ++formatWorks
;
668 childTid
.send(ReformatWork(cast(shared)bookText
, null, GWidth
, GHeight
));
672 void formatComplete (ref ReformatWorkComplete w
) {
673 scope(exit
) { import core
.memory
: GC
; GC
.collect(); GC
.minimize(); }
675 auto lt
= cast(LayTextC
)w
.laytext
;
676 scope(exit
) if (lt
) lt
.freeMemory();
679 BookMetadata meta
= cast(BookMetadata
)w
.meta
;
682 BookText
bt = cast(BookText
)w
.booktext
;
684 if (bt !is bookText
) {
688 sdwindow
.title
= bookText
.title
~" \xe2\x80\x94 "~bookText
.authorFirst
~" "~bookText
.authorLast
;
689 } else if (bookmeta
is null) {
693 if (isQuitRequested || formatWorks
<= 0) return;
697 if (formatWorks
== 0) reformat();
701 if (formatWorks
!= 0) return;
705 if (!firstFormat
&& laytext
!is null && laytext
.lineCount
) {
706 auto lidx
= laytext
.findLineAtY(topY
);
707 if (lidx
>= 0) widx
= laytext
.line(lidx
).wstart
;
709 if (laytext
!is null) laytext
.freeMemory();
723 void closeWindow () {
724 doSaveState(true); // forced state save
725 if (!sdwindow
.closed
&& vg
!is null) {
735 sdwindow
.visibleForTheFirstTime
= delegate () {
736 sdwindow
.setAsCurrentOpenGlContext(); // make this window active
737 sdwindow
.vsync
= false;
738 //sdwindow.useGLFinish = false;
739 //glbindLoadFunctions();
742 NVGContextFlag
[4] flagList
;
744 if (flagNanoAA
) flagList
[flagCount
++] = NVGContextFlag
.Antialias
;
745 if (flagNanoSS
) flagList
[flagCount
++] = NVGContextFlag
.StencilStrokes
;
746 if (flagNanoFAA
) flagList
[flagCount
++] = NVGContextFlag
.FontAA
; else flagList
[flagCount
++] = NVGContextFlag
.FontNoAA
;
747 vg
= nvgCreateContext(flagList
[0..flagCount
]);
749 conwriteln("Could not init nanovg.");
754 curImg
= createCursorImage(vg
);
755 curImgWhite
= createCursorImage(vg
, true);
756 fps
= new PerfGraph("Frame Time", PerfGraph
.Style
.FPS
, "ui");
757 } catch (Exception e
) {
758 conwriteln("ERROR: ", e
.msg
);
766 sdwindow
.redrawOpenGlScene();
770 sdwindow
.windowResized
= delegate (int w
, int h
) {
771 //conwriteln("w=", w, "; h=", h);
772 //if (w < MinWinWidth) w = MinWinWidth;
773 //if (h < MinWinHeight) h = MinWinHeight;
774 glViewport(0, 0, w
, h
);
777 textHeight
= GHeight
-8;
783 sdwindow
.redrawOpenGlScene
= delegate () {
784 if (isQuitRequested
) return;
786 glconResize(GWidth
/oglConScale
, GHeight
/oglConScale
, oglConScale
);
789 conwriteln("cnt=", cnt++);
792 //glClearColor(0, 0, 0, 0);
793 //glClearColor(0.18, 0.18, 0.18, 0);
794 glClearColor(colorBack
.r
, colorBack
.g
, colorBack
.b
, 0);
795 glClear(glNVGClearFlags|EliteModel
.glClearFlags
);
798 needRedrawFlag
= (fps
!is null && fpsVisible
);
799 if (vg
is null) return;
801 scope(exit
) vg
.releaseImages();
802 vg
.beginFrame(GWidth
, GHeight
, 1);
806 float curHeight
= (laytext
!is null ? topY
: 0);
807 float th
= (laytext
!is null ? laytext
.textHeight
-textHeight
: 0);
808 if (th
<= 0) { curHeight
= 0; th
= 1; }
809 float sz
= cast(float)(GHeight
-4)/th
;
810 if (sz
> 1) sz
= 1; else if (sz
< 0.05) sz
= 0.05;
811 int sx
= (sbLeft ?
2 : GWidth
-BND_SCROLLBAR_WIDTH
-2);
812 vg
.bndScrollSlider(sx
, 2, BND_SCROLLBAR_WIDTH
, GHeight
-4, BND_DEFAULT
, curHeight
/th
, sz
);
814 if (laytext
is null) {
815 if (shipModel
is null) {
817 vg
.fillColor(colorText
);
818 int drawY
= (GHeight
-textHeight
)/2;
819 vg
.intersectScissor((sbLeft ?
2+BND_SCROLLBAR_WIDTH
+2 : 4), drawY
, GWidth
-4-2-BND_SCROLLBAR_WIDTH
-2, textHeight
);
820 vg
.fontFaceId(textFont
);
821 vg
.fontSize(fsizeText
);
822 vg
.textAlign(NVGTextAlign
.H
.Center
, NVGTextAlign
.V
.Middle
);
823 vg
.text(GWidth
/2, GHeight
/2, "REFORMATTING");
829 if (laytext
!is null && laytext
.lineCount
) {
831 vg
.fillColor(colorText
);
833 vg
.intersectScissor((sbLeft ?
2+BND_SCROLLBAR_WIDTH
+2 : 4), drawY
, GWidth
-4-2-BND_SCROLLBAR_WIDTH
-2, textHeight
);
834 //FIXME: not GHeight!
835 int lidx
= laytext
.findLineAtY(topY
);
836 if (lidx
>= 0 && lidx
< laytext
.lineCount
) {
837 drawY
-= topY
-laytext
.line(lidx
).y
;
838 vg
.textAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
);
839 LayFontStyle lastStyle
;
841 bool setColor
= true;
842 while (lidx
< laytext
.lineCount
&& drawY
< GHeight
) {
843 auto ln
= laytext
.line(lidx
);
844 foreach (ref LayWord w
; laytext
.lineWords(lidx
)) {
845 if (lastStyle
!= w
.style || setColor
) {
846 if (w
.style
.fontface
!= lastStyle
.fontface
) vg
.fontFace(laytext
.fontFace(w
.style
.fontface
));
847 vg
.fontSize(w
.style
.fontsize
);
848 auto c
= NVGColor(w
.style
.color
);
855 if (newYFade
&& newYLine
== lidx
) vg
.fillColor(nvgLerpRGBA(colorText
, colorTextHi
, newYAlpha
));
856 auto oid
= w
.objectIdx
;
859 laytext
.objectAtIndex(oid
).draw(vg
, startx
+w
.x
, drawY
+ln
.h
+ln
.desc
);
862 vg
.text(startx
+w
.x
, drawY
+ln
.h
+ln
.desc
, laytext
.wordText(w
));
864 //TODO: draw lines over whitespace
865 if (lastStyle
.underline
) vg
.rect(startx
+w
.x
, drawY
+ln
.h
+ln
.desc
+1, w
.w
, 1);
866 if (lastStyle
.strike
) vg
.rect(startx
+w
.x
, drawY
+ln
.h
+ln
.desc
-w
.asc
/3, w
.w
, 2);
867 if (lastStyle
.overline
) vg
.rect(startx
+w
.x
, drawY
+ln
.h
+ln
.desc
-w
.asc
-1, w
.w
, 1);
868 version(debug_draw
) {
872 vg
.strokeColor(nvgRGB(0, 0, 255));
873 vg
.rect(startx
+w
.x
, drawY
, w
.w
, w
.h
);
877 if (newYFade
&& newYLine
== lidx
) vg
.fillColor(NVGColor(lastStyle
.color
));
879 if (newYFade
&& newYLine
== lidx
) markerY
= drawY
;
886 // draw scroll marker
887 if (markerY
!= -666) {
889 vg
.fillColor(NVGColor(0.3f, 0.3f, 0.3f, newYAlpha
));
890 vg
.rect(startX
, markerY
, GWidth
, 2);
895 if (colorDim
.a
!= 1) {
896 //vg.scissor(0, 0, GWidth, GHeight);
899 vg
.fillColor(colorDim
);
900 vg
.rect(0, 0, GWidth
, GHeight
);
904 // dim more if menu is active
905 if (inMenu || showShip || formatWorks
!= 0) {
906 //vg.scissor(0, 0, GWidth, GHeight);
909 //vg.globalAlpha(0.5);
910 vg
.fillColor(nvgRGBA(0, 0, 0, (showShip || formatWorks
!= 0 ?
127 : 64)));
911 vg
.rect(0, 0, GWidth
, GHeight
);
914 if (shipModel
!is null) {
916 float zz
= shipModel
.bbox
[1].z
-shipModel
.bbox
[0].z
;
930 drawModel(shipAngle
, shipModel
);
931 vg
.beginFrame(GWidth
, GHeight
, 1);
935 if (formatWorks
== 0) {
937 //vg.beginFrame(GWidth, GHeight, 1);
938 //vg.scissor(0, 0, GWidth, GHeight);
944 if (fps
!is null && fpsVisible
) {
945 //vg.beginFrame(GWidth, GHeight, 1);
946 //vg.scissor(0, 0, GWidth, GHeight);
948 fps
.render(vg
, GWidth
-fps
.width
-4, GHeight
-fps
.height
-4);
951 if (inGalaxyMap
) drawGalaxy(vg
);
953 if (curImg
.valid
&& !mouseHidden
) {
955 //vg.beginFrame(GWidth, GHeight, 1);
957 //vg.scissor(0, 0, GWidth, GHeight);
959 vg
.imageSize(curImg
, w
, h
);
960 if (mouseFadingAway
) {
962 if (mouseAlpha
<= 0) { mouseFadingAway
= false; mouseHidden
= true; }
966 vg
.globalAlpha(mouseAlpha
);
968 vg
.fillPaint(vg
.imagePattern(mouseX
, mouseY
, w
, h
, 0, curImg
, 1));
970 vg
.fillPaint(vg
.imagePattern(mouseX
, mouseY
, w
, h
, 0, curImgWhite
, 1));
972 vg
.rect(mouseX
, mouseY
, w
, h
);
977 vg.fillPaint(vg.imagePattern(mouseX, mouseY, w, h, 0, curImgWhite, 0.4));
978 vg.rect(mouseX, mouseY, w, h);
988 void processThreads () {
989 ReformatWorkComplete wd
;
991 bool workDone
= false;
992 auto res
= receiveTimeout(Duration
.zero
,
996 (ReformatWorkComplete w
) {
1001 if (!res
) { assert(!workDone
); break; }
1002 if (workDone
) { workDone
= false; formatComplete(wd
); }
1006 auto lastTimerEventTime
= MonoTime
.currTime
;
1007 bool somethingVisible
= true;
1009 sdwindow
.visibilityChanged
= delegate (bool vis
) {
1010 //import core.stdc.stdio; printf("VISCHANGED: %s\n", (vis ? "tan" : "ona").ptr);
1011 somethingVisible
= vis
;
1014 conRegVar
!fpsVisible("r_fps", "show fps indicator", (self
, valstr
) { refresh(); });
1015 conRegVar
!inGalaxyMap("r_galaxymap", "show Elite galaxy map", (self
, valstr
) { refresh(); });
1017 conRegVar
!interAllowed("r_interference", "show interference", (self
, valstr
) { refresh(); });
1018 conRegVar
!sbLeft("r_sbleft", "show scrollbar at the left side", (self
, valstr
) { refresh(); });
1020 conRegVar
!showShip("r_showship", "show Elite ship", (self
, valstr
) {
1021 if (eliteShipFiles
.length
== 0) return false;
1025 if (showShip
) ensureShipModel(); else freeShipModel();
1031 if (currPopup
!is null) {
1037 onPopupSelect
= null;
1038 })("menu_close", "close current popup menu");
1041 if (formatWorks
== 0 && !showShip
&& !inGalaxyMap
) {
1045 createSectionMenu();
1048 })("menu_section", "show section menu");
1051 if (formatWorks
== 0 && !showShip
&& !inGalaxyMap
) {
1058 })("menu_recent", "show recent menu");
1060 sdwindow
.eventLoop(1000/34,
1063 if (sdwindow
.closed
) return;
1065 if (isQuitRequested
) { closeWindow(); return; }
1066 auto ctt
= MonoTime
.currTime
;
1069 auto spass
= (ctt
-lastTimerEventTime
).total
!"msecs";
1070 //if (spass >= 30) { import core.stdc.stdio; printf("WARNING: too long frame time: %u\n", cast(uint)spass); }
1071 //{ import core.stdc.stdio; printf("FRAME TIME: %u\n", cast(uint)spass); }
1072 lastTimerEventTime
= ctt
;
1075 //curt = MonoTime.currTime;
1077 //auto secs = cast(double)((curt-stt).total!"msecs")/1000.0;
1078 auto dt = cast(double)((curt
-prevt
).total
!"msecs")/1000.0;
1079 if (fps
!is null) fps
.update(dt);
1083 if (formatWorks
== 0) {
1086 import std
.math
: abs
;
1087 immutable int sign
= (toMove
< 0 ?
-1 : 1);
1089 if (arrowSpeed
== 0) arrowSpeed
= 16;
1090 if (abs(toMove
) <= arrowSpeed
) {
1092 if (arrowSpeed
< 4) arrowSpeed
= 4;
1095 if (arrowSpeed
> Delta
) arrowSpeed
= Delta
;
1097 // calc move distance
1098 int sc
= arrowSpeed
;
1099 if (sc
> abs(toMove
)) sc
= abs(toMove
);
1100 hardScrollBy(sc
*sign
);
1102 if (toMove
== 0) arrowSpeed
= 0;
1103 nextFadeTime
= ctt
+500.msecs
;
1105 } else if (arrowDir
) {
1106 if ((arrowDir
< 0 && arrowSpeed
> 0) ||
(arrowDir
> 0 && arrowSpeed
< 0)) arrowSpeed
+= arrowDir
*4;
1107 arrowSpeed
+= arrowDir
*2;
1108 if (arrowSpeed
< -64) arrowSpeed
= -64; else if (arrowSpeed
> 64) arrowSpeed
= 64;
1109 hardScrollBy(arrowSpeed
);
1111 } else if (arrowSpeed
!= 0) {
1112 if (arrowSpeed
< 0) {
1113 if ((arrowSpeed
+= 4) > 0) arrowSpeed
= 0;
1115 if ((arrowSpeed
-= 4) < 0) arrowSpeed
= 0;
1118 hardScrollBy(arrowSpeed
);
1124 if (ctt
>= nextFadeTime
) {
1125 if ((newYAlpha
-= 0.1) <= 0) {
1128 nextFadeTime
= ctt
+25.msecs
;
1134 // interference processing
1135 if (ctt
>= nextIfTime
) {
1136 import std
.random
: uniform
;
1137 if (uniform
!"[]"(0, 100) >= 50) { if (addIf()) refresh(); }
1138 nextIfTime
+= (uniform
!"[]"(50, 1500)).msecs
;
1140 if (processIfs()) refresh();
1142 if (shipModel
!is null) {
1144 if (shipAngle
< 359) shipAngle
+= 360;
1148 if (!mouseFadingAway
) {
1149 if (!mouseHidden
&& !mouseHigh
) {
1150 if ((ctt
-lastMMove
).total
!"msecs" > 2500) {
1151 //mouseHidden = true;
1152 mouseFadingAway
= true;
1158 if (somethingVisible
) {
1159 // sadly, to keep framerate we have to redraw each frame, or driver will throw us out of the ship
1160 if (/*needRedraw*/true) sdwindow
.redrawOpenGlSceneNow();
1166 if (newBookFileName
.length
&& formatWorks
== 0 && vg
!is null) {
1167 doSaveState(true); // forced state save
1170 loadAndFormat(newBookFileName
);
1171 newBookFileName
= null;
1172 sdwindow
.redrawOpenGlSceneNow();
1176 delegate (KeyEvent event
) {
1177 if (sdwindow
.closed
) return;
1178 if (glconKeyEvent(event
)) return;
1179 if (event
.key
== Key
.PadEnter
) event
.key
= Key
.Enter
;
1180 if ((event
.modifierState
&ModifierState
.numLock
) == 0) {
1181 switch (event
.key
) {
1182 case Key
.Pad0
: event
.key
= Key
.Insert
; break;
1183 case Key
.PadDot
: event
.key
= Key
.Delete
; break;
1184 case Key
.Pad1
: event
.key
= Key
.End
; break;
1185 case Key
.Pad2
: event
.key
= Key
.Down
; break;
1186 case Key
.Pad3
: event
.key
= Key
.PageDown
; break;
1187 case Key
.Pad4
: event
.key
= Key
.Left
; break;
1188 case Key
.Pad6
: event
.key
= Key
.Right
; break;
1189 case Key
.Pad7
: event
.key
= Key
.Home
; break;
1190 case Key
.Pad8
: event
.key
= Key
.Up
; break;
1191 case Key
.Pad9
: event
.key
= Key
.PageUp
; break;
1192 //case Key.PadEnter: event.key = Key.Enter; break;
1196 if (controlKey(event
)) return;
1197 if (menuKey(event
)) return;
1198 if (readerKey(event
)) return;
1200 delegate (MouseEvent event
) {
1201 if (sdwindow
.closed
) return;
1203 int linkAt (int msx
, int msy
) {
1204 if (laytext
!is null && bookmeta
!is null) {
1205 if (msx
>= startX
&& msx
<= endX
&& msy
>= startY
&& msy
<= endY
) {
1206 auto widx
= laytext
.wordAtXY(msx
-startX
, topY
+msy
-startY
);
1208 //conwriteln("word at (", msx-startX, ",", msy-startY, "): ", widx);
1209 auto w
= laytext
.wordByIndex(widx
);
1211 //conwriteln("word #", widx, "; href=", w.style.href);
1212 if (!w
.style
.href
) break;
1213 if (auto hr
= w
.wordNum
in bookmeta
.hrefs
) {
1214 dstring href
= hr
.name
;
1215 if (href
.length
> 1 && href
[0] == '#') {
1217 foreach (const ref id
; bookmeta
.ids
) {
1218 if (id
.name
== href
) {
1224 //conwriteln("id '", hr.name, "' not found!");
1237 lastMMove
= MonoTime
.currTime
;
1238 if (mouseHidden || mouseFadingAway
) {
1239 mouseHidden
= false;
1240 mouseFadingAway
= false;
1244 if (mouseX
!= event
.x || mouseY
!= event
.y
) {
1249 if (!menuMouse(event
) && !showShip
) {
1250 if (event
.type
== MouseEventType
.buttonPressed
) {
1251 switch (event
.button
) {
1252 case MouseButton
.wheelUp
: hardScrollBy(-42); break;
1253 case MouseButton
.wheelDown
: hardScrollBy(42); break;
1254 case MouseButton
.left
:
1255 auto wid
= linkAt(event
.x
, event
.y
);
1261 case MouseButton
.right
:
1268 mouseHigh
= (linkAt(event
.x
, event
.y
) >= 0);
1270 delegate (dchar ch
) {
1271 if (sdwindow
.closed
) return;
1272 if (glconCharEvent(ch
)) return;
1273 if (menuChar(ch
)) return;
1274 //if (ch == '`') { concmd("r_console tan"); return; }
1279 childTid
.send(QuitWork());
1280 while (formatWorks
>= 0) processThreads();
1284 // ////////////////////////////////////////////////////////////////////////// //
1285 struct FlibustaUrl
{
1286 string fullUrl
; // onion
1290 this (const(char)[] aurl
) {
1291 import std
.format
: format
;
1292 aurl
= aurl
.xstrip();
1293 auto flibustaRE
= regex(`^(?:https?://)?(?:www\.)?flibusta\.[^/]+/b/(\d+)`);
1294 auto ct
= aurl
.matchFirst(flibustaRE
);
1296 fullUrl
= "http://flibustahezeous3.onion/b/%s/fb2".format(ct
[1]);
1298 host
= "flibustahezeous3.onion";
1301 auto protoRE
= regex(`^([^:/]+):`);
1302 auto protoMt
= aurl
.matchFirst(protoRE
);
1303 if (protoMt
.empty
) fullUrl
= "http:%s%s".format((aurl
[0] == '/' ?
"" : "//"), aurl
);
1305 auto hostRE
= regex(`^(?:[^:/]+)://([^/]+)`);
1306 auto hostMt
= fullUrl
.matchFirst(hostRE
);
1307 if (hostMt
.empty
) { fullUrl
= null; return; }
1308 host
= hostMt
[1].idup
;
1312 @property bool valid () const pure nothrow @safe @nogc { pragma(inline
, true); return (fullUrl
.length
> 0); }
1313 @property bool isFlibusta () const pure nothrow @safe @nogc { pragma(inline
, true); return (id
.length
> 0); }
1314 @property bool isOnion () const pure nothrow @safe @nogc { pragma(inline
, true); return host
.endsWithCI(".onion"); }
1318 // ////////////////////////////////////////////////////////////////////////// //
1319 __gshared SDBM dbCache
= null;
1322 void dbOpenCache () {
1323 if (dbCache
is null) {
1324 import std
.file
: exists
, mkdirRecurse
;
1326 string xfn
= buildPath(RcDir
, "cache");
1328 xfn
= buildPath(xfn
, ".cache.db");
1329 dbCache
= new SDBM(xfn
, SDBM
.WRITER|SDBM
.CREAT|SDBM
.NOLCK
);
1334 char[] dbBuildNamePathById (char[] dest
, const(char)[] id
) {
1335 import core
.stdc
.stdio
: snprintf
;
1336 assert(id
.length
> 0);
1337 assert(dest
.length
> 0);
1338 auto len
= snprintf(dest
.ptr
, dest
.length
, "/files/%.*s/name", cast(uint)id
.length
, id
.ptr
);
1339 if (len
< 1 || len
>= dest
.length
) assert(0, "out of destination buffer");
1340 return dest
[0..len
];
1344 string
dbFindInCache() (in auto ref FlibustaUrl furl
) {
1345 if (!furl
.valid
) return null;
1347 char[128] keybuf
= void;
1348 auto dbpath
= dbBuildNamePathById(keybuf
[], furl
.id
);
1349 string fname
= dbCache
.get
!string(dbpath
);
1350 if (fname
.length
== 0) return null;
1351 import std
.file
: exists
;
1353 if (fname
.exists
) return fname
;
1354 } catch (Exception e
) {}
1355 // no such file, remove it from database
1356 dbCache
.del(dbpath
);
1361 void dbPutToCache() (in auto ref FlibustaUrl furl
, const(char)[] fname
) {
1362 if (!furl
.valid || fname
.length
== 0) return;
1364 char[128] keybuf
= void;
1365 dbCache
.put(dbBuildNamePathById(keybuf
[], furl
.id
), fname
);
1369 void dbCloseCache () {
1370 if (dbCache
is null) return;
1373 import core
.memory
: GC
;
1380 void dbCleanupCache () {
1383 auto xre
= regex(`^/files/(.+)/name$`);
1386 auto key
= dbCache
.itNext();
1387 if (key
is null) break;
1388 //writeln("[", key, "]");
1389 auto mt
= key
.matchFirst(xre
);
1390 if (mt
.empty
) continue;
1391 auto fname
= dbCache
.get
!string(key
);
1392 if (fname
.length
!= 0) {
1394 import std
.file
: exists
;
1395 if (fname
.exists
) continue nextkey
;
1396 } catch (Exception e
) {}
1398 dead
[key
.idup
] = true; // oops
1400 foreach (string k
; dead
.byKey
) {
1401 //writeln("deleting stale cache record for '", k, "'");
1408 // ////////////////////////////////////////////////////////////////////////// //
1409 // returns file name
1410 string
fileDown() (in auto ref FlibustaUrl furl
) {
1411 if (!furl
.valid ||
!furl
.isFlibusta
) return null;
1413 string cachedFName
= dbFindInCache(furl
);
1414 if (cachedFName
.length
) return cachedFName
;
1416 // content-disposition: attachment; filename="Divov_Sled-zombi.1lzb6Q.96382.fb2.zip"
1417 auto cdRE0
= regex(`^\s*attachment\s*;\s*filename="(.+?)"`, "i");
1418 auto cdRE1
= regex(`^\s*attachment\s*;\s*filename=([^;]+?)`, "i");
1420 auto http
= HTTP(furl
.host
);
1421 http
.method
= HTTP
.Method
.get
;
1422 http
.url
= furl
.fullUrl
;
1424 string fname
= null;
1425 string tmpfname
= null;
1427 bool alreadyDowned
= false;
1430 http
.onReceiveHeader
= delegate (in char[] key
, in char[] value
) {
1431 //writeln(key ~ ": " ~ value);
1432 if (key
.strEquCI("content-disposition")) {
1433 auto ct
= value
.matchFirst(cdRE0
);
1434 if (ct
.empty
) ct
= value
.matchFirst(cdRE1
);
1436 auto fnp
= ct
[1].xstrip
;
1437 auto lslpos
= fnp
.lastIndexOf('/');
1438 if (lslpos
> 0) fnp
= fnp
[lslpos
+1..$];
1439 if (fnp
.length
== 0) {
1442 import std
.file
: exists
, mkdirRecurse
;
1444 string xfn
= buildPath(RcDir
, "cache");
1447 xxname
.reserve(fnp
.length
);
1448 foreach (char ch
; fnp
) {
1449 if (ch
<= ' ' || ch
== 127) ch
= '_';
1452 fnps
= cast(string
)xxname
; // it is safe to cast here
1453 fname
= buildPath(xfn
, fnps
);
1454 tmpfname
= fname
~".down.part";
1457 alreadyDowned = true;
1458 throw new Exception("already here");
1459 //throw new FileAlreadyDowned("already here");
1462 //write("\r", fnp, " [", furl.fullUrl, "]\e[K");
1468 http
.onReceive
= delegate (ubyte[] data
) {
1470 if (fname
.length
== 0) throw new Exception("no file name found in headers");
1471 //writeln(" downloading to ", fname);
1472 fo
= VFile(tmpfname
, "w");
1474 fo
.rawWriteExact(data
);
1478 MonoTime lastProgTime
= MonoTime
.zero
;
1479 enum BarLength
= 68;
1480 bool doProgUpdate
= true;
1481 char[1024] buf
= void;
1482 int oldDots
= -1, oldPrc
= -1;
1485 immutable string stickStr
= `|/-\`;
1487 // will set `doProgUpdate`, and update `oldXXX`
1488 void buildPBar (usize dlTotal
, usize dlNow
) {
1489 void put (const(char)[] s
...) nothrow {
1490 if (s
.length
== 0) return;
1491 if (bufpos
>= buf
.length
) return;
1492 int left
= cast(int)buf
.length
-bufpos
;
1493 if (s
.length
> left
) s
= s
[0..left
];
1494 assert(s
.length
> 0);
1495 import core
.stdc
.string
: memcpy
;
1496 memcpy(buf
.ptr
+bufpos
, s
.ptr
, s
.length
);
1497 bufpos
+= cast(int)s
.length
;
1499 void putprc (int prc
) {
1500 if (prc
< 0) prc
= 0; else if (prc
> 100) prc
= 100;
1501 if (bufpos
>= buf
.length || buf
.length
-bufpos
< 5) return; // oops
1502 import core
.stdc
.stdio
;
1503 bufpos
+= cast(int)snprintf(buf
.ptr
+bufpos
, 5, "%3d%%", prc
);
1505 void putCommaNum (usize n
, usize max
=0) {
1506 char[128] buf
= void;
1508 put(intWithCommas(buf
[], n
));
1510 auto len
= intWithCommas(buf
[], max
).length
;
1511 auto pt
= intWithCommas(buf
[], n
);
1512 while (len
-- > pt
.length
) put(" ");
1520 auto barpos
= bufpos
;
1521 foreach (immutable _
; 0..BarLength
) put(" ");
1524 int prc
= cast(int)(cast(ulong)100*dlNow
/dlTotal
);
1525 if (prc
< 0) prc
= 0; else if (prc
> 100) prc
= 100;
1526 int dots
= cast(int)(cast(ulong)BarLength
*dlNow
/dlTotal
);
1527 if (dots
< 0) dots
= 0; else if (dots
> BarLength
) dots
= BarLength
;
1528 if (prc
!= oldPrc || dots
!= oldDots
) {
1529 doProgUpdate
= true;
1534 putCommaNum(dlNow
, dlTotal
);
1536 putCommaNum(dlTotal
);
1540 foreach (immutable dp
; 0..dots
) if (barpos
+dp
< buf
.length
) buf
[barpos
+dp
] = '.';
1543 if (oldDots
!= -1 || oldPrc
!= -1) doProgUpdate
= true;
1549 http
.onProgress
= delegate (usize dltotal
, usize dlnow
, usize ultotal
, usize ulnow
) {
1550 //writeln("Progress ", dltotal, ", ", dlnow, ", ", ultotal, ", ", ulnow);
1551 if (fname
.length
== 0) {
1552 auto ct
= MonoTime
.currTime
;
1553 if ((ct
-lastProgTime
).total
!"msecs" >= 100) {
1554 write("\x08", stickStr
[stickPos
]);
1555 stickPos
= (stickPos
+1)%cast(int)stickStr
.length
;
1560 buildPBar(dltotal
, dlnow
);
1561 if (doProgUpdate
) { write("\e[?7l", buf
[0..bufpos
], "\e[K\e[?7h"); doProgUpdate
= false; }
1562 //if (dltotal == 0) return 0;
1563 //auto ct = MonoTime.currTime;
1564 //if ((ct-lastProgTime).total!"msecs" < 1000) return 0;
1565 //lastProgTime = ct;
1566 //writef("\r%s [%s] -- [%s/%s] %3u%%\e[K", fnps, host, intWithCommas(dlnow), intWithCommas(dltotal), 100UL*dlnow/dltotal);
1571 http
.proxyType
= HTTP
.CurlProxy
.socks5_hostname
;
1572 http
.proxy
= "127.0.0.1";
1573 http
.proxyPort
= 9050;
1577 //write("downloading from [", host, "]: ", realUrl, " ... ");
1578 write("downloading from Flibusta: ", furl
.fullUrl
, " ... ", stickStr
[0]);
1581 buildPBar(cast(uint)fo
.size
, cast(uint)fo
.size
);
1585 writeln(buf
[0..bufpos
], "\e[K");
1587 } catch (Exception e
) {
1588 if (/*cast(FileAlreadyDowned)e*/alreadyDowned
) {
1589 write("\r", fname
, " already downloaded.\e[K");
1590 return fname
; // already here
1592 if (tmpfname
.length
) {
1593 import std
.exception
: collectException
;
1594 import std
.file
: remove
;
1595 collectException(tmpfname
.remove
);
1601 // something was downloaded, rename it
1602 import std
.file
: rename
;
1604 rename(tmpfname
, fname
);
1605 dbPutToCache(furl
, fname
);
1613 // ////////////////////////////////////////////////////////////////////////// //
1614 void main (string
[] args
) {
1617 conRegVar
!oglConScale(1, 4, "r_conscale", "console scale");
1619 conProcessQueue(256*1024); // load config
1620 conProcessArgs
!true(args
);
1621 conProcessQueue(256*1024);
1623 universe
= Galaxy(0);
1625 if (args
.length
== 1) {
1628 foreach (string line
; VFile(buildPath(RcDir
, ".lastfile")).byLineCopy
) {
1629 if (!line
.isComment
) lnn
= line
;
1631 if (lnn
.length
) args
~= lnn
;
1632 } catch (Exception
) {}
1634 if (args
.length
!= 2) assert(0, "invalid number of arguments");
1635 auto furl
= FlibustaUrl(args
[1]);
1636 if (furl
.valid
&& furl
.isFlibusta
) {
1638 scope(exit
) dbCloseCache();
1639 string fn
= fileDown(furl
);
1640 if (fn
.length
== 0) assert(0, "can't download file");
1645 if (args
.length
== 1) assert(0, "no filename");
1649 bookFileName
= args
[1];