diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-02-26 10:24:09 +0200 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-02-26 10:24:09 +0200 |
commit | 91a6225d8508db01574d7da2c013cb30d6a87ec8 (patch) | |
tree | e3bd2c2f24a22c694c1c23aefd5fc531ae108723 | |
parent | 4708a6580e9af65cd15769e87487fdf4456f1e00 (diff) |
DocumentWidget: Inline downloads
-rw-r--r-- | CMakeLists.txt | 6 | ||||
-rw-r--r-- | src/app.c | 55 | ||||
-rw-r--r-- | src/app.h | 1 | ||||
-rw-r--r-- | src/gmdocument.c | 43 | ||||
-rw-r--r-- | src/media.c | 93 | ||||
-rw-r--r-- | src/media.h | 9 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 397 | ||||
-rw-r--r-- | src/ui/mediaui.c | 111 | ||||
-rw-r--r-- | src/ui/mediaui.h | 17 |
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 | ) |
176 | if (IOS) | 176 | if (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 |
@@ -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 | ||
584 | const 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 | |||
584 | const iString *debugInfo_App(void) { | 639 | const 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_; |
@@ -85,6 +85,7 @@ enum iColorTheme colorTheme_App (void); | |||
85 | const iString * schemeProxy_App (iRangecc scheme); | 85 | const iString * schemeProxy_App (iRangecc scheme); |
86 | iBool willUseProxy_App (const iRangecc scheme); | 86 | iBool willUseProxy_App (const iRangecc scheme); |
87 | const iString * searchQueryUrl_App (const iString *queryStringUnescaped); | 87 | const iString * searchQueryUrl_App (const iString *queryStringUnescaped); |
88 | const iString * downloadPathForUrl_App(const iString *url, const iString *mime); | ||
88 | 89 | ||
89 | typedef void (*iTickerFunc)(iAny *); | 90 | typedef 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 | ||
258 | static 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 | |||
258 | static void doLayout_GmDocument_(iGmDocument *d) { | 267 | static 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 | ||
35 | iDeclareType(GmMediaProps) | 37 | iDeclareType(GmMediaProps) |
36 | 38 | ||
@@ -132,15 +134,63 @@ iDeclareType(GmDownload) | |||
132 | 134 | ||
133 | struct Impl_GmDownload { | 135 | struct 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 | ||
146 | static 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 | |||
156 | static void closeFile_GmDownload_(iGmDownload *d) { | ||
157 | d->currentRate = (float) (d->numBytes / elapsedSeconds_Time(&d->startTime)); | ||
158 | iReleasePtr(&d->file); | ||
159 | } | ||
160 | |||
138 | void init_GmDownload(iGmDownload *d) { | 161 | void 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 | ||
142 | void deinit_GmDownload(iGmDownload *d) { | 172 | void 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 | |||
178 | static 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 | ||
146 | iDefineTypeConstruction(GmDownload) | 196 | iDefineTypeConstruction(GmDownload) |
@@ -183,7 +233,7 @@ void clear_Media(iMedia *d) { | |||
183 | clear_PtrArray(&d->downloads); | 233 | clear_PtrArray(&d->downloads); |
184 | } | 234 | } |
185 | 235 | ||
186 | iBool setUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) { | 236 | iBool 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 | ||
439 | iBool 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 | |||
451 | void 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 | ||
383 | static void updated_MediaRequest_(iAnyObject *obj) { | 468 | static 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 | ||
49 | void clear_Media (iMedia *); | 49 | void clear_Media (iMedia *); |
50 | iBool setUrl_Media (iMedia *, uint16_t linkId, const iString *url); | 50 | iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url); |
51 | iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); | 51 | iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags); |
52 | 52 | ||
53 | iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); | 53 | iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId); |
54 | iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); | 54 | iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out); |
@@ -61,6 +61,9 @@ iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaI | |||
61 | iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); | 61 | iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId); |
62 | 62 | ||
63 | iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId); | 63 | iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId); |
64 | iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out); | ||
65 | void 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 | ||
141 | iDeclareType(OutlineItem) | 141 | static void animateMedia_DocumentWidget_ (iDocumentWidget *d); |
142 | |||
143 | struct Impl_OutlineItem { | ||
144 | iRangecc text; | ||
145 | int font; | ||
146 | iRect rect; | ||
147 | }; | ||
148 | |||
149 | /*----------------------------------------------------------------------------------------------*/ | ||
150 | |||
151 | static void animatePlayers_DocumentWidget_ (iDocumentWidget *d); | ||
152 | static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d); | 142 | static void updateSideIconBuf_DocumentWidget_ (iDocumentWidget *d); |
153 | 143 | ||
154 | static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */ | 144 | static 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 | ||
535 | static void animate_DocumentWidget_(void *ticker) { | 520 | static 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 | ||
552 | static void updateOutlineOpacity_DocumentWidget_(iDocumentWidget *d) { | 537 | static 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 | |||
565 | static 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 | ||
581 | static uint32_t postPlayerUpdate_DocumentWidget_(uint32_t interval, void *context) { | 559 | static 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 | ||
588 | static void updatePlayers_DocumentWidget_(iDocumentWidget *d) { | 566 | static 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 | ||
606 | static void animatePlayers_DocumentWidget_(iDocumentWidget *d) { | 586 | static 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 | ||
759 | static 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 | |||
772 | static iRangecc bannerText_DocumentWidget_(const iDocumentWidget *d) { | 739 | static 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 | ||
777 | static 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 | |||
808 | static void documentRunsInvalidated_DocumentWidget_(iDocumentWidget *d) { | 744 | static 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 | ||
1378 | static iBool requestMedia_DocumentWidget_(iDocumentWidget *d, iGmLinkId linkId) { | 1311 | static 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 | ||
1321 | static iBool isDownloadRequest_DocumentWidget(const iDocumentWidget *d, const iMediaRequest *req) { | ||
1322 | return findLinkDownload_Media(constMedia_GmDocument(d->doc), req->linkId) != 0; | ||
1323 | } | ||
1324 | |||
1388 | static iBool handleMediaCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) { | 1325 | static 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 | ||
1483 | static void saveToDownloads_(const iString *url, const iString *mime, const iBlock *content) { | 1422 | static 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 | ||
1555 | static void addAllLinks_(void *context, const iGmRun *run) { | 1443 | static 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 | ||
2173 | static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { | 2070 | static 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 | ||
2199 | static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { | 2096 | static 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 | ||
3286 | static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { | 3146 | static 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 | ||
3303 | int documentWidth_DocumentWidget(const iDocumentWidget *d) { | ||
3304 | return documentWidth_DocumentWidget_(d); | ||
3305 | } | ||
3306 | |||
3433 | const iString *feedTitle_DocumentWidget(const iDocumentWidget *d) { | 3307 | const 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 |
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | 21 | SOFTWARE, 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 | |||
28 | static const char *volumeChar_(float volume) { | 33 | static 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 | ||
80 | static const uint32_t sevenSegmentDigit_ = 0x1fbf0; | ||
81 | |||
82 | static const char *sevenSegmentStr_ = "\U0001fbf0"; | ||
83 | |||
75 | static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */ | 84 | static 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 | |||
196 | static 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 | |||
228 | void 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 | |||
234 | iBool processEvent_DownloadUI(iDownloadUI *d, const SDL_Event *ev) { | ||
235 | return iFalse; | ||
236 | } | ||
237 | |||
238 | void 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 | ||
27 | iDeclareType(Paint) | 28 | iDeclareType(Paint) |
28 | iDeclareType(Player) | 29 | iDeclareType(Player) |
@@ -42,3 +43,19 @@ struct Impl_PlayerUI { | |||
42 | 43 | ||
43 | void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds); | 44 | void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds); |
44 | void draw_PlayerUI (iPlayerUI *, iPaint *p); | 45 | void draw_PlayerUI (iPlayerUI *, iPaint *p); |
46 | |||
47 | /*----------------------------------------------------------------------------------------------*/ | ||
48 | |||
49 | iDeclareType(DocumentWidget) | ||
50 | iDeclareType(Media) | ||
51 | iDeclareType(DownloadUI) | ||
52 | |||
53 | struct Impl_DownloadUI { | ||
54 | const iDocumentWidget *doc; | ||
55 | uint16_t mediaId; | ||
56 | iRect bounds; | ||
57 | }; | ||
58 | |||
59 | void init_DownloadUI (iDownloadUI *, const iDocumentWidget *doc, uint16_t mediaId, iRect bounds); | ||
60 | iBool processEvent_DownloadUI (iDownloadUI *, const SDL_Event *ev); | ||
61 | void draw_DownloadUI (const iDownloadUI *, iPaint *p); | ||