summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-04-24 15:37:48 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-04-24 15:37:48 +0300
commit15a10da4e82c688b0cb1e266f4aebd477c979dce (patch)
treee6fef2fc87be5e03a26f554063b52695d4db2aba
parent90717c77b29c4a8ca0c3f49e9b4670b6fcbbe9d9 (diff)
Gempub cover page; cleanup
Use MIME hooks to generate a Gempub cover page with a preloaded cover image. This required applying MIME filtering to "file://" requests as well. Todo: More cleanup, add a gempub.c.
-rw-r--r--src/gmrequest.c104
-rw-r--r--src/gmutil.c71
-rw-r--r--src/gmutil.h4
-rw-r--r--src/mimehooks.c109
-rw-r--r--src/ui/documentwidget.c68
5 files changed, 281 insertions, 75 deletions
diff --git a/src/gmrequest.c b/src/gmrequest.c
index 4a9cc763..8b87a638 100644
--- a/src/gmrequest.c
+++ b/src/gmrequest.c
@@ -148,7 +148,7 @@ iDefineAudienceGetter(GmRequest, updated)
148iDefineAudienceGetter(GmRequest, finished) 148iDefineAudienceGetter(GmRequest, finished)
149 149
150static void checkServerCertificate_GmRequest_(iGmRequest *d) { 150static void checkServerCertificate_GmRequest_(iGmRequest *d) {
151 const iTlsCertificate *cert = serverCertificate_TlsRequest(d->req); 151 const iTlsCertificate *cert = d->req ? serverCertificate_TlsRequest(d->req) : NULL;
152 iGmResponse *resp = d->resp; 152 iGmResponse *resp = d->resp;
153 resp->certFlags = 0; 153 resp->certFlags = 0;
154 if (cert) { 154 if (cert) {
@@ -256,6 +256,20 @@ static void readIncoming_GmRequest_(iGmRequest *d, iTlsRequest *req) {
256 } 256 }
257} 257}
258 258
259static void applyFilter_GmRequest_(iGmRequest *d) {
260 iAssert(d->state == finished_GmRequestState);
261 iBlock *xbody = tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body, &d->url);
262 if (xbody) {
263 lock_Mutex(d->mtx);
264 clear_String(&d->resp->meta);
265 clear_Block(&d->resp->body);
266 d->state = receivingHeader_GmRequestState;
267 processIncomingData_GmRequest_(d, xbody);
268 d->state = finished_GmRequestState;
269 unlock_Mutex(d->mtx);
270 }
271}
272
259static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) { 273static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) {
260 iAssert(req == d->req); 274 iAssert(req == d->req);
261 lock_Mutex(d->mtx); 275 lock_Mutex(d->mtx);
@@ -275,17 +289,7 @@ static void requestFinished_GmRequest_(iGmRequest *d, iTlsRequest *req) {
275 unlock_Mutex(d->mtx); 289 unlock_Mutex(d->mtx);
276 /* Check for mimehooks. */ 290 /* Check for mimehooks. */
277 if (d->isRespFiltered && d->state == finished_GmRequestState) { 291 if (d->isRespFiltered && d->state == finished_GmRequestState) {
278 iBlock *xbody = 292 applyFilter_GmRequest_(d);
279 tryFilter_MimeHooks(mimeHooks_App(), &d->resp->meta, &d->resp->body, &d->url);
280 if (xbody) {
281 lock_Mutex(d->mtx);
282 clear_String(&d->resp->meta);
283 clear_Block(&d->resp->body);
284 d->state = receivingHeader_GmRequestState;
285 processIncomingData_GmRequest_(d, xbody);
286 d->state = finished_GmRequestState;
287 unlock_Mutex(d->mtx);
288 }
289 } 293 }
290 iNotifyAudience(d, finished, GmRequestFinished); 294 iNotifyAudience(d, finished, GmRequestFinished);
291} 295}
@@ -531,7 +535,8 @@ static const iString *findContainerArchive_(const iString *path) {
531 iBeginCollect(); 535 iBeginCollect();
532 while (!isEmpty_String(path) && cmp_String(path, ".")) { 536 while (!isEmpty_String(path) && cmp_String(path, ".")) {
533 iString *dir = newRange_String(dirName_Path(path)); 537 iString *dir = newRange_String(dirName_Path(path));
534 if (endsWithCase_String(dir, ".zip")) { 538 if (endsWithCase_String(dir, ".zip") ||
539 endsWithCase_String(dir, ".gpub")) {
535 iEndCollect(); 540 iEndCollect();
536 return collect_String(dir); 541 return collect_String(dir);
537 } 542 }
@@ -541,49 +546,6 @@ static const iString *findContainerArchive_(const iString *path) {
541 return NULL; 546 return NULL;
542} 547}
543 548
544static const char *mediaTypeFromPath_(const iString *path) {
545 if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) {
546 return "text/gemini; charset=utf-8";
547 }
548 /* TODO: It would be better to default to text/plain, but switch to
549 application/octet-stream if the contents fail to parse as UTF-8. */
550 else if (endsWithCase_String(path, ".txt") ||
551 endsWithCase_String(path, ".md") ||
552 endsWithCase_String(path, ".c") ||
553 endsWithCase_String(path, ".h") ||
554 endsWithCase_String(path, ".cc") ||
555 endsWithCase_String(path, ".hh") ||
556 endsWithCase_String(path, ".cpp") ||
557 endsWithCase_String(path, ".hpp")) {
558 return "text/plain";
559 }
560 else if (endsWithCase_String(path, ".zip")) {
561 return "application/zip";
562 }
563 else if (endsWithCase_String(path, ".png")) {
564 return "image/png";
565 }
566 else if (endsWithCase_String(path, ".jpg") || endsWithCase_String(path, ".jpeg")) {
567 return "image/jpeg";
568 }
569 else if (endsWithCase_String(path, ".gif")) {
570 return "image/gif";
571 }
572 else if (endsWithCase_String(path, ".wav")) {
573 return "audio/wave";
574 }
575 else if (endsWithCase_String(path, ".ogg")) {
576 return "audio/ogg";
577 }
578 else if (endsWithCase_String(path, ".mp3")) {
579 return "audio/mpeg";
580 }
581 else if (endsWithCase_String(path, ".mid")) {
582 return "audio/midi";
583 }
584 return "application/octet-stream";
585}
586
587static iBool isDirectory_(const iString *path) { 549static iBool isDirectory_(const iString *path) {
588 /* TODO: move this to the_Foundation */ 550 /* TODO: move this to the_Foundation */
589 iFileInfo *info = new_FileInfo(path); 551 iFileInfo *info = new_FileInfo(path);
@@ -644,14 +606,8 @@ void submit_GmRequest(iGmRequest *d) {
644 return; 606 return;
645 } 607 }
646 else if (equalCase_Rangecc(url.scheme, "file")) { 608 else if (equalCase_Rangecc(url.scheme, "file")) {
647 /* TODO: Move this elsewhere. */ 609 /* TODO: Move handling of "file://" URLs elsewhere, it's getting complex. */
648 iString *path = collect_String(urlDecode_String(collect_String(newRange_String(url.path)))); 610 iString *path = collect_String(localFilePathFromUrl_String(&d->url));
649#if defined (iPlatformMsys)
650 /* Remove the extra slash from the beginning. */
651 if (startsWith_String(path, "/")) {
652 remove_Block(&path->chars, 0, 1);
653 }
654#endif
655 iFile *f = new_File(path); 611 iFile *f = new_File(path);
656 if (isDirectory_(path)) { 612 if (isDirectory_(path)) {
657 if (endsWith_String(path, "/")) { 613 if (endsWith_String(path, "/")) {
@@ -669,6 +625,14 @@ void submit_GmRequest(iGmRequest *d) {
669 iPtrArray *sortedInfo = collectNew_PtrArray(); 625 iPtrArray *sortedInfo = collectNew_PtrArray();
670 iForEach(DirFileInfo, entry, 626 iForEach(DirFileInfo, entry,
671 iClob(directoryContents_FileInfo(iClob(new_FileInfo(path))))) { 627 iClob(directoryContents_FileInfo(iClob(new_FileInfo(path))))) {
628 /* Ignore some files. */
629#if defined (iPlatformApple)
630 const iRangecc name = baseName_Path(path_FileInfo(entry.value));
631 if (equal_Rangecc(name, ".DS_Store") ||
632 equal_Rangecc(name, ".localized")) {
633 continue;
634 }
635#endif
672 pushBack_PtrArray(sortedInfo, ref_Object(entry.value)); 636 pushBack_PtrArray(sortedInfo, ref_Object(entry.value));
673 } 637 }
674 sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_); 638 sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_);
@@ -684,7 +648,7 @@ void submit_GmRequest(iGmRequest *d) {
684 } 648 }
685 else if (open_File(f, readOnly_FileMode)) { 649 else if (open_File(f, readOnly_FileMode)) {
686 resp->statusCode = success_GmStatusCode; 650 resp->statusCode = success_GmStatusCode;
687 setCStr_String(&resp->meta, mediaTypeFromPath_(path)); 651 setCStr_String(&resp->meta, mediaTypeFromPath_String(path));
688 /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */ 652 /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */
689 set_Block(&resp->body, collect_Block(readAll_File(f))); 653 set_Block(&resp->body, collect_Block(readAll_File(f)));
690 d->state = receivingBody_GmRequestState; 654 d->state = receivingBody_GmRequestState;
@@ -718,6 +682,7 @@ void submit_GmRequest(iGmRequest *d) {
718 } 682 }
719 } 683 }
720 /* Show an archive index page if this is a directory. */ 684 /* Show an archive index page if this is a directory. */
685 /* TODO: Use a built-in MIME hook for this? */
721 if (isDir) { 686 if (isDir) {
722 iString *page = new_String(); 687 iString *page = new_String();
723 const iRangecc containerName = baseName_Path(container); 688 const iRangecc containerName = baseName_Path(container);
@@ -779,7 +744,7 @@ void submit_GmRequest(iGmRequest *d) {
779 const iBlock *data = data_Archive(arch, entryPath); 744 const iBlock *data = data_Archive(arch, entryPath);
780 if (data) { 745 if (data) {
781 resp->statusCode = success_GmStatusCode; 746 resp->statusCode = success_GmStatusCode;
782 setCStr_String(&resp->meta, mediaTypeFromPath_(entryPath)); 747 setCStr_String(&resp->meta, mediaTypeFromPath_String(entryPath));
783 set_Block(&resp->body, data); 748 set_Block(&resp->body, data);
784 } 749 }
785 else { 750 else {
@@ -787,6 +752,7 @@ void submit_GmRequest(iGmRequest *d) {
787 setCStr_String(&resp->meta, cstr_String(path)); 752 setCStr_String(&resp->meta, cstr_String(path));
788 } 753 }
789 } 754 }
755 fileRequestFinished:;
790 } 756 }
791 } 757 }
792 else { 758 else {
@@ -794,9 +760,13 @@ void submit_GmRequest(iGmRequest *d) {
794 setCStr_String(&resp->meta, cstr_String(path)); 760 setCStr_String(&resp->meta, cstr_String(path));
795 } 761 }
796 } 762 }
797fileRequestFinished:
798 iRelease(f); 763 iRelease(f);
799 d->state = finished_GmRequestState; 764 d->state = finished_GmRequestState;
765 /* MIME hooks may to this content. */
766 if (d->isFilterEnabled && resp->statusCode == success_GmStatusCode) {
767 /* TODO: Use a background thread, the hook may take some time to run. */
768 applyFilter_GmRequest_(d);
769 }
800 iNotifyAudience(d, finished, GmRequestFinished); 770 iNotifyAudience(d, finished, GmRequestFinished);
801 return; 771 return;
802 } 772 }
diff --git a/src/gmutil.c b/src/gmutil.c
index 18130951..806c989c 100644
--- a/src/gmutil.c
+++ b/src/gmutil.c
@@ -422,6 +422,71 @@ const char *makeFileUrl_CStr(const char *localFilePath) {
422 return cstrCollect_String(makeFileUrl_String(collectNewCStr_String(localFilePath))); 422 return cstrCollect_String(makeFileUrl_String(collectNewCStr_String(localFilePath)));
423} 423}
424 424
425iString *localFilePathFromUrl_String(const iString *d) {
426 iUrl url;
427 init_Url(&url, d);
428 if (!equalCase_Rangecc(url.scheme, "file")) {
429 return NULL;
430 }
431 iString *path = urlDecode_String(collect_String(newRange_String(url.path)));
432#if defined (iPlatformMsys)
433 /* Remove the extra slash from the beginning. */
434 if (startsWith_String(path, "/")) {
435 remove_Block(&path->chars, 0, 1);
436 }
437#endif
438 return path;
439}
440
441const char *mediaTypeFromPath_String(const iString *path) {
442 if (endsWithCase_String(path, ".gmi") || endsWithCase_String(path, ".gemini")) {
443 return "text/gemini; charset=utf-8";
444 }
445 /* TODO: It would be better to default to text/plain, but switch to
446 application/octet-stream if the contents fail to parse as UTF-8. */
447 else if (endsWithCase_String(path, ".txt") ||
448 endsWithCase_String(path, ".md") ||
449 endsWithCase_String(path, ".c") ||
450 endsWithCase_String(path, ".h") ||
451 endsWithCase_String(path, ".cc") ||
452 endsWithCase_String(path, ".hh") ||
453 endsWithCase_String(path, ".cpp") ||
454 endsWithCase_String(path, ".hpp")) {
455 return "text/plain";
456 }
457 else if (endsWithCase_String(path, ".zip")) {
458 return "application/zip";
459 }
460 else if (endsWithCase_String(path, ".gpub")) {
461 return "application/gpub+zip";
462 }
463 else if (endsWithCase_String(path, ".xml")) {
464 return "text/xml";
465 }
466 else if (endsWithCase_String(path, ".png")) {
467 return "image/png";
468 }
469 else if (endsWithCase_String(path, ".jpg") || endsWithCase_String(path, ".jpeg")) {
470 return "image/jpeg";
471 }
472 else if (endsWithCase_String(path, ".gif")) {
473 return "image/gif";
474 }
475 else if (endsWithCase_String(path, ".wav")) {
476 return "audio/wave";
477 }
478 else if (endsWithCase_String(path, ".ogg")) {
479 return "audio/ogg";
480 }
481 else if (endsWithCase_String(path, ".mp3")) {
482 return "audio/mpeg";
483 }
484 else if (endsWithCase_String(path, ".mid")) {
485 return "audio/midi";
486 }
487 return "application/octet-stream";
488}
489
425void urlEncodeSpaces_String(iString *d) { 490void urlEncodeSpaces_String(iString *d) {
426 for (;;) { 491 for (;;) {
427 const size_t pos = indexOfCStr_String(d, " "); 492 const size_t pos = indexOfCStr_String(d, " ");
@@ -440,6 +505,12 @@ const iString *withSpacesEncoded_String(const iString *d) {
440 return collect_String(enc); 505 return collect_String(enc);
441} 506}
442 507
508iRangecc mediaTypeWithoutParameters_Rangecc(iRangecc mime) {
509 iRangecc part = iNullRange;
510 nextSplit_Rangecc(mime, ";", &part);
511 return part;
512}
513
443const iString *feedEntryOpenCommand_String(const iString *url, int newTab) { 514const iString *feedEntryOpenCommand_String(const iString *url, int newTab) {
444 if (!isEmpty_String(url)) { 515 if (!isEmpty_String(url)) {
445 iString *cmd = collectNew_String(); 516 iString *cmd = collectNew_String();
diff --git a/src/gmutil.h b/src/gmutil.h
index 64c015b8..7aedbc47 100644
--- a/src/gmutil.h
+++ b/src/gmutil.h
@@ -115,7 +115,11 @@ void urlDecodePath_String (iString *);
115void urlEncodePath_String (iString *); 115void urlEncodePath_String (iString *);
116iString * makeFileUrl_String (const iString *localFilePath); 116iString * makeFileUrl_String (const iString *localFilePath);
117const char * makeFileUrl_CStr (const char *localFilePath); 117const char * makeFileUrl_CStr (const char *localFilePath);
118iString * localFilePathFromUrl_String(const iString *);
118void urlEncodeSpaces_String (iString *); 119void urlEncodeSpaces_String (iString *);
119const iString * withSpacesEncoded_String(const iString *); 120const iString * withSpacesEncoded_String(const iString *);
120 121
122const char * mediaTypeFromPath_String (const iString *path);
123iRangecc mediaTypeWithoutParameters_Rangecc (iRangecc mime);
124
121const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */ 125const iString * feedEntryOpenCommand_String (const iString *url, int newTab); /* checks fragment */
diff --git a/src/mimehooks.c b/src/mimehooks.c
index 3b3c0d1a..04cbcb36 100644
--- a/src/mimehooks.c
+++ b/src/mimehooks.c
@@ -1,6 +1,9 @@
1#include "mimehooks.h" 1#include "mimehooks.h"
2#include "defs.h"
3#include "gmutil.h"
2#include "app.h" 4#include "app.h"
3 5
6#include <the_Foundation/archive.h>
4#include <the_Foundation/file.h> 7#include <the_Foundation/file.h>
5#include <the_Foundation/fileinfo.h> 8#include <the_Foundation/fileinfo.h>
6#include <the_Foundation/path.h> 9#include <the_Foundation/path.h>
@@ -109,7 +112,8 @@ static iBlock *translateAtomXmlToGeminiFeed_(const iString *mime, const iBlock *
109 init_String(&out); 112 init_String(&out);
110 format_String(&out, 113 format_String(&out,
111 "20 text/gemini\r\n" 114 "20 text/gemini\r\n"
112 "# %s\n\n", cstr_String(title)); 115 "# %s\n\n",
116 cstr_String(title));
113 if (!isEmpty_String(subtitle)) { 117 if (!isEmpty_String(subtitle)) {
114 appendFormat_String(&out, "## %s\n\n", cstr_String(subtitle)); 118 appendFormat_String(&out, "## %s\n\n", cstr_String(subtitle));
115 } 119 }
@@ -177,8 +181,97 @@ finished:
177 return output; 181 return output;
178} 182}
179 183
184static void appendGemPubProperty_(iString *out, const char *key, const iString *value) {
185 if (!isEmpty_String(value)) {
186 appendFormat_String(out, "%s: %s\n", key, cstr_String(value));
187 }
188}
189
190iBlock *translateGemPubCoverPage_(const iString *mime, const iBlock *source,
191 const iString *requestUrl) {
192 iBlock *output = NULL;
193 iArchive *arch = new_Archive();
194 if (openData_Archive(arch, source)) {
195 /* Parse the metadata and check if the required contents are present. */
196 const iBlock *metadata = dataCStr_Archive(arch, "metadata");
197 if (!metadata) {
198 goto cleanup;
199 }
200 enum iGemPubProperty {
201 title_GemPubProperty,
202 author_GemPubProperty,
203 lang_GemPubProperty,
204 description_GemPubProperty,
205 pubDate_GemPubProperty,
206 revDate_GemPubProperty,
207 version_GemPubProperty,
208 cover_GemPubProperty,
209 max_GemPubProperty
210 };
211 static const char *labels[max_GemPubProperty] = {
212 "title:",
213 "author:",
214 "language:",
215 "description:",
216 "publishDate:",
217 "revisionDate:",
218 "version:",
219 "cover:"
220 };
221 iString *props[max_GemPubProperty];
222 iForIndices(i, props) {
223 props[i] = collectNew_String();
224 }
225 /* Default values. */
226 setCStr_String(props[title_GemPubProperty], "Untitled Book");
227 setCStr_String(props[cover_GemPubProperty],
228 entryCStr_Archive(arch, "cover.jpg") ? "cover.jpg" :
229 entryCStr_Archive(arch, "cover.png") ? "cover.png" : "");
230 iRangecc line = iNullRange;
231 while (nextSplit_Rangecc(range_Block(metadata), "\n", &line)) {
232 iRangecc clean = line;
233 trim_Rangecc(&clean);
234 iForIndices(i, props) {
235 if (startsWithCase_Rangecc(clean, labels[i])) {
236 setRange_String(props[i], (iRangecc){ clean.start + strlen(labels[i]), clean.end });
237 trim_String(props[i]);
238 }
239 }
240 }
241 const iString *baseUrl = withSpacesEncoded_String(requestUrl);
242 iString *out = new_String();
243 format_String(out, "20 text/gemini; charset=utf-8\r\n"
244 "# %s\n",
245 cstr_String(props[title_GemPubProperty]));
246 if (!isEmpty_String(props[description_GemPubProperty])) {
247 appendFormat_String(out, "%s\n", cstr_String(props[description_GemPubProperty]));
248 }
249 appendCStr_String(out, "\n");
250 appendGemPubProperty_(out, "Author", props[author_GemPubProperty]);
251 appendGemPubProperty_(out, "Version", props[version_GemPubProperty]);
252 appendFormat_String(out, "\n=> %s/capsule/ " book_Icon " Book index page\n", cstr_String(baseUrl));
253 if (!isEmpty_String(props[cover_GemPubProperty])) {
254 appendFormat_String(out, "\n=> %s/%s Cover image\n",
255 cstr_String(baseUrl),
256 cstr_String(props[cover_GemPubProperty]));
257 }
258 appendCStr_String(out, "\n## About this book\n");
259 appendGemPubProperty_(out, "Revision date", props[revDate_GemPubProperty]);
260 appendGemPubProperty_(out, "Publish date", props[pubDate_GemPubProperty]);
261 appendGemPubProperty_(out, "Language", props[lang_GemPubProperty]);
262 output = copy_Block(utf8_String(out));
263 delete_String(out);
264 }
265cleanup:
266 iRelease(arch);
267 return output;
268}
269
180/*----------------------------------------------------------------------------------------------*/ 270/*----------------------------------------------------------------------------------------------*/
181 271
272static const char *gpubMimeType_MimeHooks_ = "application/gpub+zip";
273static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt";
274
182struct Impl_MimeHooks { 275struct Impl_MimeHooks {
183 iPtrArray filters; 276 iPtrArray filters;
184}; 277};
@@ -196,6 +289,12 @@ void deinit_MimeHooks(iMimeHooks *d) {
196 deinit_PtrArray(&d->filters); 289 deinit_PtrArray(&d->filters);
197} 290}
198 291
292static iBool checkGemPub_(const iString *mime, const iString *requestUrl) {
293 /* Only process GemPub in local files. */
294 return (equalCase_Rangecc(urlScheme_String(requestUrl), "file") &&
295 startsWithCase_String(mime, gpubMimeType_MimeHooks_));
296}
297
199iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) { 298iBool willTryFilter_MimeHooks(const iMimeHooks *d, const iString *mime) {
200 /* TODO: Combine this function with tryFilter_MimeHooks! */ 299 /* TODO: Combine this function with tryFilter_MimeHooks! */
201 iRegExpMatch m; 300 iRegExpMatch m;
@@ -228,6 +327,12 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo
228 } 327 }
229 } 328 }
230 /* Built-in filters. */ 329 /* Built-in filters. */
330 if (checkGemPub_(mime, requestUrl)) {
331 iBlock *result = translateGemPubCoverPage_(mime, body, requestUrl);
332 if (result) {
333 return result;
334 }
335 }
231 init_RegExpMatch(&m); 336 init_RegExpMatch(&m);
232 if (matchString_RegExp(xmlMimePattern_(), mime, &m)) { 337 if (matchString_RegExp(xmlMimePattern_(), mime, &m)) {
233 iBlock *result = translateAtomXmlToGeminiFeed_(mime, body, requestUrl); 338 iBlock *result = translateAtomXmlToGeminiFeed_(mime, body, requestUrl);
@@ -238,8 +343,6 @@ iBlock *tryFilter_MimeHooks(const iMimeHooks *d, const iString *mime, const iBlo
238 return NULL; 343 return NULL;
239} 344}
240 345
241static const char *mimeHooksFilename_MimeHooks_ = "mimehooks.txt";
242
243void load_MimeHooks(iMimeHooks *d, const char *saveDir) { 346void load_MimeHooks(iMimeHooks *d, const char *saveDir) {
244 iBool reportError = iFalse; 347 iBool reportError = iFalse;
245 iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_)); 348 iFile *f = newCStr_File(concatPath_CStr(saveDir, mimeHooksFilename_MimeHooks_));
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 9b414b63..11baf9ee 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -54,6 +54,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
54# include "ios.h" 54# include "ios.h"
55#endif 55#endif
56 56
57#include <the_Foundation/archive.h>
57#include <the_Foundation/file.h> 58#include <the_Foundation/file.h>
58#include <the_Foundation/fileinfo.h> 59#include <the_Foundation/fileinfo.h>
59#include <the_Foundation/objectlist.h> 60#include <the_Foundation/objectlist.h>
@@ -1038,6 +1039,22 @@ static void updateFetchProgress_DocumentWidget_(iDocumentWidget *d) {
1038 } 1039 }
1039} 1040}
1040 1041
1042static const char *zipPageHeading_(const iRangecc mime) {
1043 if (equalCase_Rangecc(mime, "application/gpub+zip")) {
1044 return book_Icon " Gempub Book";
1045 }
1046 iRangecc type = iNullRange;
1047 nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */
1048 nextSplit_Rangecc(mime, "/", &type);
1049 if (startsWithCase_Rangecc(type, "x-")) {
1050 type.start += 2;
1051 }
1052 iString *heading = upper_String(collectNewRange_String(type));
1053 appendCStr_String(heading, " Archive");
1054 prependCStr_String(heading, folder_Icon " ");
1055 return cstrCollect_String(heading);
1056}
1057
1041static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response, 1058static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response,
1042 const iBool isInitialUpdate) { 1059 const iBool isInitialUpdate) {
1043 if (d->state == ready_RequestState) { 1060 if (d->state == ready_RequestState) {
@@ -1078,13 +1095,16 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1078 docFormat = plainText_GmDocumentFormat; 1095 docFormat = plainText_GmDocumentFormat;
1079 setRange_String(&d->sourceMime, param); 1096 setRange_String(&d->sourceMime, param);
1080 } 1097 }
1081 else if (equal_Rangecc(param, "application/zip")) { 1098 else if (equal_Rangecc(param, "application/zip") ||
1099 (startsWith_Rangecc(param, "application/") &&
1100 endsWithCase_Rangecc(param, "+zip"))) {
1082 docFormat = gemini_GmDocumentFormat; 1101 docFormat = gemini_GmDocumentFormat;
1083 setRange_String(&d->sourceMime, param); 1102 setRange_String(&d->sourceMime, param);
1084 iString *key = collectNew_String(); 1103 iString *key = collectNew_String();
1085 toString_Sym(SDLK_s, KMOD_PRIMARY, key); 1104 toString_Sym(SDLK_s, KMOD_PRIMARY, key);
1086 format_String(&str, "# " folder_Icon " ZIP Archive\n" 1105 format_String(&str, "# %s\n"
1087 "%s is a ZIP archive.\n\n%s\n\n", 1106 "%s is a compressed archive.\n\n%s\n\n",
1107 zipPageHeading_(param),
1088 cstr_Rangecc(baseName_Path(d->mod.url)), 1108 cstr_Rangecc(baseName_Path(d->mod.url)),
1089 format_CStr(cstr_Lang("error.unsupported.suggestsave"), 1109 format_CStr(cstr_Lang("error.unsupported.suggestsave"),
1090 cstr_String(key), 1110 cstr_String(key),
@@ -1101,6 +1121,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1101 /* Make a simple document with an image or audio player. */ 1121 /* Make a simple document with an image or audio player. */
1102 docFormat = gemini_GmDocumentFormat; 1122 docFormat = gemini_GmDocumentFormat;
1103 setRange_String(&d->sourceMime, param); 1123 setRange_String(&d->sourceMime, param);
1124 const iGmLinkId imgLinkId = 1; /* there's only the one link */
1104 if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) { 1125 if ((isAudio && isInitialUpdate) || (!isAudio && isRequestFinished)) {
1105 const char *linkTitle = 1126 const char *linkTitle =
1106 startsWith_String(mimeStr, "image/") ? "Image" : "Audio"; 1127 startsWith_String(mimeStr, "image/") ? "Image" : "Audio";
@@ -1112,7 +1133,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1112 } 1133 }
1113 format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), linkTitle); 1134 format_String(&str, "=> %s %s\n", cstr_String(d->mod.url), linkTitle);
1114 setData_Media(media_GmDocument(d->doc), 1135 setData_Media(media_GmDocument(d->doc),
1115 1, 1136 imgLinkId,
1116 mimeStr, 1137 mimeStr,
1117 &response->body, 1138 &response->body,
1118 !isRequestFinished ? partialData_MediaFlag : 0); 1139 !isRequestFinished ? partialData_MediaFlag : 0);
@@ -1121,7 +1142,7 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1121 else if (isAudio && !isInitialUpdate) { 1142 else if (isAudio && !isInitialUpdate) {
1122 /* Update the audio content. */ 1143 /* Update the audio content. */
1123 setData_Media(media_GmDocument(d->doc), 1144 setData_Media(media_GmDocument(d->doc),
1124 1, 1145 imgLinkId,
1125 mimeStr, 1146 mimeStr,
1126 &response->body, 1147 &response->body,
1127 !isRequestFinished ? partialData_MediaFlag : 0); 1148 !isRequestFinished ? partialData_MediaFlag : 0);
@@ -1157,6 +1178,43 @@ static void updateDocument_DocumentWidget_(iDocumentWidget *d, const iGmResponse
1157 if (setSource) { 1178 if (setSource) {
1158 setSource_DocumentWidget(d, &str); 1179 setSource_DocumentWidget(d, &str);
1159 } 1180 }
1181 if (isRequestFinished) {
1182 /* Gempub: Preload cover image. */ {
1183 /* TODO: move to a gempub.c along with other related code */
1184 iString *localPath = localFilePathFromUrl_String(d->mod.url);
1185 if (localPath) {
1186 if (!iCmpStr(mediaTypeFromPath_String(localPath), "application/gpub+zip")) {
1187 iArchive *arch = iClob(new_Archive());
1188 if (openFile_Archive(arch, localPath)) {
1189 iBool haveImage = iFalse;
1190 for (size_t linkId = 1; ; linkId++) {
1191 const iString *linkUrl = linkUrl_GmDocument(d->doc, linkId);
1192 if (!linkUrl) break;
1193 if (findLinkImage_Media(media_GmDocument(d->doc), linkId)) {
1194 continue; /* got this already */
1195 }
1196 if (linkFlags_GmDocument(d->doc, linkId) & imageFileExtension_GmLinkFlag) {
1197 iString *imgEntryPath = collect_String(localFilePathFromUrl_String(linkUrl));
1198 remove_Block(&imgEntryPath->chars, 0, size_String(localPath) + 1 /* slash, too */);
1199 setData_Media(media_GmDocument(d->doc),
1200 linkId,
1201 collectNewCStr_String(mediaTypeFromPath_String(linkUrl)),
1202 data_Archive(arch, imgEntryPath),
1203 0);
1204 haveImage = iTrue;
1205 }
1206 }
1207 if (haveImage) {
1208 redoLayout_GmDocument(d->doc);
1209 updateVisible_DocumentWidget_(d);
1210 invalidate_DocumentWidget_(d);
1211 }
1212 }
1213 }
1214 delete_String(localPath);
1215 }
1216 }
1217 }
1160 deinit_String(&str); 1218 deinit_String(&str);
1161 } 1219 }
1162} 1220}