summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-02-26 10:24:09 +0200
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-02-26 10:24:09 +0200
commit91a6225d8508db01574d7da2c013cb30d6a87ec8 (patch)
treee3bd2c2f24a22c694c1c23aefd5fc531ae108723
parent4708a6580e9af65cd15769e87487fdf4456f1e00 (diff)
DocumentWidget: Inline downloads
-rw-r--r--CMakeLists.txt6
-rw-r--r--src/app.c55
-rw-r--r--src/app.h1
-rw-r--r--src/gmdocument.c43
-rw-r--r--src/media.c93
-rw-r--r--src/media.h9
-rw-r--r--src/ui/documentwidget.c397
-rw-r--r--src/ui/mediaui.c111
-rw-r--r--src/ui/mediaui.h17
9 files changed, 435 insertions, 297 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 382d3229..e13fc2d5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -142,8 +142,8 @@ set (SOURCES
142 src/ui/metrics.h 142 src/ui/metrics.h
143 src/ui/paint.c 143 src/ui/paint.c
144 src/ui/paint.h 144 src/ui/paint.h
145 src/ui/playerui.c 145 src/ui/mediaui.c
146 src/ui/playerui.h 146 src/ui/mediaui.h
147 src/ui/scrollwidget.c 147 src/ui/scrollwidget.c
148 src/ui/scrollwidget.h 148 src/ui/scrollwidget.h
149 src/ui/sidebarwidget.c 149 src/ui/sidebarwidget.c
@@ -175,7 +175,7 @@ set (SOURCES
175) 175)
176if (IOS) 176if (IOS)
177 add_definitions (-DiPlatformAppleMobile=1) 177 add_definitions (-DiPlatformAppleMobile=1)
178 list (APPEND SOURCES 178 list (APPEND SOURCES
179 src/ios.m 179 src/ios.m
180 src/ios.h 180 src/ios.h
181 app/Images.xcassets 181 app/Images.xcassets
diff --git a/src/app.c b/src/app.c
index c04347b0..ed2c71b4 100644
--- a/src/app.c
+++ b/src/app.c
@@ -581,6 +581,61 @@ const iString *downloadDir_App(void) {
581 return collect_String(cleaned_Path(&app_.prefs.downloadDir)); 581 return collect_String(cleaned_Path(&app_.prefs.downloadDir));
582} 582}
583 583
584const iString *downloadPathForUrl_App(const iString *url, const iString *mime) {
585 /* Figure out a file name from the URL. */
586 iUrl parts;
587 init_Url(&parts, url);
588 while (startsWith_Rangecc(parts.path, "/")) {
589 parts.path.start++;
590 }
591 while (endsWith_Rangecc(parts.path, "/")) {
592 parts.path.end--;
593 }
594 iString *name = collectNewCStr_String("pagecontent");
595 if (isEmpty_Range(&parts.path)) {
596 if (!isEmpty_Range(&parts.host)) {
597 setRange_String(name, parts.host);
598 replace_Block(&name->chars, '.', '_');
599 }
600 }
601 else {
602 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
603 parts.path.end };
604 if (!isEmpty_Range(&fn)) {
605 setRange_String(name, fn);
606 }
607 }
608 if (startsWith_String(name, "~")) {
609 /* This would be interpreted as a reference to a home directory. */
610 remove_Block(&name->chars, 0, 1);
611 }
612 iString *savePath = concat_Path(downloadDir_App(), name);
613 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
614 /* No extension specified in URL. */
615 if (startsWith_String(mime, "text/gemini")) {
616 appendCStr_String(savePath, ".gmi");
617 }
618 else if (startsWith_String(mime, "text/")) {
619 appendCStr_String(savePath, ".txt");
620 }
621 else if (startsWith_String(mime, "image/")) {
622 appendCStr_String(savePath, cstr_String(mime) + 6);
623 }
624 }
625 if (fileExists_FileInfo(savePath)) {
626 /* Make it unique. */
627 iDate now;
628 initCurrent_Date(&now);
629 size_t insPos = lastIndexOfCStr_String(savePath, ".");
630 if (insPos == iInvalidPos) {
631 insPos = size_String(savePath);
632 }
633 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
634 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
635 }
636 return collect_String(savePath);
637}
638
584const iString *debugInfo_App(void) { 639const iString *debugInfo_App(void) {
585 extern char **environ; /* The environment variables. */ 640 extern char **environ; /* The environment variables. */
586 iApp *d = &app_; 641 iApp *d = &app_;
diff --git a/src/app.h b/src/app.h
index e29745c3..9a68c362 100644
--- a/src/app.h
+++ b/src/app.h
@@ -85,6 +85,7 @@ enum iColorTheme colorTheme_App (void);
85const iString * schemeProxy_App (iRangecc scheme); 85const iString * schemeProxy_App (iRangecc scheme);
86iBool willUseProxy_App (const iRangecc scheme); 86iBool willUseProxy_App (const iRangecc scheme);
87const iString * searchQueryUrl_App (const iString *queryStringUnescaped); 87const iString * searchQueryUrl_App (const iString *queryStringUnescaped);
88const iString * downloadPathForUrl_App(const iString *url, const iString *mime);
88 89
89typedef void (*iTickerFunc)(iAny *); 90typedef void (*iTickerFunc)(iAny *);
90 91
diff --git a/src/gmdocument.c b/src/gmdocument.c
index abfefea7..4926587d 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -255,6 +255,15 @@ static iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {
255 return iFalse; 255 return iFalse;
256} 256}
257 257
258static void linkContentLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,
259 uint16_t linkId) {
260 iGmLink *link = at_PtrArray(&d->links, linkId - 1);
261 link->flags |= content_GmLinkFlag;
262 if (mediaInfo && mediaInfo->isPermanent) {
263 link->flags |= permanent_GmLinkFlag;
264 }
265}
266
258static void doLayout_GmDocument_(iGmDocument *d) { 267static void doLayout_GmDocument_(iGmDocument *d) {
259 const iBool isMono = isForcedMonospace_GmDocument_(d); 268 const iBool isMono = isForcedMonospace_GmDocument_(d);
260 /* TODO: Collect these parameters into a GmTheme. */ 269 /* TODO: Collect these parameters into a GmTheme. */
@@ -558,17 +567,12 @@ static void doLayout_GmDocument_(iGmDocument *d) {
558 if (type == link_GmLineType) { 567 if (type == link_GmLineType) {
559 const iMediaId imageId = findLinkImage_Media(d->media, run.linkId); 568 const iMediaId imageId = findLinkImage_Media(d->media, run.linkId);
560 const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0; 569 const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0;
570 const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0;
561 if (imageId) { 571 if (imageId) {
562 iGmMediaInfo img; 572 iGmMediaInfo img;
563 imageInfo_Media(d->media, imageId, &img); 573 imageInfo_Media(d->media, imageId, &img);
564 const iInt2 imgSize = imageSize_Media(d->media, imageId); 574 const iInt2 imgSize = imageSize_Media(d->media, imageId);
565 /* Mark the link as having content. */ { 575 linkContentLaidOut_GmDocument_(d, &img, run.linkId);
566 iGmLink *link = at_PtrArray(&d->links, run.linkId - 1);
567 link->flags |= content_GmLinkFlag;
568 if (img.isPermanent) {
569 link->flags |= permanent_GmLinkFlag;
570 }
571 }
572 const int margin = lineHeight_Text(paragraph_FontId) / 2; 576 const int margin = lineHeight_Text(paragraph_FontId) / 2;
573 pos.y += margin; 577 pos.y += margin;
574 run.bounds.pos = pos; 578 run.bounds.pos = pos;
@@ -596,13 +600,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
596 else if (audioId) { 600 else if (audioId) {
597 iGmMediaInfo info; 601 iGmMediaInfo info;
598 audioInfo_Media(d->media, audioId, &info); 602 audioInfo_Media(d->media, audioId, &info);
599 /* Mark the link as having content. */ { 603 linkContentLaidOut_GmDocument_(d, &info, run.linkId);
600 iGmLink *link = at_PtrArray(&d->links, run.linkId - 1);
601 link->flags |= content_GmLinkFlag;
602 if (info.isPermanent) {
603 link->flags |= permanent_GmLinkFlag;
604 }
605 }
606 const int margin = lineHeight_Text(paragraph_FontId) / 2; 604 const int margin = lineHeight_Text(paragraph_FontId) / 2;
607 pos.y += margin; 605 pos.y += margin;
608 run.bounds.pos = pos; 606 run.bounds.pos = pos;
@@ -616,6 +614,23 @@ static void doLayout_GmDocument_(iGmDocument *d) {
616 pushBack_Array(&d->layout, &run); 614 pushBack_Array(&d->layout, &run);
617 pos.y += run.bounds.size.y + margin; 615 pos.y += run.bounds.size.y + margin;
618 } 616 }
617 else if (downloadId) {
618 iGmMediaInfo info;
619 downloadInfo_Media(d->media, downloadId, &info);
620 linkContentLaidOut_GmDocument_(d, &info, run.linkId);
621 const int margin = lineHeight_Text(paragraph_FontId) / 2;
622 pos.y += margin;
623 run.bounds.pos = pos;
624 run.bounds.size.x = d->size.x;
625 run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;
626 run.visBounds = run.bounds;
627 run.text = iNullRange;
628 run.color = 0;
629 run.mediaType = download_GmRunMediaType;
630 run.mediaId = downloadId;
631 pushBack_Array(&d->layout, &run);
632 pos.y += run.bounds.size.y + margin;
633 }
619 } 634 }
620 prevType = type; 635 prevType = type;
621 } 636 }
diff --git a/src/media.c b/src/media.c
index 65454756..000214b2 100644
--- a/src/media.c
+++ b/src/media.c
@@ -27,10 +27,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
27#include "audio/player.h" 27#include "audio/player.h"
28#include "app.h" 28#include "app.h"
29 29
30#include <the_Foundation/file.h>
30#include <the_Foundation/ptrarray.h> 31#include <the_Foundation/ptrarray.h>
31#include <stb_image.h> 32#include <stb_image.h>
32#include <SDL_hints.h> 33#include <SDL_hints.h>
33#include <SDL_render.h> 34#include <SDL_render.h>
35#include <SDL_timer.h>
34 36
35iDeclareType(GmMediaProps) 37iDeclareType(GmMediaProps)
36 38
@@ -132,15 +134,63 @@ iDeclareType(GmDownload)
132 134
133struct Impl_GmDownload { 135struct Impl_GmDownload {
134 iGmMediaProps props; 136 iGmMediaProps props;
135 /* TODO: Speed statistics. */ 137 uint64_t numBytes;
138 iTime startTime;
139 uint32_t rateStartTime;
140 size_t rateNumBytes;
141 float currentRate;
142 iString * path;
143 iFile * file;
136}; 144};
137 145
146static iBool openFile_GmDownload_(iGmDownload *d) {
147 iAssert(!isEmpty_String(&d->props.url));
148 d->path = copy_String(downloadPathForUrl_App(&d->props.url, &d->props.mime));
149 d->file = new_File(d->path);
150 if (!open_File(d->file, writeOnly_FileMode)) {
151 return iFalse;
152 }
153 return iTrue;
154}
155
156static void closeFile_GmDownload_(iGmDownload *d) {
157 d->currentRate = (float) (d->numBytes / elapsedSeconds_Time(&d->startTime));
158 iReleasePtr(&d->file);
159}
160
138void init_GmDownload(iGmDownload *d) { 161void init_GmDownload(iGmDownload *d) {
139 init_GmMediaProps_(&d->props); 162 init_GmMediaProps_(&d->props);
163 initCurrent_Time(&d->startTime);
164 d->numBytes = 0;
165 d->rateStartTime = SDL_GetTicks();
166 d->rateNumBytes = 0;
167 d->currentRate = 0.0f;
168 d->path = NULL;
169 d->file = NULL;
140} 170}
141 171
142void deinit_GmDownload(iGmDownload *d) { 172void deinit_GmDownload(iGmDownload *d) {
173 closeFile_GmDownload_(d);
143 deinit_GmMediaProps_(&d->props); 174 deinit_GmMediaProps_(&d->props);
175 delete_String(d->path);
176}
177
178static void writeToFile_GmDownload_(iGmDownload *d, const iBlock *data) {
179 const static unsigned rateInterval_ = 1000;
180 iAssert(d->file);
181 writeData_File(d->file,
182 constBegin_Block(data) + d->numBytes,
183 size_Block(data) - d->numBytes);
184 const size_t newBytes = size_Block(data) - d->numBytes;
185 d->numBytes = size_Block(data);
186 d->rateNumBytes += newBytes;
187 const uint32_t now = SDL_GetTicks();
188 if (now - d->rateStartTime > rateInterval_) {
189 const double elapsed = (double) (now - d->rateStartTime) / 1000.0;
190 d->rateStartTime = now;
191 d->currentRate = (float) (d->rateNumBytes / elapsed);
192 d->rateNumBytes = 0;
193 }
144} 194}
145 195
146iDefineTypeConstruction(GmDownload) 196iDefineTypeConstruction(GmDownload)
@@ -183,7 +233,7 @@ void clear_Media(iMedia *d) {
183 clear_PtrArray(&d->downloads); 233 clear_PtrArray(&d->downloads);
184} 234}
185 235
186iBool setUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) { 236iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) {
187 iGmDownload *dl = NULL; 237 iGmDownload *dl = NULL;
188 iMediaId existing = findLinkDownload_Media(d, linkId); 238 iMediaId existing = findLinkDownload_Media(d, linkId);
189 iBool isNew = iFalse; 239 iBool isNew = iFalse;
@@ -251,8 +301,16 @@ iBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo
251 } 301 }
252 else { 302 else {
253 dl = at_PtrArray(&d->downloads, existing - 1); 303 dl = at_PtrArray(&d->downloads, existing - 1);
254 iAssert(equal_String(&dl->props.mime, mime)); /* MIME cannot change */ 304 if (isEmpty_String(&dl->props.mime)) {
255 /* TODO: Write data chunk to file. */ 305 set_String(&dl->props.mime, mime);
306 }
307 if (!dl->file) {
308 openFile_GmDownload_(dl);
309 }
310 writeToFile_GmDownload_(dl, data);
311 if (!isPartial) {
312 closeFile_GmDownload_(dl);
313 }
256 } 314 }
257 } 315 }
258 else if (!isDeleting) { 316 else if (!isDeleting) {
@@ -378,6 +436,33 @@ iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {
378 return NULL; 436 return NULL;
379} 437}
380 438
439iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {
440 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
441 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
442 info_out->type = cstr_String(&dl->props.mime);
443 info_out->isPermanent = dl->props.isPermanent;
444 info_out->numBytes = dl->numBytes;
445 return iTrue;
446 }
447 iZap(*info_out);
448 return iFalse;
449}
450
451void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out,
452 float *bytesPerSecond_out, iBool *isFinished_out) {
453 *path_out = NULL;
454 *bytesPerSecond_out = 0.0f;
455 *isFinished_out = iFalse;
456 if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {
457 const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);
458 if (dl->path) {
459 *path_out = dl->path;
460 }
461 *bytesPerSecond_out = dl->currentRate;
462 *isFinished_out = (dl->path && !dl->file);
463 }
464}
465
381/*----------------------------------------------------------------------------------------------*/ 466/*----------------------------------------------------------------------------------------------*/
382 467
383static void updated_MediaRequest_(iAnyObject *obj) { 468static void updated_MediaRequest_(iAnyObject *obj) {
diff --git a/src/media.h b/src/media.h
index ebead352..ece60630 100644
--- a/src/media.h
+++ b/src/media.h
@@ -46,9 +46,9 @@ enum iMediaFlags {
46 partialData_MediaFlag = iBit(2), 46 partialData_MediaFlag = iBit(2),
47}; 47};
48 48
49void clear_Media (iMedia *); 49void clear_Media (iMedia *);
50iBool setUrl_Media (iMedia *, uint16_t linkId, const iString *url); 50iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url);
51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); 51iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);
52 52
53iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); 53iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId);
54iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); 54iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out);
@@ -61,6 +61,9 @@ iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaI
61iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); 61iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);
62 62
63iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId); 63iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId);
64iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);
65void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out,
66 float *bytesPerSecond_out, iBool *isFinished_out);
64 67
65/*----------------------------------------------------------------------------------------------*/ 68/*----------------------------------------------------------------------------------------------*/
66 69
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 87b8d7ad..6f47a26e 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -41,7 +41,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
41#include "labelwidget.h" 41#include "labelwidget.h"
42#include "media.h" 42#include "media.h"
43#include "paint.h" 43#include "paint.h"
44#include "playerui.h" 44#include "mediaui.h"
45#include "scrollwidget.h" 45#include "scrollwidget.h"
46#include "util.h" 46#include "util.h"
47#include "visbuf.h" 47#include "visbuf.h"
@@ -138,17 +138,7 @@ iDefineTypeConstruction(PersistentDocumentState)
138 138
139/*----------------------------------------------------------------------------------------------*/ 139/*----------------------------------------------------------------------------------------------*/
140 140
141iDeclareType(OutlineItem) 141static void animateMedia_DocumentWidget_ (iDocumentWidget *d);
142
143struct Impl_OutlineItem {
144 iRangecc text;
145 int font;
146 iRect rect;
147};
148
149/*----------------------------------------------------------------------------------------------*/
150
151static void animatePlayers_DocumentWidget_ (iDocumentWidget *d);
152static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d); 142static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d);
153 143
154static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */ 144static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */
@@ -208,10 +198,10 @@ struct Impl_DocumentWidget {
208 iAnim animWideRunOffset; 198 iAnim animWideRunOffset;
209 uint16_t animWideRunId; 199 uint16_t animWideRunId;
210 iGmRunRange animWideRunRange; 200 iGmRunRange animWideRunRange;
211 iPtrArray visiblePlayers; /* currently playing audio */ 201 iPtrArray visibleMedia; /* currently playing audio / ongoing downloads */
212 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ 202 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
213 float grabbedStartVolume; 203 float grabbedStartVolume;
214 int playerTimer; 204 int mediaTimer;
215 const iGmRun * hoverLink; 205 const iGmRun * hoverLink;
216 const iGmRun * contextLink; 206 const iGmRun * contextLink;
217 const iGmRun * firstVisibleRun; 207 const iGmRun * firstVisibleRun;
@@ -221,8 +211,6 @@ struct Impl_DocumentWidget {
221 float initNormScrollY; 211 float initNormScrollY;
222 iAnim scrollY; 212 iAnim scrollY;
223 iAnim sideOpacity; 213 iAnim sideOpacity;
224 iAnim outlineOpacity;
225 iArray outline;
226 iScrollWidget *scroll; 214 iScrollWidget *scroll;
227 iWidget * menu; 215 iWidget * menu;
228 iWidget * playerMenu; 216 iWidget * playerMenu;
@@ -266,9 +254,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
266 d->lastVisibleRun = NULL; 254 d->lastVisibleRun = NULL;
267 d->visBuf = new_VisBuf(); 255 d->visBuf = new_VisBuf();
268 d->invalidRuns = new_PtrSet(); 256 d->invalidRuns = new_PtrSet();
269 init_Array(&d->outline, sizeof(iOutlineItem));
270 init_Anim(&d->sideOpacity, 0); 257 init_Anim(&d->sideOpacity, 0);
271 init_Anim(&d->outlineOpacity, 0);
272 d->sourceStatus = none_GmStatusCode; 258 d->sourceStatus = none_GmStatusCode;
273 init_String(&d->sourceHeader); 259 init_String(&d->sourceHeader);
274 init_String(&d->sourceMime); 260 init_String(&d->sourceMime);
@@ -277,9 +263,9 @@ void init_DocumentWidget(iDocumentWidget *d) {
277 init_PtrArray(&d->visibleLinks); 263 init_PtrArray(&d->visibleLinks);
278 init_PtrArray(&d->visibleWideRuns); 264 init_PtrArray(&d->visibleWideRuns);
279 init_Array(&d->wideRunOffsets, sizeof(int)); 265 init_Array(&d->wideRunOffsets, sizeof(int));
280 init_PtrArray(&d->visiblePlayers); 266 init_PtrArray(&d->visibleMedia);
281 d->grabbedPlayer = NULL; 267 d->grabbedPlayer = NULL;
282 d->playerTimer = 0; 268 d->mediaTimer = 0;
283 init_String(&d->pendingGotoHeading); 269 init_String(&d->pendingGotoHeading);
284 init_Click(&d->click, d, SDL_BUTTON_LEFT); 270 init_Click(&d->click, d, SDL_BUTTON_LEFT);
285 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 271 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
@@ -309,7 +295,6 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
309 delete_TextBuf(d->timestampBuf); 295 delete_TextBuf(d->timestampBuf);
310 delete_VisBuf(d->visBuf); 296 delete_VisBuf(d->visBuf);
311 delete_PtrSet(d->invalidRuns); 297 delete_PtrSet(d->invalidRuns);
312 deinit_Array(&d->outline);
313 iRelease(d->media); 298 iRelease(d->media);
314 iRelease(d->request); 299 iRelease(d->request);
315 deinit_String(&d->pendingGotoHeading); 300 deinit_String(&d->pendingGotoHeading);
@@ -317,11 +302,11 @@ void deinit_DocumentWidget(iDocumentWidget *d) {
317 deinit_String(&d->sourceMime); 302 deinit_String(&d->sourceMime);
318 deinit_String(&d->sourceHeader); 303 deinit_String(&d->sourceHeader);
319 iRelease(d->doc); 304 iRelease(d->doc);
320 if (d->playerTimer) { 305 if (d->mediaTimer) {
321 SDL_RemoveTimer(d->playerTimer); 306 SDL_RemoveTimer(d->mediaTimer);
322 } 307 }
323 deinit_Array(&d->wideRunOffsets); 308 deinit_Array(&d->wideRunOffsets);
324 deinit_PtrArray(&d->visiblePlayers); 309 deinit_PtrArray(&d->visibleMedia);
325 deinit_PtrArray(&d->visibleWideRuns); 310 deinit_PtrArray(&d->visibleWideRuns);
326 deinit_PtrArray(&d->visibleLinks); 311 deinit_PtrArray(&d->visibleLinks);
327 delete_Block(d->certFingerprint); 312 delete_Block(d->certFingerprint);
@@ -423,9 +408,9 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) {
423 if (run->preId && run->flags & wide_GmRunFlag) { 408 if (run->preId && run->flags & wide_GmRunFlag) {
424 pushBack_PtrArray(&d->visibleWideRuns, run); 409 pushBack_PtrArray(&d->visibleWideRuns, run);
425 } 410 }
426 if (run->mediaType == audio_GmRunMediaType) { 411 if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) {
427 iAssert(run->mediaId); 412 iAssert(run->mediaId);
428 pushBack_PtrArray(&d->visiblePlayers, run); 413 pushBack_PtrArray(&d->visibleMedia, run);
429 } 414 }
430 if (run->linkId) { 415 if (run->linkId) {
431 pushBack_PtrArray(&d->visibleLinks, run); 416 pushBack_PtrArray(&d->visibleLinks, run);
@@ -534,7 +519,7 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) {
534 519
535static void animate_DocumentWidget_(void *ticker) { 520static void animate_DocumentWidget_(void *ticker) {
536 iDocumentWidget *d = ticker; 521 iDocumentWidget *d = ticker;
537 if (!isFinished_Anim(&d->sideOpacity) || !isFinished_Anim(&d->outlineOpacity)) { 522 if (!isFinished_Anim(&d->sideOpacity)) {
538 addTicker_App(animate_DocumentWidget_, d); 523 addTicker_App(animate_DocumentWidget_, d);
539 } 524 }
540} 525}
@@ -549,71 +534,66 @@ static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimat
549 animate_DocumentWidget_(d); 534 animate_DocumentWidget_(d);
550} 535}
551 536
552static void updateOutlineOpacity_DocumentWidget_(iDocumentWidget *d) { 537static uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
553 float opacity = 0.0f;
554 if (isEmpty_Array(&d->outline)) {
555 setValue_Anim(&d->outlineOpacity, 0.0f, 0);
556 return;
557 }
558 if (contains_Widget(constAs_Widget(d->scroll), mouseCoord_Window(get_Window()))) {
559 opacity = 1.0f;
560 }
561 setValue_Anim(&d->outlineOpacity, opacity, opacity > 0.5f? 100 : 166);
562 animate_DocumentWidget_(d);
563}
564
565static uint32_t playerUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {
566 if (document_App() != d) { 538 if (document_App() != d) {
567 return 0; 539 return 0;
568 } 540 }
569 uint32_t interval = 0; 541 static const uint32_t invalidInterval_ = ~0u;
570 iConstForEach(PtrArray, i, &d->visiblePlayers) { 542 uint32_t interval = invalidInterval_;
543 iConstForEach(PtrArray, i, &d->visibleMedia) {
571 const iGmRun *run = i.ptr; 544 const iGmRun *run = i.ptr;
572 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 545 if (run->mediaType == audio_GmRunMediaType) {
573 if (flags_Player(plr) & adjustingVolume_PlayerFlag || 546 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
574 (isStarted_Player(plr) && !isPaused_Player(plr))) { 547 if (flags_Player(plr) & adjustingVolume_PlayerFlag ||
575 interval = 1000 / 15; 548 (isStarted_Player(plr) && !isPaused_Player(plr))) {
549 interval = iMin(interval, 1000 / 15);
550 }
551 }
552 else if (run->mediaType == download_GmRunMediaType) {
553 interval = iMin(interval, 1000);
576 } 554 }
577 } 555 }
578 return interval; 556 return interval != invalidInterval_ ? interval : 0;
579} 557}
580 558
581static uint32_t postPlayerUpdate_DocumentWidget_(uint32_t interval, void *context) { 559static uint32_t postMediaUpdate_DocumentWidget_(uint32_t interval, void *context) {
582 /* Called in timer thread; don't access the widget. */ 560 /* Called in timer thread; don't access the widget. */
583 iUnused(context); 561 iUnused(context);
584 postCommand_App("media.player.update"); 562 postCommand_App("media.player.update");
585 return interval; 563 return interval;
586} 564}
587 565
588static void updatePlayers_DocumentWidget_(iDocumentWidget *d) { 566static void updateMedia_DocumentWidget_(iDocumentWidget *d) {
589 if (document_App() == d) { 567 if (document_App() == d) {
590 refresh_Widget(d); 568 refresh_Widget(d);
591 iConstForEach(PtrArray, i, &d->visiblePlayers) { 569 iConstForEach(PtrArray, i, &d->visibleMedia) {
592 const iGmRun *run = i.ptr; 570 const iGmRun *run = i.ptr;
593 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 571 if (run->mediaType == audio_GmRunMediaType) {
594 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && 572 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
595 flags_Player(plr) & adjustingVolume_PlayerFlag) { 573 if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&
596 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); 574 flags_Player(plr) & adjustingVolume_PlayerFlag) {
575 setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);
576 }
597 } 577 }
598 } 578 }
599 } 579 }
600 if (d->playerTimer && playerUpdateInterval_DocumentWidget_(d) == 0) { 580 if (d->mediaTimer && mediaUpdateInterval_DocumentWidget_(d) == 0) {
601 SDL_RemoveTimer(d->playerTimer); 581 SDL_RemoveTimer(d->mediaTimer);
602 d->playerTimer = 0; 582 d->mediaTimer = 0;
603 } 583 }
604} 584}
605 585
606static void animatePlayers_DocumentWidget_(iDocumentWidget *d) { 586static void animateMedia_DocumentWidget_(iDocumentWidget *d) {
607 if (document_App() != d) { 587 if (document_App() != d) {
608 if (d->playerTimer) { 588 if (d->mediaTimer) {
609 SDL_RemoveTimer(d->playerTimer); 589 SDL_RemoveTimer(d->mediaTimer);
610 d->playerTimer = 0; 590 d->mediaTimer = 0;
611 } 591 }
612 return; 592 return;
613 } 593 }
614 uint32_t interval = playerUpdateInterval_DocumentWidget_(d); 594 uint32_t interval = mediaUpdateInterval_DocumentWidget_(d);
615 if (interval && !d->playerTimer) { 595 if (interval && !d->mediaTimer) {
616 d->playerTimer = SDL_AddTimer(interval, postPlayerUpdate_DocumentWidget_, d); 596 d->mediaTimer = SDL_AddTimer(interval, postMediaUpdate_DocumentWidget_, d);
617 } 597 }
618} 598}
619 599
@@ -649,7 +629,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
649 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); 629 docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0);
650 clear_PtrArray(&d->visibleLinks); 630 clear_PtrArray(&d->visibleLinks);
651 clear_PtrArray(&d->visibleWideRuns); 631 clear_PtrArray(&d->visibleWideRuns);
652 clear_PtrArray(&d->visiblePlayers); 632 clear_PtrArray(&d->visibleMedia);
653 const iRangecc oldHeading = currentHeading_DocumentWidget_(d); 633 const iRangecc oldHeading = currentHeading_DocumentWidget_(d);
654 /* Scan for visible runs. */ { 634 /* Scan for visible runs. */ {
655 d->firstVisibleRun = NULL; 635 d->firstVisibleRun = NULL;
@@ -661,7 +641,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) {
661 } 641 }
662 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window())); 642 updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window()));
663 updateSideOpacity_DocumentWidget_(d, iTrue); 643 updateSideOpacity_DocumentWidget_(d, iTrue);
664 animatePlayers_DocumentWidget_(d); 644 animateMedia_DocumentWidget_(d);
665 /* Remember scroll positions of recently visited pages. */ { 645 /* Remember scroll positions of recently visited pages. */ {
666 iRecentUrl *recent = mostRecentUrl_History(d->mod.history); 646 iRecentUrl *recent = mostRecentUrl_History(d->mod.history);
667 if (recent && docSize && d->state == ready_RequestState) { 647 if (recent && docSize && d->state == ready_RequestState) {
@@ -756,55 +736,11 @@ static void invalidate_DocumentWidget_(iDocumentWidget *d) {
756 clear_PtrSet(d->invalidRuns); 736 clear_PtrSet(d->invalidRuns);
757} 737}
758 738
759static int outlineWidth_DocumentWidget_(const iDocumentWidget *d) {
760 const iWidget *w = constAs_Widget(d);
761 const iRect bounds = bounds_Widget(w);
762 const int docWidth = documentWidth_DocumentWidget_(d);
763 int width =
764 (width_Rect(bounds) - docWidth) / 2 - gap_Text * d->pageMargin - gap_UI * d->pageMargin
765 - 2 * outlinePadding_DocumentWidget_ * gap_UI;
766 if (width < outlineMinWidth_DocumentWdiget_ * gap_UI) {
767 return outlineMinWidth_DocumentWdiget_ * gap_UI;
768 }
769 return iMin(width, outlineMaxWidth_DocumentWidget_ * gap_UI);
770}
771
772static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) { 739static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) {
773 return isEmpty_String(d->titleUser) ? range_String(bannerText_GmDocument(d->doc)) 740 return isEmpty_String(d->titleUser) ? range_String(bannerText_GmDocument(d->doc))
774 : range_String(d->titleUser); 741 : range_String(d->titleUser);
775} 742}
776 743
777static void updateOutline_DocumentWidget_(iDocumentWidget *d) {
778 iWidget *w = as_Widget(d);
779 int outWidth = outlineWidth_DocumentWidget_(d);
780 clear_Array(&d->outline);
781 if (outWidth == 0 || d->state != ready_RequestState) {
782 return;
783 }
784 if (size_GmDocument(d->doc).y < height_Rect(bounds_Widget(w)) * 2) {
785 return; /* Too short */
786 }
787 iInt2 pos = zero_I2();
788// const iRangecc topText = urlHost_String(d->mod.url);
789// iInt2 size = advanceWrapRange_Text(uiContent_FontId, outWidth, topText);
790// pushBack_Array(&d->outline, &(iOutlineItem){ topText, uiContent_FontId, (iRect){ pos, size },
791// tmBannerTitle_ColorId, none_ColorId });
792// pos.y += size.y;
793 iInt2 size;
794 iConstForEach(Array, i, headings_GmDocument(d->doc)) {
795 const iGmHeading *head = i.value;
796 const int indent = head->level * 5 * gap_UI;
797 size = advanceWrapRange_Text(uiLabel_FontId, outWidth - indent, head->text);
798 if (head->level == 0) {
799 pos.y += gap_UI * 1.5f;
800 }
801 pushBack_Array(
802 &d->outline,
803 &(iOutlineItem){ head->text, uiLabel_FontId, (iRect){ addX_I2(pos, indent), size } });
804 pos.y += size.y;
805 }
806}
807
808static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { 744static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) {
809 d->foundMark = iNullRange; 745 d->foundMark = iNullRange;
810 d->selectMark = iNullRange; 746 d->selectMark = iNullRange;
@@ -818,11 +754,9 @@ static void setSource_DocumentWidget_(iDocumentWidget *d, const iString *source)
818 setUrl_GmDocument(d->doc, d->mod.url); 754 setUrl_GmDocument(d->doc, d->mod.url);
819 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d)); 755 setSource_GmDocument(d->doc, source, documentWidth_DocumentWidget_(d));
820 documentRunsInvalidated_DocumentWidget_(d); 756 documentRunsInvalidated_DocumentWidget_(d);
821 setValue_Anim(&d->outlineOpacity, 0.0f, 0);
822 updateWindowTitle_DocumentWidget_(d); 757 updateWindowTitle_DocumentWidget_(d);
823 updateVisible_DocumentWidget_(d); 758 updateVisible_DocumentWidget_(d);
824 updateSideIconBuf_DocumentWidget_(d); 759 updateSideIconBuf_DocumentWidget_(d);
825 updateOutline_DocumentWidget_(d);
826 invalidate_DocumentWidget_(d); 760 invalidate_DocumentWidget_(d);
827 refresh_Widget(as_Widget(d)); 761 refresh_Widget(as_Widget(d));
828} 762}
@@ -1090,7 +1024,6 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) {
1090 d->state = ready_RequestState; 1024 d->state = ready_RequestState;
1091 updateSideOpacity_DocumentWidget_(d, iFalse); 1025 updateSideOpacity_DocumentWidget_(d, iFalse);
1092 updateSideIconBuf_DocumentWidget_(d); 1026 updateSideIconBuf_DocumentWidget_(d);
1093 updateOutline_DocumentWidget_(d);
1094 updateVisible_DocumentWidget_(d); 1027 updateVisible_DocumentWidget_(d);
1095 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 1028 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1096 return iTrue; 1029 return iTrue;
@@ -1377,14 +1310,18 @@ static iMediaRequest *findMediaRequest_DocumentWidget_(const iDocumentWidget *d,
1377 1310
1378static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { 1311static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) {
1379 if (!findMediaRequest_DocumentWidget_(d, linkId)) { 1312 if (!findMediaRequest_DocumentWidget_(d, linkId)) {
1380 const iString *imageUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId)); 1313 const iString *mediaUrl = absoluteUrl_String(d->mod.url, linkUrl_GmDocument(d->doc, linkId));
1381 pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, imageUrl))); 1314 pushBack_ObjectList(d->media, iClob(new_MediaRequest(d, linkId, mediaUrl)));
1382 invalidate_DocumentWidget_(d); 1315 invalidate_DocumentWidget_(d);
1383 return iTrue; 1316 return iTrue;
1384 } 1317 }
1385 return iFalse; 1318 return iFalse;
1386} 1319}
1387 1320
1321static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) {
1322 return findLinkDownload_Media(constMedia_GmDocument(d->doc), req->linkId) != 0;
1323}
1324
1388static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { 1325static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) {
1389 iMediaRequest *req = pointerLabel_Command(cmd, "request"); 1326 iMediaRequest *req = pointerLabel_Command(cmd, "request");
1390 iBool isOurRequest = iFalse; 1327 iBool isOurRequest = iFalse;
@@ -1403,7 +1340,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
1403 const enum iGmStatusCode code = status_GmRequest(req->req); 1340 const enum iGmStatusCode code = status_GmRequest(req->req);
1404 if (isSuccess_GmStatusCode(code)) { 1341 if (isSuccess_GmStatusCode(code)) {
1405 iGmResponse *resp = lockResponse_GmRequest(req->req); 1342 iGmResponse *resp = lockResponse_GmRequest(req->req);
1406 if (startsWith_String(&resp->meta, "audio/")) { 1343 if (isDownloadRequest_DocumentWidget(d, req) ||
1344 startsWith_String(&resp->meta, "audio/")) {
1407 /* TODO: Use a helper? This is same as below except for the partialData flag. */ 1345 /* TODO: Use a helper? This is same as below except for the partialData flag. */
1408 if (setData_Media(media_GmDocument(d->doc), 1346 if (setData_Media(media_GmDocument(d->doc),
1409 req->linkId, 1347 req->linkId,
@@ -1427,7 +1365,8 @@ static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *
1427 const enum iGmStatusCode code = status_GmRequest(req->req); 1365 const enum iGmStatusCode code = status_GmRequest(req->req);
1428 /* Give the media to the document for presentation. */ 1366 /* Give the media to the document for presentation. */
1429 if (isSuccess_GmStatusCode(code)) { 1367 if (isSuccess_GmStatusCode(code)) {
1430 if (startsWith_String(meta_GmRequest(req->req), "image/") || 1368 if (isDownloadRequest_DocumentWidget(d, req) ||
1369 startsWith_String(meta_GmRequest(req->req), "image/") ||
1431 startsWith_String(meta_GmRequest(req->req), "audio/")) { 1370 startsWith_String(meta_GmRequest(req->req), "audio/")) {
1432 setData_Media(media_GmDocument(d->doc), 1371 setData_Media(media_GmDocument(d->doc),
1433 req->linkId, 1372 req->linkId,
@@ -1481,57 +1420,7 @@ static iBool fetchNextUnfetchedImage_DocumentWidget_(iDocumentWidget *d) {
1481} 1420}
1482 1421
1483static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) { 1422static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) {
1484 /* Figure out a file name from the URL. */ 1423 const iString *savePath = downloadPathForUrl_App(url, mime);
1485 iUrl parts;
1486 init_Url(&parts, url);
1487 while (startsWith_Rangecc(parts.path, "/")) {
1488 parts.path.start++;
1489 }
1490 while (endsWith_Rangecc(parts.path, "/")) {
1491 parts.path.end--;
1492 }
1493 iString *name = collectNewCStr_String("pagecontent");
1494 if (isEmpty_Range(&parts.path)) {
1495 if (!isEmpty_Range(&parts.host)) {
1496 setRange_String(name, parts.host);
1497 replace_Block(&name->chars, '.', '_');
1498 }
1499 }
1500 else {
1501 iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,
1502 parts.path.end };
1503 if (!isEmpty_Range(&fn)) {
1504 setRange_String(name, fn);
1505 }
1506 }
1507 if (startsWith_String(name, "~")) {
1508 /* This would be interpreted as a reference to a home directory. */
1509 remove_Block(&name->chars, 0, 1);
1510 }
1511 iString *savePath = concat_Path(downloadDir_App(), name);
1512 if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {
1513 /* No extension specified in URL. */
1514 if (startsWith_String(mime, "text/gemini")) {
1515 appendCStr_String(savePath, ".gmi");
1516 }
1517 else if (startsWith_String(mime, "text/")) {
1518 appendCStr_String(savePath, ".txt");
1519 }
1520 else if (startsWith_String(mime, "image/")) {
1521 appendCStr_String(savePath, cstr_String(mime) + 6);
1522 }
1523 }
1524 if (fileExists_FileInfo(savePath)) {
1525 /* Make it unique. */
1526 iDate now;
1527 initCurrent_Date(&now);
1528 size_t insPos = lastIndexOfCStr_String(savePath, ".");
1529 if (insPos == iInvalidPos) {
1530 insPos = size_String(savePath);
1531 }
1532 const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));
1533 insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));
1534 }
1535 /* Write the file. */ { 1424 /* Write the file. */ {
1536 iFile *f = new_File(savePath); 1425 iFile *f = new_File(savePath);
1537 if (open_File(f, writeOnly_FileMode)) { 1426 if (open_File(f, writeOnly_FileMode)) {
@@ -1549,7 +1438,6 @@ static void saveToDownloads_(const iString *url, const iString *mime, const iBlo
1549 } 1438 }
1550 iRelease(f); 1439 iRelease(f);
1551 } 1440 }
1552 delete_String(savePath);
1553} 1441}
1554 1442
1555static void addAllLinks_(void *context, const iGmRun *run) { 1443static void addAllLinks_(void *context, const iGmRun *run) {
@@ -1626,7 +1514,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1626 const iBool keepCenter = equal_Command(cmd, "font.changed"); 1514 const iBool keepCenter = equal_Command(cmd, "font.changed");
1627 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter); 1515 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, keepCenter);
1628 updateSideIconBuf_DocumentWidget_(d); 1516 updateSideIconBuf_DocumentWidget_(d);
1629 updateOutline_DocumentWidget_(d);
1630 invalidate_DocumentWidget_(d); 1517 invalidate_DocumentWidget_(d);
1631 dealloc_VisBuf(d->visBuf); 1518 dealloc_VisBuf(d->visBuf);
1632 updateWindowTitle_DocumentWidget_(d); 1519 updateWindowTitle_DocumentWidget_(d);
@@ -1641,7 +1528,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1641 return iFalse; 1528 return iFalse;
1642 } 1529 }
1643 else if (equal_Command(cmd, "window.mouse.exited")) { 1530 else if (equal_Command(cmd, "window.mouse.exited")) {
1644 updateOutlineOpacity_DocumentWidget_(d);
1645 return iFalse; 1531 return iFalse;
1646 } 1532 }
1647 else if (equal_Command(cmd, "theme.changed") && document_App() == d) { 1533 else if (equal_Command(cmd, "theme.changed") && document_App() == d) {
@@ -1666,10 +1552,9 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1666 } 1552 }
1667 init_Anim(&d->sideOpacity, 0); 1553 init_Anim(&d->sideOpacity, 0);
1668 updateSideOpacity_DocumentWidget_(d, iFalse); 1554 updateSideOpacity_DocumentWidget_(d, iFalse);
1669 updateOutlineOpacity_DocumentWidget_(d);
1670 updateWindowTitle_DocumentWidget_(d); 1555 updateWindowTitle_DocumentWidget_(d);
1671 allocVisBuffer_DocumentWidget_(d); 1556 allocVisBuffer_DocumentWidget_(d);
1672 animatePlayers_DocumentWidget_(d); 1557 animateMedia_DocumentWidget_(d);
1673 return iFalse; 1558 return iFalse;
1674 } 1559 }
1675 else if (equal_Command(cmd, "tab.created")) { 1560 else if (equal_Command(cmd, "tab.created")) {
@@ -1795,6 +1680,19 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1795 } 1680 }
1796 return iTrue; 1681 return iTrue;
1797 } 1682 }
1683 else if (equalWidget_Command(cmd, w, "document.downloadlink")) {
1684 if (d->contextLink) {
1685 const iGmLinkId linkId = d->contextLink->linkId;
1686 setDownloadUrl_Media(
1687 media_GmDocument(d->doc), linkId, linkUrl_GmDocument(d->doc, linkId));
1688 requestMedia_DocumentWidget_(d, linkId);
1689 redoLayout_GmDocument(d->doc); /* inline downloader becomes visible */
1690 updateVisible_DocumentWidget_(d);
1691 invalidate_DocumentWidget_(d);
1692 refresh_Widget(w);
1693 }
1694 return iTrue;
1695 }
1798 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) { 1696 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) {
1799 iString *value = suffix_Command(cmd, "value"); 1697 iString *value = suffix_Command(cmd, "value");
1800 set_String(value, collect_String(urlEncode_String(value))); 1698 set_String(value, collect_String(urlEncode_String(value)));
@@ -1851,7 +1749,6 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1851 iReleasePtr(&d->request); 1749 iReleasePtr(&d->request);
1852 updateVisible_DocumentWidget_(d); 1750 updateVisible_DocumentWidget_(d);
1853 updateSideIconBuf_DocumentWidget_(d); 1751 updateSideIconBuf_DocumentWidget_(d);
1854 updateOutline_DocumentWidget_(d);
1855 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url)); 1752 postCommandf_App("document.changed doc:%p url:%s", d, cstr_String(d->mod.url));
1856 /* Check for a pending goto. */ 1753 /* Check for a pending goto. */
1857 if (!isEmpty_String(&d->pendingGotoHeading)) { 1754 if (!isEmpty_String(&d->pendingGotoHeading)) {
@@ -1876,7 +1773,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
1876 } 1773 }
1877 } 1774 }
1878 else if (equal_Command(cmd, "media.player.update")) { 1775 else if (equal_Command(cmd, "media.player.update")) {
1879 updatePlayers_DocumentWidget_(d); 1776 updateMedia_DocumentWidget_(d);
1880 return iFalse; 1777 return iFalse;
1881 } 1778 }
1882 else if (equal_Command(cmd, "document.stop") && document_App() == d) { 1779 else if (equal_Command(cmd, "document.stop") && document_App() == d) {
@@ -2170,7 +2067,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2170 return iFalse; 2067 return iFalse;
2171} 2068}
2172 2069
2173static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { 2070static iRect runRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
2174 const iRect docBounds = documentBounds_DocumentWidget_(d); 2071 const iRect docBounds = documentBounds_DocumentWidget_(d);
2175 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY))); 2072 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY)));
2176} 2073}
@@ -2196,7 +2093,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r
2196 } 2093 }
2197} 2094}
2198 2095
2199static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 2096static iBool processMediaEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
2200 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP && 2097 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP &&
2201 ev->type != SDL_MOUSEMOTION) { 2098 ev->type != SDL_MOUSEMOTION) {
2202 return iFalse; 2099 return iFalse;
@@ -2211,10 +2108,13 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
2211 return iFalse; 2108 return iFalse;
2212 } 2109 }
2213 const iInt2 mouse = init_I2(ev->button.x, ev->button.y); 2110 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
2214 iConstForEach(PtrArray, i, &d->visiblePlayers) { 2111 iConstForEach(PtrArray, i, &d->visibleMedia) {
2215 const iGmRun *run = i.ptr; 2112 const iGmRun *run = i.ptr;
2216 const iRect rect = playerRect_DocumentWidget_(d, run); 2113 if (run->mediaType != audio_GmRunMediaType) {
2217 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 2114 continue;
2115 }
2116 const iRect rect = runRect_DocumentWidget_(d, run);
2117 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);
2218 if (contains_Rect(rect, mouse)) { 2118 if (contains_Rect(rect, mouse)) {
2219 iPlayerUI ui; 2119 iPlayerUI ui;
2220 init_PlayerUI(&ui, plr, rect); 2120 init_PlayerUI(&ui, plr, rect);
@@ -2235,7 +2135,7 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
2235 } 2135 }
2236 if (contains_Rect(ui.playPauseRect, mouse)) { 2136 if (contains_Rect(ui.playPauseRect, mouse)) {
2237 setPaused_Player(plr, !isPaused_Player(plr)); 2137 setPaused_Player(plr, !isPaused_Player(plr));
2238 animatePlayers_DocumentWidget_(d); 2138 animateMedia_DocumentWidget_(d);
2239 return iTrue; 2139 return iTrue;
2240 } 2140 }
2241 else if (contains_Rect(ui.rewindRect, mouse)) { 2141 else if (contains_Rect(ui.rewindRect, mouse)) {
@@ -2251,7 +2151,7 @@ static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_E
2251 setFlags_Player(plr, 2151 setFlags_Player(plr,
2252 adjustingVolume_PlayerFlag, 2152 adjustingVolume_PlayerFlag,
2253 !(flags_Player(plr) & adjustingVolume_PlayerFlag)); 2153 !(flags_Player(plr) & adjustingVolume_PlayerFlag));
2254 animatePlayers_DocumentWidget_(d); 2154 animateMedia_DocumentWidget_(d);
2255 refresh_Widget(d); 2155 refresh_Widget(d);
2256 return iTrue; 2156 return iTrue;
2257 } 2157 }
@@ -2480,7 +2380,6 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2480 else { 2380 else {
2481 updateHover_DocumentWidget_(d, mpos); 2381 updateHover_DocumentWidget_(d, mpos);
2482 } 2382 }
2483 updateOutlineOpacity_DocumentWidget_(d);
2484 } 2383 }
2485 if (ev->type == SDL_MOUSEBUTTONDOWN) { 2384 if (ev->type == SDL_MOUSEBUTTONDOWN) {
2486 if (ev->button.button == SDL_BUTTON_X1) { 2385 if (ev->button.button == SDL_BUTTON_X1) {
@@ -2508,11 +2407,14 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2508 init_Array(&items, sizeof(iMenuItem)); 2407 init_Array(&items, sizeof(iMenuItem));
2509 if (d->contextLink) { 2408 if (d->contextLink) {
2510 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId); 2409 const iString *linkUrl = linkUrl_GmDocument(d->doc, d->contextLink->linkId);
2410 const int linkFlags = linkFlags_GmDocument(d->doc, d->contextLink->linkId);
2511 const iRangecc scheme = urlScheme_String(linkUrl); 2411 const iRangecc scheme = urlScheme_String(linkUrl);
2512 const iBool isGemini = equalCase_Rangecc(scheme, "gemini"); 2412 const iBool isGemini = equalCase_Rangecc(scheme, "gemini");
2413 iBool isNative = iFalse;
2513 if (willUseProxy_App(scheme) || isGemini || 2414 if (willUseProxy_App(scheme) || isGemini ||
2514 equalCase_Rangecc(scheme, "finger") || 2415 equalCase_Rangecc(scheme, "finger") ||
2515 equalCase_Rangecc(scheme, "gopher")) { 2416 equalCase_Rangecc(scheme, "gopher")) {
2417 isNative = iTrue;
2516 /* Regular links that we can open. */ 2418 /* Regular links that we can open. */
2517 pushBackN_Array( 2419 pushBackN_Array(
2518 &items, 2420 &items,
@@ -2556,10 +2458,18 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2556 0, 2458 0,
2557 format_CStr("!bookmark.add title:%s url:%s", 2459 format_CStr("!bookmark.add title:%s url:%s",
2558 cstr_String(linkLabel), 2460 cstr_String(linkLabel),
2559 cstr_String(linkUrl)) } }, 2461 cstr_String(linkUrl)) },
2462 },
2560 3); 2463 3);
2464 if (isNative && d->contextLink->mediaType != download_GmRunMediaType) {
2465 pushBackN_Array(&items, (iMenuItem[]){
2466 { "---", 0, 0, NULL },
2467 { "Download Linked File", 0, 0, "document.downloadlink" },
2468 }, 2);
2469 }
2561 iMediaRequest *mediaReq; 2470 iMediaRequest *mediaReq;
2562 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL) { 2471 if ((mediaReq = findMediaRequest_DocumentWidget_(d, d->contextLink->linkId)) != NULL &&
2472 d->contextLink->mediaType != download_GmRunMediaType) {
2563 if (isFinished_GmRequest(mediaReq->req)) { 2473 if (isFinished_GmRequest(mediaReq->req)) {
2564 pushBack_Array(&items, 2474 pushBack_Array(&items,
2565 &(iMenuItem){ "Save to Downloads", 2475 &(iMenuItem){ "Save to Downloads",
@@ -2610,7 +2520,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2610 processContextMenuEvent_Widget(d->menu, ev, {}); 2520 processContextMenuEvent_Widget(d->menu, ev, {});
2611 } 2521 }
2612 } 2522 }
2613 if (processPlayerEvents_DocumentWidget_(d, ev)) { 2523 if (processMediaEvents_DocumentWidget_(d, ev)) {
2614 return iTrue; 2524 return iTrue;
2615 } 2525 }
2616 /* The left mouse button. */ 2526 /* The left mouse button. */
@@ -2623,7 +2533,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2623 iPlayer *plr = 2533 iPlayer *plr =
2624 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId); 2534 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->mediaId);
2625 iPlayerUI ui; 2535 iPlayerUI ui;
2626 init_PlayerUI(&ui, plr, playerRect_DocumentWidget_(d, d->grabbedPlayer)); 2536 init_PlayerUI(&ui, plr, runRect_DocumentWidget_(d, d->grabbedPlayer));
2627 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); 2537 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
2628 setVolume_Player(plr, d->grabbedStartVolume + off); 2538 setVolume_Player(plr, d->grabbedStartVolume + off);
2629 refresh_Widget(w); 2539 refresh_Widget(w);
@@ -2937,8 +2847,8 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
2937 } 2847 }
2938 return; 2848 return;
2939 } 2849 }
2940 else if (run->mediaType == audio_GmRunMediaType) { 2850 else if (run->mediaType) {
2941 /* Audio player UI is drawn afterwards as a dynamic overlay. */ 2851 /* Media UIs are drawn afterwards as a dynamic overlay. */
2942 return; 2852 return;
2943 } 2853 }
2944 enum iColorId fg = run->color; 2854 enum iColorId fg = run->color;
@@ -3002,7 +2912,9 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3002 init_String(&text); 2912 init_String(&text);
3003 iMediaId imageId = linkImage_GmDocument(doc, run->linkId); 2913 iMediaId imageId = linkImage_GmDocument(doc, run->linkId);
3004 iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0; 2914 iMediaId audioId = !imageId ? linkAudio_GmDocument(doc, run->linkId) : 0;
3005 iAssert(imageId || audioId); 2915 iMediaId downloadId = !imageId && !audioId ?
2916 findLinkDownload_Media(constMedia_GmDocument(doc), run->linkId) : 0;
2917 iAssert(imageId || audioId || downloadId);
3006 if (imageId) { 2918 if (imageId) {
3007 iAssert(!isEmpty_Rect(run->bounds)); 2919 iAssert(!isEmpty_Rect(run->bounds));
3008 iGmMediaInfo info; 2920 iGmMediaInfo info;
@@ -3016,6 +2928,11 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) {
3016 audioInfo_Media(constMedia_GmDocument(doc), audioId, &info); 2928 audioInfo_Media(constMedia_GmDocument(doc), audioId, &info);
3017 format_String(&text, "%s", info.type); 2929 format_String(&text, "%s", info.type);
3018 } 2930 }
2931 else if (downloadId) {
2932 iGmMediaInfo info;
2933 downloadInfo_Media(constMedia_GmDocument(doc), downloadId, &info);
2934 format_String(&text, "%s", info.type);
2935 }
3019 if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) { 2936 if (findMediaRequest_DocumentWidget_(d->widget, run->linkId)) {
3020 appendFormat_String( 2937 appendFormat_String(
3021 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : ""); 2938 &text, " %s\u2a2f", isHover ? escape_Color(tmLinkText_ColorId) : "");
@@ -3142,11 +3059,11 @@ static void updateSideIconBuf_DocumentWidget_(iDocumentWidget *d) {
3142 if (!banner) { 3059 if (!banner) {
3143 return; 3060 return;
3144 } 3061 }
3145 const int margin = gap_UI * d->pageMargin; 3062 const int margin = gap_UI * d->pageMargin;
3146 const int minBannerSize = lineHeight_Text(banner_FontId) * 2; 3063 const int minBannerSize = lineHeight_Text(banner_FontId) * 2;
3147 const iChar icon = siteIcon_GmDocument(d->doc); 3064 const iChar icon = siteIcon_GmDocument(d->doc);
3148 const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin; 3065 const int avail = sideElementAvailWidth_DocumentWidget_(d) - margin;
3149 iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d); 3066 iBool isHeadingVisible = isSideHeadingVisible_DocumentWidget_(d);
3150 /* Determine the required size. */ 3067 /* Determine the required size. */
3151 iInt2 bufSize = init1_I2(minBannerSize); 3068 iInt2 bufSize = init1_I2(minBannerSize);
3152 if (isHeadingVisible) { 3069 if (isHeadingVisible) {
@@ -3223,74 +3140,24 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) {
3223 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))), 3140 iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))),
3224 tmQuoteIcon_ColorId); 3141 tmQuoteIcon_ColorId);
3225 } 3142 }
3226#if 0
3227 /* Outline on the right side. */
3228 const float outlineOpacity = value_Anim(&d->outlineOpacity);
3229 if (prefs_App()->hoverOutline && !isEmpty_Array(&d->outline) && outlineOpacity > 0.0f) {
3230 /* TODO: This is very slow to draw; should be buffered appropriately. */
3231 const int innerWidth = outlineWidth_DocumentWidget_(d);
3232 const int outWidth = innerWidth + 2 * outlinePadding_DocumentWidget_ * gap_UI;
3233 const int topMargin = 0;
3234 const int bottomMargin = 3 * gap_UI;
3235 const int scrollMax = scrollMax_DocumentWidget_(d);
3236 const int outHeight = outlineHeight_DocumentWidget_(d);
3237 const int oversize = outHeight - height_Rect(bounds) + topMargin + bottomMargin;
3238 const int scroll = (oversize > 0 && scrollMax > 0
3239 ? oversize * value_Anim(&d->scrollY) / scrollMax_DocumentWidget_(d)
3240 : 0);
3241 iInt2 pos =
3242 add_I2(topRight_Rect(bounds), init_I2(-outWidth - width_Widget(d->scroll), topMargin));
3243 /* Center short outlines vertically. */
3244 if (oversize < 0) {
3245 pos.y -= oversize / 2;
3246 }
3247 pos.y -= scroll;
3248 setOpacity_Text(outlineOpacity);
3249 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_BLEND);
3250 p.alpha = outlineOpacity * 255;
3251 iRect outlineFrame = {
3252 addY_I2(pos, -outlinePadding_DocumentWidget_ * gap_UI / 2),
3253 init_I2(outWidth, outHeight + outlinePadding_DocumentWidget_ * gap_UI * 1.5f)
3254 };
3255 fillRect_Paint(&p, outlineFrame, tmBannerBackground_ColorId);
3256 drawSideRect_(&p, outlineFrame);
3257 iBool wasAbove = iTrue;
3258 iConstForEach(Array, i, &d->outline) {
3259 const iOutlineItem *item = i.value;
3260 iInt2 visPos = addX_I2(add_I2(pos, item->rect.pos), outlinePadding_DocumentWidget_ * gap_UI);
3261 const iBool isVisible = d->lastVisibleRun && d->lastVisibleRun->text.start >= item->text.start;
3262 const int fg = index_ArrayConstIterator(&i) == 0 || isVisible ? tmOutlineHeadingAbove_ColorId
3263 : tmOutlineHeadingBelow_ColorId;
3264 if (fg == tmOutlineHeadingBelow_ColorId) {
3265 if (wasAbove) {
3266 drawHLine_Paint(&p,
3267 init_I2(left_Rect(outlineFrame), visPos.y - 1),
3268 width_Rect(outlineFrame),
3269 tmOutlineHeadingBelow_ColorId);
3270 wasAbove = iFalse;
3271 }
3272 }
3273 drawWrapRange_Text(
3274 item->font, visPos, innerWidth - left_Rect(item->rect), fg, item->text);
3275 if (left_Rect(item->rect) > 0) {
3276 drawRange_Text(item->font, addX_I2(visPos, -2.75f * gap_UI), fg, range_CStr("\u2022"));
3277 }
3278 }
3279 setOpacity_Text(1.0f);
3280 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
3281 }
3282#endif
3283 unsetClip_Paint(&p); 3143 unsetClip_Paint(&p);
3284} 3144}
3285 3145
3286static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { 3146static void drawMedia_DocumentWidget_(const iDocumentWidget *d, iPaint *p) {
3287 iConstForEach(PtrArray, i, &d->visiblePlayers) { 3147 iConstForEach(PtrArray, i, &d->visibleMedia) {
3288 const iGmRun * run = i.ptr; 3148 const iGmRun * run = i.ptr;
3289 const iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId); 3149 if (run->mediaType == audio_GmRunMediaType) {
3290 const iRect rect = playerRect_DocumentWidget_(d, run); 3150 iPlayerUI ui;
3291 iPlayerUI ui; 3151 init_PlayerUI(&ui,
3292 init_PlayerUI(&ui, plr, rect); 3152 audioPlayer_Media(media_GmDocument(d->doc), run->mediaId),
3293 draw_PlayerUI(&ui, p); 3153 runRect_DocumentWidget_(d, run));
3154 draw_PlayerUI(&ui, p);
3155 }
3156 else if (run->mediaType == download_GmRunMediaType) {
3157 iDownloadUI ui;
3158 init_DownloadUI(&ui, d, run->mediaId, runRect_DocumentWidget_(d, run));
3159 draw_DownloadUI(&ui, p);
3160 }
3294 } 3161 }
3295} 3162}
3296 3163
@@ -3384,7 +3251,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3384 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); 3251 render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx);
3385 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); 3252 SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE);
3386 } 3253 }
3387 drawPlayers_DocumentWidget_(d, &ctx.paint); 3254 drawMedia_DocumentWidget_(d, &ctx.paint);
3388 unsetClip_Paint(&ctx.paint); 3255 unsetClip_Paint(&ctx.paint);
3389 /* Fill the top and bottom, in case the document is short. */ 3256 /* Fill the top and bottom, in case the document is short. */
3390 if (yTop > top_Rect(bounds)) { 3257 if (yTop > top_Rect(bounds)) {
@@ -3409,6 +3276,9 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
3409 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId); 3276 fillRect_Paint(&ctx.paint, linkRect, tmBackground_ColorId);
3410 drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl); 3277 drawRange_Text(font, addX_I2(topLeft_Rect(linkRect), gap_UI), tmParagraph_ColorId, linkUrl);
3411 } 3278 }
3279 if (colorTheme_App() == pureWhite_ColorTheme) {
3280 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId);
3281 }
3412 draw_Widget(w); 3282 draw_Widget(w);
3413} 3283}
3414 3284
@@ -3430,6 +3300,10 @@ const iBlock *sourceContent_DocumentWidget(const iDocumentWidget *d) {
3430 return &d->sourceContent; 3300 return &d->sourceContent;
3431} 3301}
3432 3302
3303int documentWidth_DocumentWidget(const iDocumentWidget *d) {
3304 return documentWidth_DocumentWidget_(d);
3305}
3306
3433const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { 3307const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) {
3434 if (!isEmpty_String(title_GmDocument(d->doc))) { 3308 if (!isEmpty_String(title_GmDocument(d->doc))) {
3435 return title_GmDocument(d->doc); 3309 return title_GmDocument(d->doc);
@@ -3507,7 +3381,6 @@ void updateSize_DocumentWidget(iDocumentWidget *d) {
3507 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse); 3381 updateDocumentWidthRetainingScrollPosition_DocumentWidget_(d, iFalse);
3508 resetWideRuns_DocumentWidget_(d); 3382 resetWideRuns_DocumentWidget_(d);
3509 updateSideIconBuf_DocumentWidget_(d); 3383 updateSideIconBuf_DocumentWidget_(d);
3510 updateOutline_DocumentWidget_(d);
3511 updateVisible_DocumentWidget_(d); 3384 updateVisible_DocumentWidget_(d);
3512 invalidate_DocumentWidget_(d); 3385 invalidate_DocumentWidget_(d);
3513} 3386}
diff --git a/src/ui/mediaui.c b/src/ui/mediaui.c
index 3e22a1d2..2fad0cec 100644
--- a/src/ui/mediaui.c
+++ b/src/ui/mediaui.c
@@ -20,11 +20,16 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ 21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22 22
23#include "playerui.h" 23#include "mediaui.h"
24#include "media.h"
25#include "documentwidget.h"
26#include "gmdocument.h"
24#include "audio/player.h" 27#include "audio/player.h"
25#include "paint.h" 28#include "paint.h"
26#include "util.h" 29#include "util.h"
27 30
31#include <the_Foundation/path.h>
32
28static const char *volumeChar_(float volume) { 33static const char *volumeChar_(float volume) {
29 if (volume <= 0) { 34 if (volume <= 0) {
30 return "\U0001f507"; 35 return "\U0001f507";
@@ -72,8 +77,11 @@ static void drawPlayerButton_(iPaint *p, iRect rect, const char *label, int font
72 drawCentered_Text(font, frameRect, iTrue, fg, "%s", label); 77 drawCentered_Text(font, frameRect, iTrue, fg, "%s", label);
73} 78}
74 79
80static const uint32_t sevenSegmentDigit_ = 0x1fbf0;
81
82static const char *sevenSegmentStr_ = "\U0001fbf0";
83
75static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */ 84static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */
76 const uint32_t sevenSegmentDigit = 0x1fbf0;
77 const int hours = seconds / 3600; 85 const int hours = seconds / 3600;
78 const int mins = (seconds / 60) % 60; 86 const int mins = (seconds / 60) % 60;
79 const int secs = seconds % 60; 87 const int secs = seconds % 60;
@@ -81,14 +89,14 @@ static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) {
81 iString num; 89 iString num;
82 init_String(&num); 90 init_String(&num);
83 if (hours) { 91 if (hours) {
84 appendChar_String(&num, sevenSegmentDigit + (hours % 10)); 92 appendChar_String(&num, sevenSegmentDigit_ + (hours % 10));
85 appendChar_String(&num, ':'); 93 appendChar_String(&num, ':');
86 } 94 }
87 appendChar_String(&num, sevenSegmentDigit + (mins / 10) % 10); 95 appendChar_String(&num, sevenSegmentDigit_ + (mins / 10) % 10);
88 appendChar_String(&num, sevenSegmentDigit + (mins % 10)); 96 appendChar_String(&num, sevenSegmentDigit_ + (mins % 10));
89 appendChar_String(&num, ':'); 97 appendChar_String(&num, ':');
90 appendChar_String(&num, sevenSegmentDigit + (secs / 10) % 10); 98 appendChar_String(&num, sevenSegmentDigit_ + (secs / 10) % 10);
91 appendChar_String(&num, sevenSegmentDigit + (secs % 10)); 99 appendChar_String(&num, sevenSegmentDigit_ + (secs % 10));
92 iInt2 size = advanceRange_Text(font, range_String(&num)); 100 iInt2 size = advanceRange_Text(font, range_String(&num));
93 if (align == right_Alignment) { 101 if (align == right_Alignment) {
94 pos.x -= size.x; 102 pos.x -= size.x;
@@ -134,10 +142,10 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
134 iRound(totalTime)); 142 iRound(totalTime));
135 } 143 }
136 /* Scrubber. */ 144 /* Scrubber. */
137 const int s1 = left_Rect(d->scrubberRect) + leftWidth + 6 * gap_UI; 145 const int s1 = left_Rect(d->scrubberRect) + leftWidth + 6 * gap_UI;
138 const int s2 = right_Rect(d->scrubberRect) - rightWidth - 6 * gap_UI; 146 const int s2 = right_Rect(d->scrubberRect) - rightWidth - 6 * gap_UI;
139 const float normPos = totalTime > 0 ? playTime / totalTime : 0.0f; 147 const float normPos = totalTime > 0 ? playTime / totalTime : 0.0f;
140 const int part = (s2 - s1) * normPos; 148 const int part = (s2 - s1) * normPos;
141 const int scrubMax = (s2 - s1) * streamProgress_Player(d->player); 149 const int scrubMax = (s2 - s1) * streamProgress_Player(d->player);
142 drawHLine_Paint(p, init_I2(s1, yMid), part, bright); 150 drawHLine_Paint(p, init_I2(s1, yMid), part, bright);
143 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim); 151 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim);
@@ -182,3 +190,84 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
182 dot); 190 dot);
183 } 191 }
184} 192}
193
194/*----------------------------------------------------------------------------------------------*/
195
196static void drawSevenSegmentBytes_(iInt2 pos, int color, size_t numBytes) {
197 iString digits;
198 init_String(&digits);
199 if (numBytes == 0) {
200 appendChar_String(&digits, sevenSegmentDigit_);
201 }
202 else {
203 int magnitude = 0;
204 while (numBytes) {
205 if (magnitude == 3) {
206 prependCStr_String(&digits, "\u2024");
207 }
208 else if (magnitude == 6) {
209 prependCStr_String(&digits, restore_ColorEscape "\u2024");
210 }
211 else if (magnitude == 9) {
212 prependCStr_String(&digits, "\u2024");
213 }
214 prependChar_String(&digits, sevenSegmentDigit_ + (numBytes % 10));
215 numBytes /= 10;
216 magnitude++;
217 }
218 if (magnitude > 6) {
219 prependCStr_String(&digits, uiTextStrong_ColorEscape);
220 }
221 }
222 const int font = uiLabel_FontId;
223 const iInt2 dims = advanceRange_Text(font, range_String(&digits));
224 drawRange_Text(font, addX_I2(pos, -dims.x), color, range_String(&digits));
225 deinit_String(&digits);
226}
227
228void init_DownloadUI(iDownloadUI *d, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds) {
229 d->doc = doc;
230 d->mediaId = mediaId;
231 d->bounds = bounds;
232}
233
234iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) {
235 return iFalse;
236}
237
238void draw_DownloadUI(const iDownloadUI *d, iPaint *p) {
239 const iMedia *media = constMedia_GmDocument(document_DocumentWidget(d->doc));
240 iGmMediaInfo info;
241 float bytesPerSecond;
242 const iString *path;
243 iBool isFinished;
244 downloadInfo_Media(media, d->mediaId, &info);
245 downloadStats_Media(media, d->mediaId, &path, &bytesPerSecond, &isFinished);
246 fillRect_Paint(p, d->bounds, uiBackground_ColorId);
247 drawRect_Paint(p, d->bounds, uiSeparator_ColorId);
248 iRect rect = d->bounds;
249 shrink_Rect(&rect, init_I2(3 * gap_UI, 0));
250 const int fonts[2] = { uiContentBold_FontId, uiLabel_FontId };
251 const int contentHeight = lineHeight_Text(fonts[0]) + lineHeight_Text(fonts[1]);
252 const int x = left_Rect(rect);
253 const int y1 = mid_Rect(rect).y - contentHeight / 2;
254 const int y2 = y1 + lineHeight_Text(fonts[1]);
255 if (path) {
256 drawRange_Text(fonts[0], init_I2(x, y1), uiHeading_ColorId, baseName_Path(path));
257 }
258 draw_Text(uiLabel_FontId,
259 init_I2(x, y2),
260 isFinished ? uiTextAction_ColorId : uiTextDim_ColorId,
261 isFinished ? "Download completed."
262 : "Download will be cancelled if this tab is closed.");
263 const int x2 = right_Rect(rect);
264 drawSevenSegmentBytes_(init_I2(x2, y1), uiTextDim_ColorId, info.numBytes);
265 const iInt2 pos = init_I2(x2, y2);
266 if (bytesPerSecond > 0) {
267 drawAlign_Text(uiLabel_FontId, pos, uiTextDim_ColorId, right_Alignment, "%.3f MB/s",
268 bytesPerSecond / 1.0e6);
269 }
270 else {
271 drawAlign_Text(uiLabel_FontId, pos, uiTextDim_ColorId, right_Alignment, "\u2014 MB/s");
272 }
273}
diff --git a/src/ui/mediaui.h b/src/ui/mediaui.h
index a1f4ca9b..e79dedc0 100644
--- a/src/ui/mediaui.h
+++ b/src/ui/mediaui.h
@@ -23,6 +23,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
23#pragma once 23#pragma once
24 24
25#include <the_Foundation/rect.h> 25#include <the_Foundation/rect.h>
26#include <SDL_events.h>
26 27
27iDeclareType(Paint) 28iDeclareType(Paint)
28iDeclareType(Player) 29iDeclareType(Player)
@@ -42,3 +43,19 @@ struct Impl_PlayerUI {
42 43
43void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds); 44void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds);
44void draw_PlayerUI (iPlayerUI *, iPaint *p); 45void draw_PlayerUI (iPlayerUI *, iPaint *p);
46
47/*----------------------------------------------------------------------------------------------*/
48
49iDeclareType(DocumentWidget)
50iDeclareType(Media)
51iDeclareType(DownloadUI)
52
53struct Impl_DownloadUI {
54 const iDocumentWidget *doc;
55 uint16_t mediaId;
56 iRect bounds;
57};
58
59void init_DownloadUI (iDownloadUI *, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds);
60iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev);
61void draw_DownloadUI (const iDownloadUI *, iPaint *p);