=> 960df03c17091aca37f53eaab8fc27c669d26a5e
[1mdiff --git a/res/arabic.fontpack/fontpack.ini b/res/arabic.fontpack/fontpack.ini[m [1mindex 00ad5241..305878ce 100644[m [1m--- a/res/arabic.fontpack/fontpack.ini[m [1m+++ b/res/arabic.fontpack/fontpack.ini[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32mversion = 1[m [32m+[m [arabic][m name = "Noto Sans Arabic UI"[m auxiliary = true[m [1mdiff --git a/res/cjk.fontpack/fontpack.ini b/res/cjk.fontpack/fontpack.ini[m [1mindex fbac54be..6e0d274c 100644[m [1m--- a/res/cjk.fontpack/fontpack.ini[m [1m+++ b/res/cjk.fontpack/fontpack.ini[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32mversion = 1[m [32m+[m [notosansjp][m name = "Noto Sans JP"[m auxiliary = true[m [1mdiff --git a/res/default.fontpack/fontpack.ini b/res/default.fontpack/fontpack.ini[m [1mindex 68316ef6..f8ef31ce 100644[m [1m--- a/res/default.fontpack/fontpack.ini[m [1m+++ b/res/default.fontpack/fontpack.ini[m [36m@@ -17,6 +17,8 @@[m # `glyphscale` and `voffset` can also be specified separately for the UI and[m # document domains by prefixing `ui.` or `doc.` to the key.[m [m [32m+[m[32mversion = 1[m [32m+[m [default][m name = "Source Sans"[m regular = "SourceSans3-Regular.ttf"[m [1mdiff --git a/res/firasans.fontpack/fontpack.ini b/res/firasans.fontpack/fontpack.ini[m [1mindex 4378a757..c6eb4c77 100644[m [1m--- a/res/firasans.fontpack/fontpack.ini[m [1m+++ b/res/firasans.fontpack/fontpack.ini[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32mversion = 1[m [32m+[m [firasans][m name = "Fira Sans"[m glyphscale = 0.85[m [1mdiff --git a/res/literata.fontpack/fontpack.ini b/res/literata.fontpack/fontpack.ini[m [1mindex e4e49bcb..7c29491d 100644[m [1m--- a/res/literata.fontpack/fontpack.ini[m [1m+++ b/res/literata.fontpack/fontpack.ini[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32mversion = 1[m [32m+[m [literata][m name = "Literata"[m regular = "Literata-Regular-opsz=14.ttf"[m [1mdiff --git a/res/nunito.fontpack/fontpack.ini b/res/nunito.fontpack/fontpack.ini[m [1mindex ea4a12b8..2f2471e1 100644[m [1m--- a/res/nunito.fontpack/fontpack.ini[m [1m+++ b/res/nunito.fontpack/fontpack.ini[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32mversion = 1[m [32m+[m [nunito][m name = "Nunito"[m tweaks = 0x1 # some hardcoded kerning changes (`Th`, etc.)[m [1mdiff --git a/res/tinos.fontpack/fontpack.ini b/res/tinos.fontpack/fontpack.ini[m [1mindex 8759b752..a2cf811e 100644[m [1m--- a/res/tinos.fontpack/fontpack.ini[m [1m+++ b/res/tinos.fontpack/fontpack.ini[m [36m@@ -1,3 +1,5 @@[m [32m+[m[32mversion = 1[m [32m+[m [tinos][m name = "Tinos"[m glyphscale = 0.850[m [1mdiff --git a/src/app.c b/src/app.c[m [1mindex b317e7b3..cb5479e8 100644[m [1m--- a/src/app.c[m [1m+++ b/src/app.c[m [36m@@ -2669,6 +2669,7 @@[m [miBool handleCommand_App(const char *cmd) {[m const iBool isSplit = numRoots_Window(get_Window()) > 1;[m if (tabCount_Widget(tabs) > 1 || isSplit) {[m iWidget *closed = removeTabPage_Widget(tabs, index);[m [32m+[m[32m cancelAllRequests_DocumentWidget((iDocumentWidget *) closed);[m destroy_Widget(closed); /* released later */[m if (index == tabCount_Widget(tabs)) {[m index--;[m [1mdiff --git a/src/defs.h b/src/defs.h[m [1mindex 65096389..f5479cf3 100644[m [1m--- a/src/defs.h[m [1m+++ b/src/defs.h[m [36m@@ -125,11 +125,13 @@[m [miLocalDef int acceptKeyMod_ReturnKeyBehavior(int behavior) {[m #define delete_Icon "\u232b"[m #define copy_Icon "\u2398" //"\u2bba"[m #define check_Icon "\u2714"[m [31m-#define ballotCheck_Icon "\U0001f5f9"[m [32m+[m[32m#define ballotChecked_Icon "\U0001f5f9"[m [32m+[m[32m#define ballotUnchecked_Icon "\U0001f5f9"[m #define inbox_Icon "\U0001f4e5"[m #define book_Icon "\U0001f56e"[m #define bookmark_Icon "\U0001f516"[m #define folder_Icon "\U0001f4c1"[m [32m+[m[32m#define file_Icon "\U0001f5ce"[m #define openTab_Icon "\u2750"[m #define openTabBg_Icon "\u2b1a"[m #define openExt_Icon "\u27a0"[m [1mdiff --git a/src/fontpack.c b/src/fontpack.c[m [1mindex ca1d1582..fb1c98ee 100644[m [1m--- a/src/fontpack.c[m [1m+++ b/src/fontpack.c[m [36m@@ -33,7 +33,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include[m #include [m [m [31m-/* TODO: Clean up and/or reorder this file, it's a bit unorganized. */[m [32m+[m[32mconst char *mimeType_FontPack = "application/lagrange-fontpack+zip";[m [m float scale_FontSize(enum iFontSize size) {[m static const float sizes[max_FontSize] = {[m [36m@@ -57,38 +57,9 @@[m [mfloat scale_FontSize(enum iFontSize size) {[m return sizes[size];[m }[m [m [31m-iDeclareType(Fonts) [m [31m- [m [31m-struct Impl_Fonts {[m [31m- iString userDir;[m [31m- iPtrArray packs;[m [31m- iPtrArray files;[m [31m- iPtrArray specOrder; /* specs sorted by priority */[m [31m-};[m [31m-[m [31m-static iFonts fonts_;[m [31m-[m [31m-static void unloadFiles_Fonts_(iFonts *d) {[m [31m- /* TODO: Mark all files in font packs as not resident. */ [m [31m- iForEach(PtrArray, i, &d->files) {[m [31m- delete_FontFile(i.ptr);[m [31m- }[m [31m- clear_PtrArray(&d->files);[m [31m-}[m [31m-[m [31m-static iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {[m [31m- iForEach(PtrArray, i, &d->files) {[m [31m- iFontFile *ff = i.ptr;[m [31m- if (equal_String(&ff->id, id)) {[m [31m- return ff;[m [31m- }[m [31m- }[m [31m- return NULL;[m [31m-}[m [31m-[m /*----------------------------------------------------------------------------------------------*/[m [m [31m-iDefineTypeConstruction(FontFile)[m [32m+[m[32miDefineObjectConstruction(FontFile)[m [m void init_FontFile(iFontFile *d) {[m init_String(&d->id);[m [36m@@ -114,7 +85,7 @@[m [mstatic void load_FontFile_(iFontFile *d, const iBlock *data) {[m HB_MEMORY_MODE_READONLY, NULL, NULL);[m d->hbFace = hb_face_create(d->hbBlob, 0);[m d->hbFont = hb_font_create(d->hbFace);[m [31m-#endif [m [32m+[m[32m#endif[m }[m [m static void unload_FontFile_(iFontFile *d) {[m [36m@@ -126,12 +97,13 @@[m [mstatic void unload_FontFile_(iFontFile *d) {[m d->hbFont = NULL;[m d->hbFace = NULL;[m d->hbBlob = NULL;[m [31m-#endif [m [32m+[m[32m#endif[m clear_Block(&d->sourceData);[m iZap(d->stbInfo);[m }[m [m void deinit_FontFile(iFontFile *d) {[m [32m+[m[32m printf("FontFile %p {%s} is DESTROYED\n", d, cstr_String(&d->id));[m unload_FontFile_(d);[m deinit_Block(&d->sourceData);[m deinit_String(&d->id);[m [36m@@ -156,12 +128,12 @@[m [mvoid measureGlyph_FontFile(const iFontFile *d, uint32_t glyphIndex,[m [m /*----------------------------------------------------------------------------------------------*/[m [m [31m-[m iDefineTypeConstruction(FontSpec)[m [m void init_FontSpec(iFontSpec *d) {[m init_String(&d->id);[m init_String(&d->name);[m [32m+[m[32m init_String(&d->sourcePath);[m d->flags = 0;[m d->priority = 0;[m for (int i = 0; i < 2; ++i) {[m [36m@@ -173,52 +145,119 @@[m [mvoid init_FontSpec(iFontSpec *d) {[m }[m [m void deinit_FontSpec(iFontSpec *d) {[m [32m+[m[32m /* FontFile references are held by FontSpecs. */[m [32m+[m[32m iForIndices(i, d->styles) {[m [32m+[m[32m iRelease(d->styles[i]);[m [32m+[m[32m }[m [32m+[m[32m deinit_String(&d->sourcePath);[m deinit_String(&d->name);[m deinit_String(&d->id);[m }[m [m /*----------------------------------------------------------------------------------------------*/[m [m [31m-iDeclareType(FontPack)[m [31m-iDeclareTypeConstruction(FontPack)[m [32m+[m[32miDeclareType(Fonts)[m [32m+[m [32m+[m[32mstruct Impl_Fonts {[m [32m+[m[32m iString userDir;[m [32m+[m[32m iPtrArray packs;[m [32m+[m[32m iObjectList *files;[m [32m+[m[32m iPtrArray specOrder; /* specs sorted by priority */[m [32m+[m[32m};[m [32m+[m [32m+[m[32mstatic iFonts fonts_;[m [32m+[m [32m+[m[32mstatic void unloadFiles_Fonts_(iFonts *d) {[m [32m+[m[32m /* TODO: Mark all files in font packs as not resident. */[m[41m [m [32m+[m[32m clear_ObjectList(d->files);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic iFontFile *findFile_Fonts_(iFonts *d, const iString *id) {[m [32m+[m[32m iForEach(ObjectList, i, d->files) {[m [32m+[m[32m iFontFile *ff = i.object;[m [32m+[m[32m if (equal_String(&ff->id, id)) {[m [32m+[m[32m return ff;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m return NULL;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic void releaseUnusedFiles_Fonts_(iFonts *d) {[m [32m+[m[32m iForEach(ObjectList, i, d->files) {[m [32m+[m[32m iFontFile *ff = i.object;[m [32m+[m[32m if (ff->object.refCount == 1) {[m [32m+[m[32m /* No specs use this. */[m [32m+[m[32m //printf("[Fonts] releasing unused font file: %p {%s}\n", ff, cstr_String(&ff->id));[m [32m+[m[32m remove_ObjectListIterator(&i);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m [32m+[m[32m/*----------------------------------------------------------------------------------------------*/[m [m struct Impl_FontPack {[m [31m- const iArchive *archive; /* opened ZIP archive */[m [32m+[m[32m iString id; /* lowercase filename without the .fontpack extension */[m [32m+[m[32m int version;[m [32m+[m[32m iBool isStandalone;[m [32m+[m[32m iBool isReadOnly;[m iArray fonts; /* array of FontSpecs */[m [32m+[m[32m const iArchive *archive; /* opened ZIP archive */[m iString * loadPath;[m iFontSpec * loadSpec;[m };[m [m [32m+[m[32miDefineTypeConstruction(FontPack)[m [32m+[m void init_FontPack(iFontPack *d) {[m [31m- d->archive = NULL;[m [32m+[m[32m init_String(&d->id);[m [32m+[m[32m d->version = 0;[m [32m+[m[32m d->isStandalone = iFalse;[m [32m+[m[32m d->isReadOnly = iFalse;[m init_Array(&d->fonts, sizeof(iFontSpec));[m [32m+[m[32m d->archive = NULL;[m d->loadSpec = NULL;[m d->loadPath = NULL;[m }[m [m void deinit_FontPack(iFontPack *d) {[m [32m+[m[32m iAssert(d->archive == NULL);[m [32m+[m[32m iAssert(d->loadSpec == NULL);[m delete_String(d->loadPath);[m iForEach(Array, i, &d->fonts) {[m deinit_FontSpec(i.value);[m }[m deinit_Array(&d->fonts);[m [31m- iAssert(d->archive == NULL);[m [31m- iAssert(d->loadSpec == NULL);[m [32m+[m[32m deinit_String(&d->id);[m [32m+[m[32m releaseUnusedFiles_Fonts_(&fonts_);[m }[m [m [31m-iDefineTypeConstruction(FontPack)[m [32m+[m[32miFontPackId id_FontPack(const iFontPack *d) {[m [32m+[m[32m return (iFontPackId){ &d->id, d->version };[m [32m+[m[32m}[m [32m+[m [32m+[m[32mconst iPtrArray *listSpecs_FontPack(const iFontPack *d) {[m [32m+[m[32m if (!d) return NULL;[m [32m+[m[32m iPtrArray *list = collectNew_PtrArray();[m [32m+[m[32m iConstForEach(Array, i, &d->fonts) {[m [32m+[m[32m pushBack_PtrArray(list, i.value);[m [32m+[m[32m }[m [32m+[m[32m return list;[m [32m+[m[32m}[m [m void handleIniTable_FontPack_(void *context, const iString *table, iBool isStart) {[m iFontPack *d = context;[m if (isStart) {[m iAssert(!d->loadSpec);[m [31m- /* Each font ID must be unique. */[m [31m- if (!findSpec_Fonts(cstr_String(table))) {[m [32m+[m[32m /* Each font ID must be unique in the non-standalone packs. */[m [32m+[m[32m if (d->isStandalone || !findSpec_Fonts(cstr_String(table))) {[m d->loadSpec = new_FontSpec();[m set_String(&d->loadSpec->id, table);[m [32m+[m[32m if (d->loadPath) {[m [32m+[m[32m set_String(&d->loadSpec->sourcePath, d->loadPath);[m [32m+[m[32m }[m }[m }[m [31m- else {[m [32m+[m[32m else if (d->loadSpec) {[m /* Set fallback font files. */ {[m const iFontFile **styles = d->loadSpec->styles;[m if (!styles[regular_FontStyle]) {[m [36m@@ -229,11 +268,11 @@[m [mvoid handleIniTable_FontPack_(void *context, const iString *table, iBool isStart[m return;[m }[m if (!styles[semiBold_FontStyle]) {[m [31m- styles[semiBold_FontStyle] = styles[bold_FontStyle];[m [32m+[m[32m styles[semiBold_FontStyle] = ref_Object(styles[bold_FontStyle]);[m }[m for (size_t s = 0; s < max_FontStyle; s++) {[m if (!styles[s]) {[m [31m- styles[s] = styles[regular_FontStyle];[m [32m+[m[32m styles[s] = ref_Object(styles[regular_FontStyle]);[m }[m }[m }[m [36m@@ -263,7 +302,15 @@[m [mstatic iBlock *readFile_FontPack_(const iFontPack *d, const iString *path) {[m void handleIniKeyValue_FontPack_(void *context, const iString *table, const iString *key,[m const iTomlValue *value) {[m iFontPack *d = context;[m [31m- if (!d->loadSpec) return;[m [32m+[m[32m if (isEmpty_String(table)) {[m [32m+[m[32m if (!cmp_String(key, "version")) {[m [32m+[m[32m d->version = number_TomlValue(value);[m [32m+[m[32m }[m [32m+[m[32m return;[m [32m+[m[32m }[m [32m+[m[32m if (!d->loadSpec) {[m [32m+[m[32m return;[m [32m+[m[32m }[m iUnused(table);[m if (!cmp_String(key, "name") && value->type == string_TomlType) {[m set_String(&d->loadSpec->name, value->value.string); [m [36m@@ -322,12 +369,13 @@[m [mvoid handleIniKeyValue_FontPack_(void *context, const iString *table, const iStr[m ff = new_FontFile();[m set_String(&ff->id, fontFileId);[m load_FontFile_(ff, data);[m [31m- pushBack_PtrArray(&fonts_.files, ff); /* centralized ownership */[m [32m+[m[32m pushBack_ObjectList(fonts_.files, ff); /* centralized ownership */[m [32m+[m[32m iRelease(ff);[m delete_Block(data);[m // printf("[FontPack] loaded file: %s\n", cstr_String(fontFileId));[m }[m }[m [31m- d->loadSpec->styles[i] = ff;[m [32m+[m[32m d->loadSpec->styles[i] = ref_Object(ff);[m delete_String(fontFileId);[m break;[m }[m [36m@@ -348,29 +396,6 @@[m [mstatic iBool load_FontPack_(iFontPack *d, const iString *ini) {[m return ok;[m }[m [m [31m-#if 0[m [31m-iBool loadIniFile_FontPack(iFontPack *d, const iString *iniPath) {[m [31m- iBeginCollect();[m [31m- iBool ok = iFalse;[m [31m- iFile *f = iClob(new_File(iniPath));[m [31m- if (open_File(f, text_FileMode | readOnly_FileMode)) {[m [31m- d->loadPath = collect_String(newRange_String(dirName_Path(iniPath)));[m [31m- iString *src = collect_String(readString_File(f));[m [31m- [m [31m- iTomlParser *ini = collect_TomlParser(new_TomlParser());[m [31m- setHandlers_TomlParser(ini, handleIniTable_FontPack_, handleIniKeyValue_FontPack_, d);[m [31m- if (!parse_TomlParser(ini, src)) {[m [31m- fprintf(stderr, "[FontPack] error parsing %s\n", cstr_String(iniPath));[m [31m- }[m [31m- iAssert(d->loadSpec == NULL);[m [31m- d->loadPath = NULL;[m [31m- ok = iTrue;[m [31m- }[m [31m- iEndCollect();[m [31m- return ok;[m [31m-}[m [31m-#endif[m [31m-[m iBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {[m d->archive = zip;[m iBool ok = iFalse;[m [36m@@ -387,6 +412,28 @@[m [miBool loadArchive_FontPack(iFontPack *d, const iArchive *zip) {[m return ok;[m }[m [m [32m+[m[32mvoid setLoadPath_FontPack(iFontPack *d, const iString *path) {[m [32m+[m[32m if (!d->loadPath) {[m [32m+[m[32m d->loadPath = new_String();[m [32m+[m[32m }[m [32m+[m[32m set_String(d->loadPath, path);[m [32m+[m[32m /* Pack ID is based on the file name. */[m [32m+[m[32m setRange_String(&d->id, baseName_Path(path));[m [32m+[m[32m setRange_String(&d->id, withoutExtension_Path(&d->id));[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid setStandalone_FontPack(iFontPack *d, iBool standalone) {[m [32m+[m[32m d->isStandalone = standalone;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid setReadOnly_FontPack(iFontPack *d, iBool readOnly) {[m [32m+[m[32m d->isReadOnly = readOnly;[m [32m+[m[32m}[m [32m+[m [32m+[m[32miBool isReadOnly_FontPack(const iFontPack *d) {[m [32m+[m[32m return d->isReadOnly;[m [32m+[m[32m}[m [32m+[m /*----------------------------------------------------------------------------------------------*/[m [m static void unloadFonts_Fonts_(iFonts *d) {[m [36m@@ -397,14 +444,23 @@[m [mstatic void unloadFonts_Fonts_(iFonts *d) {[m clear_PtrArray(&d->packs);[m }[m [m [32m+[m[32mstatic int cmpName_FontSpecPtr_(const void *a, const void *b) {[m [32m+[m[32m const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;[m [32m+[m[32m return cmpStringCase_String(&(*p1)->name, &(*p2)->name);[m [32m+[m[32m}[m [32m+[m static int cmpPriority_FontSpecPtr_(const void *a, const void *b) {[m const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;[m [31m- return -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */[m [32m+[m[32m const int cmp = -iCmp((*p1)->priority, (*p2)->priority); /* highest priority first */[m [32m+[m[32m if (cmp) return cmp;[m [32m+[m[32m return cmpName_FontSpecPtr_(a, b);[m }[m [m [31m-static int cmpName_FontSpecPtr_(const void *a, const void *b) {[m [32m+[m[32mstatic int cmpSourceAndPriority_FontSpecPtr_(const void *a, const void *b) {[m const iFontSpec **p1 = (const iFontSpec **) a, **p2 = (const iFontSpec **) b;[m [31m- return cmpStringCase_String(&(*p1)->name, &(*p2)->name);[m [32m+[m[32m const int cmp = cmpStringCase_String(&(*p1)->sourcePath, &(*p2)->sourcePath);[m [32m+[m[32m if (cmp) return cmp;[m [32m+[m[32m return cmpPriority_FontSpecPtr_(a, b);[m }[m [m static void sortSpecs_Fonts_(iFonts *d) {[m [36m@@ -422,11 +478,13 @@[m [mvoid init_Fonts(const char *userDir) {[m iFonts *d = &fonts_;[m initCStr_String(&d->userDir, userDir);[m init_PtrArray(&d->packs);[m [31m- init_PtrArray(&d->files);[m [32m+[m[32m d->files = new_ObjectList();[m init_PtrArray(&d->specOrder);[m /* Load the required fonts. */ {[m iFontPack *pack = new_FontPack();[m [32m+[m[32m setCStr_String(&pack->id, "default");[m iArchive *arch = new_Archive();[m [32m+[m[32m setReadOnly_FontPack(pack, iTrue);[m openData_Archive(arch, &fontpackDefault_Embedded);[m loadArchive_FontPack(pack, arch); /* should never fail if we've made it this far */[m iRelease(arch);[m [36m@@ -436,7 +494,7 @@[m [mvoid init_Fonts(const char *userDir) {[m const char *locations[] = {[m ".",[m "./fonts",[m [31m- "../share/lagrange",[m [32m+[m[32m "../share/lagrange", /* Note: These must match CMakeLists.txt install destination */[m "../../share/lagrange",[m concatPath_CStr(userDir, "fonts"),[m userDir,[m [36m@@ -450,7 +508,13 @@[m [mvoid init_Fonts(const char *userDir) {[m iArchive *arch = new_Archive();[m if (openFile_Archive(arch, entryPath)) {[m iFontPack *pack = new_FontPack();[m [31m- pack->loadPath = copy_String(entryPath);[m [32m+[m[32m setLoadPath_FontPack(pack, entryPath);[m [32m+[m[32m setReadOnly_FontPack(pack, !isWritable_FileInfo(entry.value));[m [32m+[m[32m#if defined (iPlatformApple)[m [32m+[m[32m if (startsWith_String(pack->loadPath, cstr_String(execDir))) {[m [32m+[m[32m setReadOnly_FontPack(pack, iTrue);[m [32m+[m[32m }[m [32m+[m[32m#endif[m if (loadArchive_FontPack(pack, arch)) {[m pushBack_PtrArray(&d->packs, pack);[m }[m [36m@@ -491,13 +555,18 @@[m [mvoid init_Fonts(const char *userDir) {[m void deinit_Fonts(void) {[m iFonts *d = &fonts_;[m unloadFonts_Fonts_(d);[m [31m- unloadFiles_Fonts_(d);[m [32m+[m[32m //unloadFiles_Fonts_(d);[m [32m+[m[32m iAssert(isEmpty_ObjectList(d->files));[m deinit_PtrArray(&d->specOrder);[m deinit_PtrArray(&d->packs);[m [31m- deinit_PtrArray(&d->files);[m [32m+[m[32m iRelease(d->files);[m deinit_String(&d->userDir);[m }[m [m [32m+[m[32mconst iPtrArray *listPacks_Fonts(void) {[m [32m+[m[32m return &fonts_.packs;[m [32m+[m[32m}[m [32m+[m const iFontSpec *findSpec_Fonts(const char *fontId) {[m iFonts *d = &fonts_;[m iConstForEach(PtrArray, i, &d->specOrder) {[m [36m@@ -524,3 +593,73 @@[m [mconst iPtrArray *listSpecs_Fonts(iBool (*filterFunc)(const iFontSpec *)) {[m const iPtrArray *listSpecsByPriority_Fonts(void) {[m return &fonts_.specOrder;[m }[m [32m+[m [32m+[m[32mconst iString *infoPage_Fonts(void) {[m [32m+[m[32m iFonts *d = &fonts_;[m [32m+[m[32m iString *str = collectNewCStr_String("# Fonts\n");[m [32m+[m[32m iPtrArray *specsByPack = collectNew_PtrArray();[m [32m+[m[32m setCopy_PtrArray(specsByPack, &d->specOrder);[m [32m+[m[32m sort_Array(specsByPack, cmpSourceAndPriority_FontSpecPtr_);[m [32m+[m[32m iString *currentSourcePath = collectNew_String();[m [32m+[m[32m iConstForEach(PtrArray, i, specsByPack) {[m [32m+[m[32m const iFontSpec *spec = i.ptr;[m [32m+[m[32m if (isEmpty_String(&spec->sourcePath)) {[m [32m+[m[32m continue; /* built-in font */[m [32m+[m[32m }[m [32m+[m[32m if (!equal_String(&spec->sourcePath, currentSourcePath)) {[m [32m+[m[32m appendFormat_String(str, "=> %s %s%s\n",[m [32m+[m[32m cstrCollect_String(makeFileUrl_String(&spec->sourcePath)),[m [32m+[m[32m endsWithCase_String(&spec->sourcePath, ".fontpack") ? "\U0001f520 " : "",[m [32m+[m[32m cstr_Rangecc(baseName_Path(&spec->sourcePath)));[m [32m+[m[32m set_String(currentSourcePath, &spec->sourcePath);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m return str;[m [32m+[m[32m}[m [32m+[m [32m+[m[32mconst iFontPack *findPack_Fonts(const iString *path) {[m [32m+[m[32m iFonts *d = &fonts_;[m [32m+[m[32m iConstForEach(PtrArray, i, &d->packs) {[m [32m+[m[32m const iFontPack *pack = i.ptr;[m [32m+[m[32m if (pack->loadPath && equal_String(pack->loadPath, path)) {[m [32m+[m[32m return pack;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m return NULL;[m [32m+[m[32m}[m [32m+[m [32m+[m[32miBool preloadLocalFontpackForPreview_Fonts(iGmDocument *doc) {[m [32m+[m[32m iBool wasLoaded = iFalse;[m [32m+[m[32m for (size_t linkId = 1; ; linkId++) {[m [32m+[m[32m const iString *linkUrl = linkUrl_GmDocument(doc, linkId);[m [32m+[m[32m if (!linkUrl) {[m [32m+[m[32m break; /* ran out of links */[m [32m+[m[32m }[m [32m+[m[32m const int linkFlags = linkFlags_GmDocument(doc, linkId);[m [32m+[m[32m if (linkFlags & fontpackFileExtension_GmLinkFlag &&[m [32m+[m[32m scheme_GmLinkFlag(linkFlags) == file_GmLinkScheme) {[m [32m+[m[32m iMediaId linkMedia = findMediaForLink_Media(media_GmDocument(doc), linkId, fontpack_MediaType);[m [32m+[m[32m if (linkMedia.type) {[m [32m+[m[32m continue; /* got this one already */[m [32m+[m[32m }[m [32m+[m[32m iString *filePath = localFilePathFromUrl_String(linkUrl);[m [32m+[m[32m iFile *f = new_File(filePath);[m [32m+[m[32m if (open_File(f, readOnly_FileMode)) {[m [32m+[m[32m iBlock *fontPackArchiveData = readAll_File(f);[m [32m+[m[32m setUrl_Media(media_GmDocument(doc), linkId, fontpack_MediaType, linkUrl);[m [32m+[m[32m setData_Media(media_GmDocument(doc),[m [32m+[m[32m linkId,[m [32m+[m[32m collectNewCStr_String(mimeType_FontPack),[m [32m+[m[32m fontPackArchiveData,[m [32m+[m[32m 0);[m [32m+[m[32m delete_Block(fontPackArchiveData);[m [32m+[m[32m wasLoaded = iTrue;[m [32m+[m[32m }[m [32m+[m[32m iRelease(f);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m return wasLoaded;[m [32m+[m[32m}[m [32m+[m [32m+[m[32miDefineClass(FontFile)[m [41m+[m [1mdiff --git a/src/fontpack.h b/src/fontpack.h[m [1mindex e59154e3..429afb5d 100644[m [1m--- a/src/fontpack.h[m [1m+++ b/src/fontpack.h[m [36m@@ -30,6 +30,8 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m # include [m #endif[m [m [32m+[m[32mextern const char *mimeType_FontPack;[m [32m+[m /* Fontpacks are ZIP archives that contain a configuration file and one of more font[m files. The fontpack format is used instead of plain TTF/OTF because the text renderer[m uses additional metadata about each font.[m [36m@@ -68,21 +70,13 @@[m [menum iFontStyle {[m [m float scale_FontSize (enum iFontSize size);[m [m [31m-iDeclareType(FontSpec)[m [31m-iDeclareTypeConstruction(FontSpec)[m [32m+[m[32m/*----------------------------------------------------------------------------------------------*/[m [m [31m-enum iFontSpecFlags {[m [31m- override_FontSpecFlag = iBit(1),[m [31m- monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */[m [31m- auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */[m [31m- arabic_FontSpecFlag = iBit(4),[m [31m- fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */[m [31m-};[m [31m-[m [31m-iDeclareType(FontFile)[m [31m-iDeclareTypeConstruction(FontFile)[m [32m+[m[32miDeclareClass(FontFile)[m [32m+[m[32miDeclareObjectConstruction(FontFile)[m [m struct Impl_FontFile {[m [32m+[m[32m iObject object; /* reference-counted */[m iString id; /* for detecting when the same file is used in many places */[m enum iFontStyle style;[m iBlock sourceData;[m [36m@@ -107,9 +101,26 @@[m [muint8_t * rasterizeGlyph_FontFile(const iFontFile *, float xScale, float yScal[m void measureGlyph_FontFile (const iFontFile *, uint32_t glyphIndex,[m float xScale, float yScale, float xShift,[m int *x0, int *y0, int *x1, int *y1);[m [32m+[m [32m+[m[32m/*----------------------------------------------------------------------------------------------*/[m [32m+[m [32m+[m[32m/* FontSpec describes a typeface, combining multiple fonts into a group.[m [32m+[m[32m The user will be choosing FontSpecs instead of individual font files. */[m [32m+[m[32miDeclareType(FontSpec)[m [32m+[m[32miDeclareTypeConstruction(FontSpec)[m [32m+[m [32m+[m[32menum iFontSpecFlags {[m [32m+[m[32m override_FontSpecFlag = iBit(1),[m [32m+[m[32m monospace_FontSpecFlag = iBit(2), /* can be used in preformatted content */[m [32m+[m[32m auxiliary_FontSpecFlag = iBit(3), /* only used for looking up glyphs missing from other fonts */[m [32m+[m[32m arabic_FontSpecFlag = iBit(4),[m [32m+[m[32m fixNunitoKerning_FontSpecFlag = iBit(31), /* manual hardcoded kerning tweaks for Nunito */[m [32m+[m[32m};[m [32m+[m struct Impl_FontSpec {[m iString id; /* unique ID */[m iString name; /* human-readable label */[m [32m+[m[32m iString sourcePath; /* file where the path was loaded, could be a .fontpack */[m int flags;[m int priority;[m float heightScale[2]; /* overall height scaling; ui, document */[m [36m@@ -121,10 +132,38 @@[m [mstruct Impl_FontSpec {[m iLocalDef int scaleType_FontSpec(enum iFontSize sizeId) {[m return sizeId / contentRegular_FontSize;[m }[m [31m- [m [32m+[m [32m+[m[32m/*----------------------------------------------------------------------------------------------*/[m [32m+[m [32m+[m[32miDeclareType(FontPack)[m [32m+[m[32miDeclareTypeConstruction(FontPack)[m [32m+[m [32m+[m[32miDeclareType(FontPackId)[m [32m+[m [32m+[m[32mstruct Impl_FontPackId {[m [32m+[m[32m const iString *id;[m [32m+[m[32m int version;[m [32m+[m[32m};[m [32m+[m [32m+[m[32mvoid setReadOnly_FontPack (iFontPack *, iBool readOnly);[m [32m+[m[32mvoid setStandalone_FontPack (iFontPack *, iBool standalone);[m [32m+[m[32mvoid setLoadPath_FontPack (iFontPack *, const iString *path);[m [32m+[m[32miBool loadArchive_FontPack (iFontPack *, const iArchive *zip);[m [32m+[m [32m+[m[32miFontPackId id_FontPack (const iFontPack *);[m [32m+[m[32mconst iPtrArray * listSpecs_FontPack (const iFontPack *);[m [32m+[m[32miBool isReadOnly_FontPack (const iFontPack *);[m [32m+[m [32m+[m[32miDeclareType(GmDocument)[m [32m+[m void init_Fonts (const char *userDir);[m void deinit_Fonts (void);[m [m [32m+[m[32mconst iFontPack * findPack_Fonts (const iString *path);[m const iFontSpec * findSpec_Fonts (const char *fontId);[m [32m+[m[32mconst iPtrArray * listPacks_Fonts (void);[m const iPtrArray * listSpecs_Fonts (iBool (*filterFunc)(const iFontSpec *));[m const iPtrArray * listSpecsByPriority_Fonts (void);[m [32m+[m[32mconst iString * infoPage_Fonts (void);[m [32m+[m [32m+[m[32miBool preloadLocalFontpackForPreview_Fonts (iGmDocument *doc);[m [1mdiff --git a/src/gempub.c b/src/gempub.c[m [1mindex 23846414..952d72a1 100644[m [1m--- a/src/gempub.c[m [1m+++ b/src/gempub.c[m [36m@@ -337,7 +337,7 @@[m [miBool preloadCoverImage_Gempub(const iGempub *d, iGmDocument *doc) {[m for (size_t linkId = 1; ; linkId++) {[m const iString *linkUrl = linkUrl_GmDocument(doc, linkId);[m if (!linkUrl) break;[m [31m- if (findLinkImage_Media(media_GmDocument(doc), linkId)) {[m [32m+[m[32m if (findLinkImage_Media(media_GmDocument(doc), linkId).type) {[m continue; /* got this already */[m }[m if (linkFlags_GmDocument(doc, linkId) & imageFileExtension_GmLinkFlag) {[m [1mdiff --git a/src/gmdocument.c b/src/gmdocument.c[m [1mindex c98b0bb8..c271ad94 100644[m [1m--- a/src/gmdocument.c[m [1m+++ b/src/gmdocument.c[m [36m@@ -27,6 +27,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include "ui/color.h"[m #include "ui/text.h"[m #include "ui/metrics.h"[m [32m+[m[32m#include "ui/mediaui.h"[m #include "ui/window.h"[m #include "visited.h"[m #include "bookmarks.h"[m [36m@@ -224,7 +225,7 @@[m [mstatic iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li[m setScheme_GmLink_(link, finger_GmLinkScheme);[m }[m else if (equalCase_Rangecc(parts.scheme, "file")) {[m [31m- setScheme_GmLink_(link, file_GmLinkScheme);[m [32m+[m[32m setScheme_GmLink_(link, file_GmLinkScheme);[m[41m [m }[m else if (equalCase_Rangecc(parts.scheme, "data")) {[m setScheme_GmLink_(link, data_GmLinkScheme);[m [36m@@ -251,6 +252,9 @@[m [mstatic iRangecc addLink_GmDocument_(iGmDocument *d, iRangecc line, iGmLinkId *li[m endsWithCase_String(path, ".mid") || endsWithCase_String(path, ".ogg")) {[m link->flags |= audioFileExtension_GmLinkFlag;[m }[m [32m+[m[32m else if (endsWithCase_String(path, ".fontpack")) {[m [32m+[m[32m link->flags |= fontpackFileExtension_GmLinkFlag;[m [32m+[m[32m }[m delete_String(path);[m }[m /* Check if visited. */[m [36m@@ -503,7 +507,7 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m static const char *arrow = rightArrowhead_Icon;[m static const char *envelope = envelope_Icon;[m static const char *bullet = "\u2022";[m [31m- static const char *folder = "\U0001f4c1";[m [32m+[m[32m static const char *folder = file_Icon;[m static const char *globe = globe_Icon;[m static const char *quote = "\u201c";[m static const char *magnifyingGlass = "\U0001f50d";[m [36m@@ -900,77 +904,77 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m ((iGmRun *) back_Array(&d->layout))->flags |= endOfLine_GmRunFlag;[m /* Image or audio content. */[m if (type == link_GmLineType) {[m [31m- const iMediaId imageId = findLinkImage_Media(d->media, run.linkId);[m [31m- const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0;[m [31m- const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0;[m [31m- if (imageId) {[m [31m- iGmMediaInfo img;[m [31m- imageInfo_Media(d->media, imageId, &img);[m [31m- const iInt2 imgSize = imageSize_Media(d->media, imageId);[m [31m- linkContentWasLaidOut_GmDocument_(d, &img, run.linkId);[m [31m- const int margin = lineHeight_Text(paragraph_FontId) / 2;[m [32m+[m[32m /* TODO: Cleanup here? Move to a function of its own. */[m [32m+[m[32m// enum iMediaType mediaType = none_MediaType;[m [32m+[m[32m const iMediaId media = findMediaForLink_Media(d->media, run.linkId, none_MediaType);[m [32m+[m[32m iGmMediaInfo info;[m [32m+[m[32m info_Media(d->media, media, &info);[m [32m+[m[32m run.mediaType = media.type;[m [32m+[m[32m run.mediaId = media.id;[m [32m+[m[32m run.text = iNullRange;[m [32m+[m[32m run.font = uiLabel_FontId;[m [32m+[m[32m run.color = 0;[m [32m+[m[32m const int margin = lineHeight_Text(paragraph_FontId) / 2;[m [32m+[m[32m if (media.type) {[m pos.y += margin;[m [31m- run.bounds.pos = pos;[m [31m- run.bounds.size.x = d->size.x;[m [31m- const float aspect = (float) imgSize.y / (float) imgSize.x;[m [31m- run.bounds.size.y = d->size.x * aspect;[m [31m- /* Extend the image to full width, including outside margin, if the viewport[m [31m- is narrow enough. */[m [31m- if (isFullWidthImages) {[m [31m- run.bounds.size.x += d->outsideMargin * 2;[m [31m- run.bounds.size.y += d->outsideMargin * 2 * aspect;[m [31m- run.bounds.pos.x -= d->outsideMargin;[m [31m- } [m [31m- run.visBounds = run.bounds;[m [31m- const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);[m [31m- if (width_Rect(run.visBounds) > maxSize.x) {[m [31m- /* Don't scale the image up. */[m [31m- run.visBounds.size.y =[m [31m- run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);[m [31m- run.visBounds.size.x = maxSize.x;[m [31m- run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;[m [31m- run.bounds.size.y = run.visBounds.size.y;[m [31m- }[m [31m- run.text = iNullRange;[m [31m- run.font = 0;[m [31m- run.color = 0;[m [31m- run.mediaType = image_GmRunMediaType;[m [31m- run.mediaId = imageId;[m [31m- pushBack_Array(&d->layout, &run);[m [31m- pos.y += run.bounds.size.y + margin;[m [31m- }[m [31m- else if (audioId) {[m [31m- iGmMediaInfo info;[m [31m- audioInfo_Media(d->media, audioId, &info);[m [32m+[m[32m run.bounds.size.y = 0;[m linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);[m [31m- const int margin = lineHeight_Text(paragraph_FontId) / 2;[m [31m- pos.y += margin;[m [31m- run.bounds.pos = pos;[m [31m- run.bounds.size.x = d->size.x;[m [31m- run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;[m [31m- run.visBounds = run.bounds;[m [31m- run.text = iNullRange;[m [31m- run.color = 0;[m [31m- run.mediaType = audio_GmRunMediaType;[m [31m- run.mediaId = audioId;[m [31m- pushBack_Array(&d->layout, &run);[m [31m- pos.y += run.bounds.size.y + margin;[m }[m [31m- else if (downloadId) {[m [31m- iGmMediaInfo info;[m [31m- downloadInfo_Media(d->media, downloadId, &info);[m [31m- linkContentWasLaidOut_GmDocument_(d, &info, run.linkId);[m [31m- const int margin = lineHeight_Text(paragraph_FontId) / 2;[m [31m- pos.y += margin;[m [31m- run.bounds.pos = pos;[m [31m- run.bounds.size.x = d->size.x;[m [31m- run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;[m [31m- run.visBounds = run.bounds;[m [31m- run.text = iNullRange;[m [31m- run.color = 0;[m [31m- run.mediaType = download_GmRunMediaType;[m [31m- run.mediaId = downloadId;[m [31m- pushBack_Array(&d->layout, &run);[m [32m+[m[32m switch (media.type) {[m [32m+[m[32m case image_MediaType: {[m [32m+[m[32m const iInt2 imgSize = imageSize_Media(d->media, media);[m [32m+[m[32m run.bounds.pos = pos;[m [32m+[m[32m run.bounds.size.x = d->size.x;[m [32m+[m[32m const float aspect = (float) imgSize.y / (float) imgSize.x;[m [32m+[m[32m run.bounds.size.y = d->size.x * aspect;[m [32m+[m[32m /* Extend the image to full width, including outside margin, if the viewport[m [32m+[m[32m is narrow enough. */[m [32m+[m[32m if (isFullWidthImages) {[m [32m+[m[32m run.bounds.size.x += d->outsideMargin * 2;[m [32m+[m[32m run.bounds.size.y += d->outsideMargin * 2 * aspect;[m [32m+[m[32m run.bounds.pos.x -= d->outsideMargin;[m [32m+[m[32m }[m [32m+[m[32m run.visBounds = run.bounds;[m [32m+[m[32m const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);[m [32m+[m[32m if (width_Rect(run.visBounds) > maxSize.x) {[m [32m+[m[32m /* Don't scale the image up. */[m [32m+[m[32m run.visBounds.size.y =[m [32m+[m[32m run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);[m [32m+[m[32m run.visBounds.size.x = maxSize.x;[m [32m+[m[32m run.visBounds.pos.x = run.bounds.size.x / 2 - width_Rect(run.visBounds) / 2;[m [32m+[m[32m run.bounds.size.y = run.visBounds.size.y;[m [32m+[m[32m }[m [32m+[m[32m pushBack_Array(&d->layout, &run);[m [32m+[m[32m break;[m [32m+[m[32m }[m [32m+[m[32m case audio_MediaType: {[m [32m+[m[32m run.bounds.pos = pos;[m [32m+[m[32m run.bounds.size.x = d->size.x;[m [32m+[m[32m run.bounds.size.y = lineHeight_Text(uiContent_FontId) + 3 * gap_UI;[m [32m+[m[32m run.visBounds = run.bounds;[m [32m+[m[32m pushBack_Array(&d->layout, &run);[m [32m+[m[32m break;[m [32m+[m[32m }[m [32m+[m[32m case download_MediaType: {[m [32m+[m[32m run.bounds.pos = pos;[m [32m+[m[32m run.bounds.size.x = d->size.x;[m [32m+[m[32m run.bounds.size.y = 2 * lineHeight_Text(uiContent_FontId) + 4 * gap_UI;[m [32m+[m[32m run.visBounds = run.bounds;[m [32m+[m[32m pushBack_Array(&d->layout, &run);[m [32m+[m[32m break;[m [32m+[m[32m }[m [32m+[m[32m case fontpack_MediaType: {[m [32m+[m[32m run.bounds.pos = pos;[m [32m+[m[32m run.bounds.size.x = d->size.x;[m [32m+[m[32m run.bounds.size.y = height_FontpackUI(d->media, media.id, d->size.x);[m [32m+[m[32m run.visBounds = run.bounds;[m [32m+[m[32m pushBack_Array(&d->layout, &run);[m [32m+[m[32m break;[m [32m+[m[32m }[m [32m+[m[32m default:[m [32m+[m[32m break;[m [32m+[m[32m }[m [32m+[m[32m if (media.type && run.bounds.size.y) {[m pos.y += run.bounds.size.y + margin;[m }[m }[m [36m@@ -1057,21 +1061,6 @@[m [mconst iString *url_GmDocument(const iGmDocument *d) {[m return &d->url;[m }[m [m [31m-#if 0[m [31m-void reset_GmDocument(iGmDocument *d) {[m [31m- clear_Media(d->media);[m [31m- clearLinks_GmDocument_(d);[m [31m- clear_Array(&d->layout);[m [31m- clear_Array(&d->headings);[m [31m- clear_Array(&d->preMeta);[m [31m- clear_String(&d->url);[m [31m- clear_String(&d->localHost);[m [31m- clear_String(&d->source);[m [31m- clear_String(&d->unormSource);[m [31m- d->themeSeed = 0;[m [31m-}[m [31m-#endif[m [31m-[m static void setDerivedThemeColors_(enum iGmDocumentTheme theme) {[m set_Color(tmQuoteIcon_ColorId,[m mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f));[m [1mdiff --git a/src/gmdocument.h b/src/gmdocument.h[m [1mindex b2c6d9b7..20bc9890 100644[m [1m--- a/src/gmdocument.h[m [1m+++ b/src/gmdocument.h[m [36m@@ -90,6 +90,7 @@[m [menum iGmLinkFlag {[m query_GmLinkFlag = iBit(14), /* Gopher query link */[m iconFromLabel_GmLinkFlag = iBit(15), /* use an Emoji/special character from label */[m isOpen_GmLinkFlag = iBit(16), /* currently open in a tab */[m [32m+[m[32m fontpackFileExtension_GmLinkFlag = iBit(17),[m };[m [m iLocalDef enum iGmLinkScheme scheme_GmLinkFlag(int flags) {[m [36m@@ -126,13 +127,6 @@[m [menum iGmRunFlags {[m altText_GmRunFlag = iBit(8),[m };[m [m [31m-enum iGmRunMediaType {[m [31m- none_GmRunMediaType,[m [31m- image_GmRunMediaType,[m [31m- audio_GmRunMediaType,[m [31m- download_GmRunMediaType,[m [31m-};[m [31m-[m /* This structure is tightly packed because GmDocuments are mostly composed of[m a large number of GmRuns. */[m struct Impl_GmRun {[m [36m@@ -146,12 +140,16 @@[m [mstruct Impl_GmRun {[m uint32_t color : 7; /* see max_ColorId */[m [m uint32_t font : 10;[m [31m- uint32_t mediaType : 2;[m [31m- uint32_t mediaId : 10; /* zero if not an image */[m [32m+[m[32m uint32_t mediaType : 3;[m [32m+[m[32m uint32_t mediaId : 9; /* zero if not an image */[m uint32_t preId : 10; /* preformatted block ID (sequential); merge with mediaId? */[m };[m };[m [m [32m+[m[32miLocalDef iMediaId mediaId_GmRun(const iGmRun *d) {[m [32m+[m[32m return (iMediaId){ .type = d->mediaType, .id = d->mediaId };[m [32m+[m[32m}[m [32m+[m iDeclareType(GmRunRange)[m [m struct Impl_GmRunRange {[m [1mdiff --git a/src/gmrequest.c b/src/gmrequest.c[m [1mindex 1a9e83a9..f7a22e0a 100644[m [1m--- a/src/gmrequest.c[m [1m+++ b/src/gmrequest.c[m [36m@@ -361,6 +361,9 @@[m [mstatic const iBlock *aboutPageSource_(iRangecc path, iRangecc query) {[m if (equalCase_Rangecc(path, "debug")) {[m return utf8_String(debugInfo_App());[m }[m [32m+[m[32m if (equalCase_Rangecc(path, "fonts")) {[m [32m+[m[32m return utf8_String(infoPage_Fonts());[m [32m+[m[32m }[m if (equalCase_Rangecc(path, "feeds")) {[m return utf8_String(entryListPage_Feeds());[m }[m [36m@@ -710,8 +713,9 @@[m [mvoid submit_GmRequest(iGmRequest *d) {[m sort_Array(sortedInfo, (int (*)(const void *, const void *)) cmp_FileInfoPtr_);[m iForEach(PtrArray, s, sortedInfo) {[m const iFileInfo *entry = s.ptr;[m [31m- appendFormat_String(page, "=> %s %s%s\n",[m [32m+[m[32m appendFormat_String(page, "=> %s %s%s%s\n",[m cstrCollect_String(makeFileUrl_String(path_FileInfo(entry))),[m [32m+[m[32m isDirectory_FileInfo(entry) ? folder_Icon " " : "",[m cstr_Rangecc(baseName_Path(path_FileInfo(entry))),[m isDirectory_FileInfo(entry) ? iPathSeparator : "");[m iRelease(entry);[m [36m@@ -808,9 +812,10 @@[m [mvoid submit_GmRequest(iGmRequest *d) {[m const iString *subPath = e.value;[m iRangecc relSub = range_String(subPath);[m relSub.start += size_String(entryPath);[m [31m- appendFormat_String(page, "=> %s/%s %s\n",[m [32m+[m[32m appendFormat_String(page, "=> %s/%s %s%s\n",[m cstr_String(&d->url),[m cstr_String(withSpacesEncoded_String(collectNewRange_String(relSub))),[m [32m+[m[32m endsWith_Rangecc(relSub, "/") ? folder_Icon " " : "",[m cstr_Rangecc(relSub));[m }[m resp->statusCode = success_GmStatusCode;[m [1mdiff --git a/src/gmutil.c b/src/gmutil.c[m [1mindex 971747d4..5be7e198 100644[m [1m--- a/src/gmutil.c[m [1m+++ b/src/gmutil.c[m [36m@@ -21,6 +21,7 @@[m [mANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT[m SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m #include "gmutil.h"[m [32m+[m[32m#include "fontpack.h"[m [m #include [m #include [m [36m@@ -511,7 +512,8 @@[m [mconst iString *findContainerArchive_Path(const iString *path) {[m while (!isEmpty_String(path) && cmp_String(path, ".")) {[m iString *dir = newRange_String(dirName_Path(path));[m if (endsWithCase_String(dir, ".zip") ||[m [31m- endsWithCase_String(dir, ".gpub")) {[m [32m+[m[32m endsWithCase_String(dir, ".gpub") ||[m [32m+[m[32m endsWithCase_String(dir, ".fontpack")) {[m iEndCollect();[m return collect_String(dir);[m }[m [36m@@ -534,6 +536,9 @@[m [mconst char *mediaTypeFromFileExtension_String(const iString *d) {[m else if (endsWithCase_String(d, ".gpub")) {[m return "application/gpub+zip";[m }[m [32m+[m[32m else if (endsWithCase_String(d, ".fontpack")) {[m [32m+[m[32m return mimeType_FontPack;[m [32m+[m[32m }[m else if (endsWithCase_String(d, ".xml")) {[m return "text/xml";[m }[m [36m@@ -562,6 +567,7 @@[m [mconst char *mediaTypeFromFileExtension_String(const iString *d) {[m return "audio/midi";[m }[m else if (endsWithCase_String(d, ".txt") ||[m [32m+[m[32m endsWithCase_String(d, ".ini") ||[m endsWithCase_String(d, ".md") ||[m endsWithCase_String(d, ".c") ||[m endsWithCase_String(d, ".h") ||[m [1mdiff --git a/src/media.c b/src/media.c[m [1mindex 26f0af4b..0ce2ac5c 100644[m [1m--- a/src/media.c[m [1m+++ b/src/media.c[m [36m@@ -36,6 +36,7 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m #include [m #include [m [32m+[m[32m#include [m #include [m #include [m #include [m [36m@@ -287,45 +288,112 @@[m [miDefineTypeConstruction(GmDownload)[m [m /*----------------------------------------------------------------------------------------------*/[m [m [32m+[m[32miDeclareType(GmFontpack)[m [32m+[m [32m+[m[32mstruct Impl_GmFontpack {[m [32m+[m[32m iGmMediaProps props;[m [32m+[m[32m iString packId;[m [32m+[m[32m iFontpackMediaInfo info;[m [32m+[m[32m /* TODO: Font preview images? */[m [32m+[m[32m};[m [32m+[m [32m+[m[32mvoid init_GmFontpack(iGmFontpack *d) {[m [32m+[m[32m init_GmMediaProps_(&d->props);[m [32m+[m[32m init_String(&d->packId);[m [32m+[m[32m iZap(d->info);[m [32m+[m[32m d->info.names = new_StringList();[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid deinit_GmFontpack(iGmFontpack *d) {[m [32m+[m[32m iRelease(d->info.names);[m [32m+[m[32m deinit_String(&d->packId);[m [32m+[m[32m deinit_GmMediaProps_(&d->props);[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic void loadData_GmFontpack_(iGmFontpack *d, const iBlock *data) {[m [32m+[m[32m const iString *loadPath = collect_String(localFilePathFromUrl_String(&d->props.url));[m [32m+[m[32m const iFontPack *pack = findPack_Fonts(loadPath);[m [32m+[m[32m d->info.isValid = d->info.isInstalled = pack != NULL;[m [32m+[m[32m d->info.isReadOnly = iFalse;[m [32m+[m[32m if (!pack) {[m [32m+[m[32m /* Let's load it now temporarily and see what's inside. */[m [32m+[m[32m iArchive *zip = new_Archive();[m [32m+[m[32m if (openData_Archive(zip, data)) {[m [32m+[m[32m iFontPack *fp = collect_FontPack(new_FontPack());[m [32m+[m[32m setLoadPath_FontPack(fp, loadPath);[m [32m+[m[32m setStandalone_FontPack(fp, iTrue);[m [32m+[m[32m if (loadArchive_FontPack(fp, zip)) {[m [32m+[m[32m d->info.isValid = iTrue;[m [32m+[m[32m pack = fp;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m iRelease(zip);[m [32m+[m[32m }[m [32m+[m[32m if (pack) {[m [32m+[m[32m set_String(&d->packId, id_FontPack(pack).id);[m [32m+[m[32m d->info.packId.id = &d->packId; /* we own this String */[m [32m+[m[32m d->info.packId.version = id_FontPack(pack).version;[m [32m+[m[32m d->info.isReadOnly = isReadOnly_FontPack(pack);[m [32m+[m[32m }[m [32m+[m[32m iPtrSet *unique = new_PtrSet();[m [32m+[m[32m iConstForEach(PtrArray, i, listSpecs_FontPack(pack)) {[m [32m+[m[32m const iFontSpec *spec = i.ptr;[m [32m+[m[32m pushBack_StringList(d->info.names, &spec->name);[m [32m+[m[32m iForIndices(j, spec->styles) {[m [32m+[m[32m insert_PtrSet(unique, spec->styles[j]);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m iConstForEach(PtrSet, j, unique) {[m [32m+[m[32m d->info.sizeInBytes += size_Block(&((const iFontFile *) *j.value)->sourceData);[m [32m+[m[32m }[m [32m+[m[32m delete_PtrSet(unique);[m [32m+[m[32m}[m [32m+[m [32m+[m[32miDefineTypeConstruction(GmFontpack)[m [32m+[m [32m+[m[32m/*----------------------------------------------------------------------------------------------*/[m [32m+[m struct Impl_Media {[m [31m- iPtrArray images;[m [31m- iPtrArray audio;[m [31m- iPtrArray downloads;[m [32m+[m[32m iPtrArray items[max_MediaType];[m [32m+[m[32m /* TODO: Add a hash to quickly look up a link's media. */[m };[m [m iDefineTypeConstruction(Media)[m [m void init_Media(iMedia *d) {[m [31m- init_PtrArray(&d->images);[m [31m- init_PtrArray(&d->audio);[m [31m- init_PtrArray(&d->downloads);[m [32m+[m[32m iForIndices(i, d->items) {[m [32m+[m[32m init_PtrArray(&d->items[i]);[m [32m+[m[32m }[m }[m [m void deinit_Media(iMedia *d) {[m clear_Media(d);[m [31m- deinit_PtrArray(&d->downloads);[m [31m- deinit_PtrArray(&d->audio);[m [31m- deinit_PtrArray(&d->images);[m [32m+[m[32m iForIndices(i, d->items) {[m [32m+[m[32m deinit_PtrArray(&d->items[i]);[m [32m+[m[32m }[m }[m [m void clear_Media(iMedia *d) {[m [31m- iForEach(PtrArray, i, &d->images) {[m [32m+[m[32m iForEach(PtrArray, i, &d->items[image_MediaType]) {[m deinit_GmImage(i.ptr);[m }[m [31m- clear_PtrArray(&d->images);[m [31m- iForEach(PtrArray, a, &d->audio) {[m [32m+[m[32m iForEach(PtrArray, a, &d->items[audio_MediaType]) {[m deinit_GmAudio(a.ptr);[m }[m [31m- clear_PtrArray(&d->audio);[m [31m- iForEach(PtrArray, n, &d->downloads) {[m [32m+[m[32m iForEach(PtrArray, n, &d->items[download_MediaType]) {[m deinit_GmDownload(n.ptr);[m }[m [31m- clear_PtrArray(&d->downloads);[m [32m+[m[32m iForEach(PtrArray, f, &d->items[fontpack_MediaType]) {[m [32m+[m[32m deinit_GmFontpack(f.ptr);[m [32m+[m[32m }[m [32m+[m[32m iForIndices(type, d->items) {[m [32m+[m[32m clear_PtrArray(&d->items[type]);[m [32m+[m[32m }[m }[m [m size_t memorySize_Media(const iMedia *d) {[m size_t memSize = 0;[m [31m- iConstForEach(PtrArray, i, &d->images) {[m [32m+[m[32m iConstForEach(PtrArray, i, &d->items[image_MediaType]) {[m const iGmImage *img = i.ptr;[m if (img->texture) {[m const iInt2 texSize = size_SDLTexture(img->texture);[m [36m@@ -335,34 +403,49 @@[m [msize_t memorySize_Media(const iMedia *d) {[m memSize += size_Block(&img->partialData);[m }[m }[m [31m- iConstForEach(PtrArray, a, &d->audio) {[m [32m+[m[32m iConstForEach(PtrArray, a, &d->items[audio_MediaType]) {[m const iGmAudio *audio = a.ptr;[m if (audio->player) {[m memSize += sourceDataSize_Player(audio->player);[m }[m }[m [31m- iConstForEach(PtrArray, n, &d->downloads) {[m [32m+[m[32m iConstForEach(PtrArray, n, &d->items[download_MediaType]) {[m const iGmDownload *down = n.ptr;[m memSize += down->numBytes;[m }[m return memSize; [m }[m [m [31m-iBool setDownloadUrl_Media(iMedia *d, iGmLinkId linkId, const iString *url) {[m [31m- iGmDownload *dl = NULL;[m [31m- iMediaId existing = findLinkDownload_Media(d, linkId);[m [31m- iBool isNew = iFalse;[m [31m- if (!existing) {[m [31m- isNew = iTrue;[m [31m- dl = new_GmDownload();[m [31m- dl->props.linkId = linkId;[m [31m- dl->props.isPermanent = iTrue;[m [31m- set_String(&dl->props.url, url);[m [31m- pushBack_PtrArray(&d->downloads, dl);[m [32m+[m[32miBool setUrl_Media(iMedia *d, iGmLinkId linkId, enum iMediaType mediaType, const iString *url) {[m [32m+[m[32m iMediaId existing = findMediaForLink_Media(d, linkId, mediaType);[m [32m+[m[32m const iBool isNew = !existing.id;[m [32m+[m[32m iGmMediaProps *props = NULL;[m [32m+[m[32m if (mediaType == download_MediaType) {[m [32m+[m[32m iGmDownload *dl = NULL;[m [32m+[m[32m if (isNew) {[m [32m+[m[32m dl = new_GmDownload();[m [32m+[m[32m pushBack_PtrArray(&d->items[download_MediaType], dl);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m dl = at_PtrArray(&d->items[download_MediaType], index_MediaId(existing));[m [32m+[m[32m }[m [32m+[m[32m props = &dl->props;[m }[m [31m- else {[m [31m- iGmDownload *dl = at_PtrArray(&d->downloads, existing - 1);[m [31m- set_String(&dl->props.url, url);[m [32m+[m[32m else if (mediaType == fontpack_MediaType) {[m [32m+[m[32m iGmFontpack *fp = NULL;[m [32m+[m[32m if (isNew) {[m [32m+[m[32m fp = new_GmFontpack();[m [32m+[m[32m pushBack_PtrArray(&d->items[fontpack_MediaType], fp);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m fp = at_PtrArray(&d->items[fontpack_MediaType], index_MediaId(existing));[m [32m+[m[32m }[m [32m+[m[32m props = &fp->props;[m [32m+[m[32m }[m [32m+[m[32m if (props) {[m [32m+[m[32m props->linkId = linkId;[m [32m+[m[32m props->isPermanent = iTrue;[m [32m+[m[32m set_String(&props->url, url);[m }[m return isNew;[m }[m [36m@@ -372,16 +455,17 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m const iBool isPartial = (flags & partialData_MediaFlag) != 0;[m const iBool allowHide = (flags & allowHide_MediaFlag) != 0;[m const iBool isDeleting = (!mime || !data);[m [31m- iMediaId existing = findLinkImage_Media(d, linkId);[m [32m+[m[32m iMediaId existing = findMediaForLink_Media(d, linkId, none_MediaType);// findLinkImage_Media(d, linkId);[m [32m+[m[32m const size_t existingIndex = index_MediaId(existing);[m iBool isNew = iFalse;[m [31m- if (existing) {[m [32m+[m[32m if (existing.type == image_MediaType) {[m iGmImage *img;[m if (isDeleting) {[m [31m- take_PtrArray(&d->images, existing - 1, (void **) &img);[m [32m+[m[32m take_PtrArray(&d->items[image_MediaType], existingIndex, (void **) &img);[m delete_GmImage(img);[m }[m else {[m [31m- img = at_PtrArray(&d->images, existing - 1);[m [32m+[m[32m img = at_PtrArray(&d->items[image_MediaType], existingIndex);[m iAssert(equal_String(&img->props.mime, mime)); /* MIME cannot change */[m set_Block(&img->partialData, data);[m if (!isPartial) {[m [36m@@ -389,14 +473,14 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m }[m }[m }[m [31m- else if ((existing = findLinkAudio_Media(d, linkId)) != 0) {[m [32m+[m[32m else if (existing.type == audio_MediaType) {[m iGmAudio *audio;[m if (isDeleting) {[m [31m- take_PtrArray(&d->audio, existing - 1, (void **) &audio);[m [32m+[m[32m take_PtrArray(&d->items[audio_MediaType], existingIndex, (void **) &audio);[m delete_GmAudio(audio);[m }[m else {[m [31m- audio = at_PtrArray(&d->audio, existing - 1);[m [32m+[m[32m audio = at_PtrArray(&d->items[audio_MediaType], existingIndex);[m iAssert(equal_String(&audio->props.mime, mime)); /* MIME cannot change */[m updateSourceData_Player(audio->player, mime, data, append_PlayerUpdate);[m if (!isPartial) {[m [36m@@ -408,14 +492,14 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m }[m }[m }[m [31m- else if ((existing = findLinkDownload_Media(d, linkId)) != 0) {[m [32m+[m[32m else if (existing.type == download_MediaType) {[m iGmDownload *dl;[m if (isDeleting) {[m [31m- take_PtrArray(&d->downloads, existing - 1, (void **) &dl);[m [32m+[m[32m take_PtrArray(&d->items[download_MediaType], existingIndex, (void **) &dl);[m delete_GmDownload(dl);[m }[m else {[m [31m- dl = at_PtrArray(&d->downloads, existing - 1);[m [32m+[m[32m dl = at_PtrArray(&d->items[download_MediaType], existingIndex);[m if (isEmpty_String(&dl->props.mime)) {[m set_String(&dl->props.mime, mime);[m }[m [36m@@ -428,6 +512,21 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m }[m }[m }[m [32m+[m[32m else if (existing.type == fontpack_MediaType) {[m [32m+[m[32m iGmFontpack *fp;[m [32m+[m[32m if (isDeleting) {[m [32m+[m[32m take_PtrArray(&d->items[fontpack_MediaType], existingIndex, (void **) &fp);[m [32m+[m[32m delete_GmFontpack(fp);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m iAssert(!isPartial);[m [32m+[m[32m fp = at_PtrArray(&d->items[fontpack_MediaType], existingIndex);[m [32m+[m[32m if (isEmpty_String(&fp->props.mime)) {[m [32m+[m[32m set_String(&fp->props.mime, mime);[m [32m+[m[32m }[m [32m+[m[32m loadData_GmFontpack_(fp, data);[m [32m+[m[32m }[m [32m+[m[32m }[m else if (!isDeleting) {[m if (startsWith_String(mime, "image/")) {[m /* Copy the image to a texture. */[m [36m@@ -435,7 +534,7 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m img->props.linkId = linkId; /* TODO: use a hash? */[m img->props.isPermanent = !allowHide;[m set_String(&img->props.mime, mime);[m [31m- pushBack_PtrArray(&d->images, img);[m [32m+[m[32m pushBack_PtrArray(&d->items[image_MediaType], img);[m if (!isPartial) {[m makeTexture_GmImage(img);[m }[m [36m@@ -450,7 +549,7 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m if (!isPartial) {[m updateSourceData_Player(audio->player, NULL, NULL, complete_PlayerUpdate);[m }[m [31m- pushBack_PtrArray(&d->audio, audio);[m [32m+[m[32m pushBack_PtrArray(&d->items[audio_MediaType], audio);[m /* Start playing right away. */[m start_Player(audio->player);[m postCommandf_App("media.player.started player:%p", audio->player);[m [36m@@ -460,125 +559,135 @@[m [miBool setData_Media(iMedia *d, iGmLinkId linkId, const iString *mime, const iBlo[m return isNew;[m }[m [m [31m-iMediaId findLinkImage_Media(const iMedia *d, iGmLinkId linkId) {[m [31m- /* TODO: use a hash */[m [31m- iConstForEach(PtrArray, i, &d->images) {[m [31m- const iGmImage *img = i.ptr;[m [31m- if (img->props.linkId == linkId) {[m [31m- return index_PtrArrayConstIterator(&i) + 1;[m [32m+[m[32mstatic iMediaId findMediaPtr_Media_(const iPtrArray *items, enum iMediaType mediaType, iGmLinkId linkId) {[m [32m+[m[32m iConstForEach(PtrArray, i, items) {[m [32m+[m[32m const iGmMediaProps *props = i.ptr;[m [32m+[m[32m if (props->linkId == linkId) {[m [32m+[m[32m return (iMediaId){[m [32m+[m[32m .type = mediaType,[m [32m+[m[32m .id = index_PtrArrayConstIterator(&i) + 1[m [32m+[m[32m };[m }[m }[m [31m- return 0;[m [31m-}[m [31m-[m [31m-size_t numAudio_Media(const iMedia *d) {[m [31m- return size_PtrArray(&d->audio);[m [32m+[m[32m return iInvalidMediaId;[m }[m [m [31m-iMediaId findLinkAudio_Media(const iMedia *d, iGmLinkId linkId) {[m [31m- /* TODO: use a hash */[m [31m- iConstForEach(PtrArray, i, &d->audio) {[m [31m- const iGmAudio *audio = i.ptr;[m [31m- if (audio->props.linkId == linkId) {[m [31m- return index_PtrArrayConstIterator(&i) + 1;[m [32m+[m[32miMediaId findMediaForLink_Media(const iMedia *d, iGmLinkId linkId, enum iMediaType mediaType) {[m [32m+[m[32m /* TODO: Use hashes, this will get very slow if there is a large number of media items. */[m [32m+[m[32m iMediaId mid;[m [32m+[m[32m for (int i = 0; i < max_MediaType; i++) {[m [32m+[m[32m if (mediaType == i || !mediaType) {[m [32m+[m[32m mid = findMediaPtr_Media_(&d->items[i], i, linkId);[m [32m+[m[32m if (mid.type) {[m [32m+[m[32m return mid;[m [32m+[m[32m }[m }[m }[m [31m- return 0;[m [32m+[m[32m return iInvalidMediaId;[m }[m [m [31m-iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {[m [31m- iConstForEach(PtrArray, i, &d->downloads) {[m [31m- const iGmDownload *dl = i.ptr;[m [31m- if (dl->props.linkId == linkId) {[m [31m- return index_PtrArrayConstIterator(&i) + 1;[m [31m- }[m [31m- }[m [31m- return 0;[m [32m+[m[32msize_t numAudio_Media(const iMedia *d) {[m [32m+[m[32m return size_PtrArray(&d->items[audio_MediaType]);[m }[m [m iInt2 imageSize_Media(const iMedia *d, iMediaId imageId) {[m [31m- if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {[m [31m- const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);[m [32m+[m[32m iAssert(imageId.type == image_MediaType);[m [32m+[m[32m const size_t index = index_MediaId(imageId);[m [32m+[m[32m if (index < size_PtrArray(&d->items[image_MediaType])) {[m [32m+[m[32m const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);[m return img->size;[m }[m return zero_I2();[m }[m [m [31m-SDL_Texture *imageTexture_Media(const iMedia *d, uint16_t imageId) {[m [31m- if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {[m [31m- const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);[m [32m+[m[32mSDL_Texture *imageTexture_Media(const iMedia *d, iMediaId imageId) {[m [32m+[m[32m iAssert(imageId.type == image_MediaType);[m [32m+[m[32m const size_t index = index_MediaId(imageId);[m [32m+[m[32m if (index < size_PtrArray(&d->items[image_MediaType])) {[m [32m+[m[32m const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);[m return img->texture;[m }[m return NULL;[m }[m [m [31m-iBool imageInfo_Media(const iMedia *d, iMediaId imageId, iGmMediaInfo *info_out) {[m [31m- if (imageId > 0 && imageId <= size_PtrArray(&d->images)) {[m [31m- const iGmImage *img = constAt_PtrArray(&d->images, imageId - 1);[m [31m- info_out->numBytes = img->numBytes;[m [31m- info_out->type = cstr_String(&img->props.mime);[m [31m- info_out->isPermanent = img->props.isPermanent;[m [31m- return iTrue;[m [32m+[m[32miBool info_Media(const iMedia *d, iMediaId mediaId, iGmMediaInfo *info_out) {[m [32m+[m[32m /* TODO: Use a hash. */[m [32m+[m[32m const size_t index = index_MediaId(mediaId);[m [32m+[m[32m switch (mediaId.type) {[m [32m+[m[32m case image_MediaType:[m [32m+[m[32m if (index < size_PtrArray(&d->items[image_MediaType])) {[m [32m+[m[32m const iGmImage *img = constAt_PtrArray(&d->items[image_MediaType], index);[m [32m+[m[32m info_out->numBytes = img->numBytes;[m [32m+[m[32m info_out->type = cstr_String(&img->props.mime);[m [32m+[m[32m info_out->isPermanent = img->props.isPermanent;[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m break;[m [32m+[m[32m case audio_MediaType:[m [32m+[m[32m if (index < size_PtrArray(&d->items[audio_MediaType])) {[m [32m+[m[32m const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);[m [32m+[m[32m info_out->type = cstr_String(&audio->props.mime);[m [32m+[m[32m info_out->isPermanent = audio->props.isPermanent;[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m break;[m [32m+[m[32m case download_MediaType:[m [32m+[m[32m if (index < size_PtrArray(&d->items[download_MediaType])) {[m [32m+[m[32m const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);[m [32m+[m[32m info_out->type = cstr_String(&dl->props.mime);[m [32m+[m[32m info_out->isPermanent = dl->props.isPermanent;[m [32m+[m[32m info_out->numBytes = dl->numBytes;[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m break;[m [32m+[m[32m case fontpack_MediaType:[m [32m+[m[32m /* TODO */[m [32m+[m[32m break;[m [32m+[m[32m default:[m [32m+[m[32m break;[m }[m iZap(*info_out);[m return iFalse;[m }[m [m iPlayer *audioData_Media(const iMedia *d, iMediaId audioId) {[m [31m- if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {[m [31m- const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);[m [32m+[m[32m iAssert(audioId.type == audio_MediaType);[m [32m+[m[32m const size_t index = index_MediaId(audioId);[m [32m+[m[32m if (index < size_PtrArray(&d->items[audio_MediaType])) {[m [32m+[m[32m const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);[m return audio->player;[m }[m return NULL;[m }[m [m [31m-iBool audioInfo_Media(const iMedia *d, iMediaId audioId, iGmMediaInfo *info_out) {[m [31m- if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {[m [31m- const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);[m [31m- info_out->type = cstr_String(&audio->props.mime);[m [31m- info_out->isPermanent = audio->props.isPermanent;[m [31m- return iTrue;[m [31m- }[m [31m- iZap(*info_out);[m [31m- return iFalse;[m [31m-}[m [31m-[m iPlayer *audioPlayer_Media(const iMedia *d, iMediaId audioId) {[m [31m- if (audioId > 0 && audioId <= size_PtrArray(&d->audio)) {[m [31m- const iGmAudio *audio = constAt_PtrArray(&d->audio, audioId - 1);[m [32m+[m[32m iAssert(audioId.type == audio_MediaType);[m [32m+[m[32m const size_t index = index_MediaId(audioId);[m [32m+[m[32m if (index < size_PtrArray(&d->items[audio_MediaType])) {[m [32m+[m[32m const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], index);[m return audio->player;[m }[m return NULL;[m }[m [m void pauseAllPlayers_Media(const iMedia *d, iBool setPaused) {[m [31m- for (size_t i = 0; i < size_PtrArray(&d->audio); ++i) {[m [31m- const iGmAudio *audio = constAt_PtrArray(&d->audio, i);[m [32m+[m[32m for (size_t i = 0; i < size_PtrArray(&d->items[audio_MediaType]); ++i) {[m [32m+[m[32m const iGmAudio *audio = constAt_PtrArray(&d->items[audio_MediaType], i);[m if (audio->player) {[m setPaused_Player(audio->player, setPaused);[m }[m }[m }[m [m [31m-iBool downloadInfo_Media(const iMedia *d, iMediaId downloadId, iGmMediaInfo *info_out) {[m [31m- if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {[m [31m- const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);[m [31m- info_out->type = cstr_String(&dl->props.mime);[m [31m- info_out->isPermanent = dl->props.isPermanent;[m [31m- info_out->numBytes = dl->numBytes;[m [31m- return iTrue;[m [31m- }[m [31m- iZap(*info_out);[m [31m- return iFalse;[m [31m-}[m [31m-[m void downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **path_out,[m float *bytesPerSecond_out, iBool *isFinished_out) {[m [31m- *path_out = NULL;[m [32m+[m[32m iAssert(downloadId.type == download_MediaType);[m [32m+[m[32m *path_out = NULL;[m *bytesPerSecond_out = 0.0f;[m [31m- *isFinished_out = iFalse;[m [31m- if (downloadId > 0 && downloadId <= size_PtrArray(&d->downloads)) {[m [31m- const iGmDownload *dl = constAt_PtrArray(&d->downloads, downloadId - 1);[m [32m+[m[32m *isFinished_out = iFalse;[m [32m+[m[32m const size_t index = index_MediaId(downloadId);[m [32m+[m[32m if (index < size_PtrArray(&d->items[download_MediaType])) {[m [32m+[m[32m const iGmDownload *dl = constAt_PtrArray(&d->items[download_MediaType], index);[m if (dl->path) {[m *path_out = dl->path;[m }[m [36m@@ -587,6 +696,16 @@[m [mvoid downloadStats_Media(const iMedia *d, iMediaId downloadId, const iString **p[m }[m }[m [m [32m+[m[32mvoid fontpackInfo_Media(const iMedia *d, iMediaId fontpackId, iFontpackMediaInfo *info_out) {[m [32m+[m[32m iAssert(fontpackId.type == fontpack_MediaType);[m [32m+[m[32m iZap(*info_out);[m [32m+[m[32m const size_t index = index_MediaId(fontpackId);[m [32m+[m[32m if (index < size_PtrArray(&d->items[fontpack_MediaType])) {[m [32m+[m[32m const iGmFontpack *fp = constAt_PtrArray(&d->items[fontpack_MediaType], index);[m [32m+[m[32m *info_out = fp->info;[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m /*----------------------------------------------------------------------------------------------*/[m [m static void updated_MediaRequest_(iAnyObject *obj) {[m [1mdiff --git a/src/media.h b/src/media.h[m [1mindex f7ad6efd..47a4da93 100644[m [1m--- a/src/media.h[m [1m+++ b/src/media.h[m [36m@@ -22,13 +22,13 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m #pragma once[m [m [32m+[m[32m#include "fontpack.h"[m [32m+[m #include [m #include [m #include [m #include [m [m [31m-typedef uint16_t iMediaId;[m [31m-[m iDeclareType(Player)[m iDeclareType(GmMediaInfo)[m [m [36m@@ -38,6 +38,7 @@[m [mstruct Impl_GmMediaInfo {[m iBool isPermanent;[m };[m [m [32m+[m[32miDeclareType(MediaId)[m iDeclareType(Media)[m iDeclareTypeConstruction(Media)[m [m [36m@@ -46,28 +47,81 @@[m [menum iMediaFlags {[m partialData_MediaFlag = iBit(2),[m };[m [m [31m-void clear_Media (iMedia *);[m [31m-iBool setDownloadUrl_Media (iMedia *, uint16_t linkId, const iString *url);[m [31m-iBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);[m [31m-[m [31m-size_t memorySize_Media (const iMedia *);[m [32m+[m[32menum iMediaType { /* Note: There is a limited number of bits for these; see GmRun below. */[m [32m+[m[32m none_MediaType,[m [32m+[m[32m image_MediaType,[m [32m+[m[32m //animatedImage_MediaType, /* TODO */[m [32m+[m[32m audio_MediaType,[m [32m+[m[32m download_MediaType,[m [32m+[m[32m fontpack_MediaType,[m [32m+[m[32m max_MediaType[m [32m+[m[32m};[m [m [31m-iMediaId findLinkImage_Media (const iMedia *, uint16_t linkId);[m [31m-iBool imageInfo_Media (const iMedia *, iMediaId imageId, iGmMediaInfo *info_out);[m [31m-iInt2 imageSize_Media (const iMedia *, iMediaId imageId);[m [31m-SDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId);[m [32m+[m[32mstruct Impl_MediaId {[m [32m+[m[32m enum iMediaType type;[m [32m+[m[32m uint16_t id; /* see GmRun for actually used number of bits */[m [32m+[m[32m};[m [m [31m-size_t numAudio_Media (const iMedia *);[m [31m-iMediaId findLinkAudio_Media (const iMedia *, uint16_t linkId);[m [31m-iBool audioInfo_Media (const iMedia *, iMediaId audioId, iGmMediaInfo *info_out);[m [31m-iPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);[m [31m-void pauseAllPlayers_Media(const iMedia *, iBool setPaused);[m [32m+[m[32miLocalDef size_t index_MediaId(const iMediaId mediaId) {[m [32m+[m[32m return (size_t) mediaId.id - 1;[m [32m+[m[32m}[m [32m+[m [32m+[m[32m#define iInvalidMediaId (iMediaId){ none_MediaType, 0 }[m [32m+[m [32m+[m[32mvoid clear_Media (iMedia *);[m [32m+[m[32miBool setUrl_Media (iMedia *, uint16_t linkId, enum iMediaType mediaType, const iString *url);[m [32m+[m[32miBool setData_Media (iMedia *, uint16_t linkId, const iString *mime, const iBlock *data, int flags);[m [32m+[m [32m+[m[32msize_t memorySize_Media (const iMedia *);[m [32m+[m[32miMediaId findMediaForLink_Media (const iMedia *, uint16_t linkId, enum iMediaType mediaType);[m [32m+[m [32m+[m[32miMediaId id_Media (const iMedia *, uint16_t linkId, enum iMediaType type);[m [32m+[m[32miBool info_Media (const iMedia *, iMediaId mediaId, iGmMediaInfo *info_out);[m [32m+[m [32m+[m[32miLocalDef iMediaId findLinkImage_Media(const iMedia *d, uint16_t linkId) {[m [32m+[m[32m return findMediaForLink_Media(d, linkId, image_MediaType);[m [32m+[m[32m}[m [32m+[m[32miLocalDef iMediaId findLinkAudio_Media (const iMedia *d, uint16_t linkId) {[m [32m+[m[32m return findMediaForLink_Media(d, linkId, audio_MediaType);[m [32m+[m[32m}[m [32m+[m[32miLocalDef iMediaId findLinkDownload_Media(const iMedia *d, uint16_t linkId) {[m [32m+[m[32m return findMediaForLink_Media(d, linkId, download_MediaType);[m [32m+[m[32m}[m [32m+[m [32m+[m[32miLocalDef iBool imageInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {[m [32m+[m[32m return info_Media(d, (iMediaId){ image_MediaType, mediaId }, info_out);[m [32m+[m[32m}[m [32m+[m[32miLocalDef iBool audioInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {[m [32m+[m[32m return info_Media(d, (iMediaId){ audio_MediaType, mediaId }, info_out);[m [32m+[m[32m}[m [32m+[m[32miLocalDef iBool downloadInfo_Media(const iMedia *d, uint16_t mediaId, iGmMediaInfo *info_out) {[m [32m+[m[32m return info_Media(d, (iMediaId){ download_MediaType, mediaId }, info_out);[m [32m+[m[32m}[m [32m+[m [32m+[m[32miInt2 imageSize_Media (const iMedia *, iMediaId imageId);[m [32m+[m[32mSDL_Texture * imageTexture_Media (const iMedia *, iMediaId imageId);[m [32m+[m [32m+[m[32msize_t numAudio_Media (const iMedia *);[m [32m+[m[32miPlayer * audioPlayer_Media (const iMedia *, iMediaId audioId);[m [32m+[m[32mvoid pauseAllPlayers_Media (const iMedia *, iBool setPaused);[m [m [31m-iMediaId findLinkDownload_Media (const iMedia *, uint16_t linkId);[m [31m-iBool downloadInfo_Media (const iMedia *, iMediaId downloadId, iGmMediaInfo *info_out);[m void downloadStats_Media (const iMedia *, iMediaId downloadId, const iString **path_out,[m float *bytesPerSecond_out, iBool *isFinished_out);[m [m [32m+[m[32miDeclareType(FontpackMediaInfo)[m [32m+[m [32m+[m[32mstruct Impl_FontpackMediaInfo {[m [32m+[m[32m iFontPackId packId;[m [32m+[m[32m iBool isValid;[m [32m+[m[32m iBool isInstalled;[m [32m+[m[32m iBool isReadOnly;[m [32m+[m[32m size_t sizeInBytes;[m [32m+[m[32m iStringList *names;[m [32m+[m[32m};[m [32m+[m [32m+[m[32mvoid fontpackInfo_Media (const iMedia *, iMediaId fontpackId,[m [32m+[m[32m iFontpackMediaInfo *info_out);[m [32m+[m /*----------------------------------------------------------------------------------------------*/[m [m iDeclareType(GmRequest)[m [36m@@ -78,7 +132,7 @@[m [miDeclareClass(MediaRequest)[m struct Impl_MediaRequest {[m iObject object;[m iDocumentWidget *doc;[m [31m- unsigned int linkId;[m [32m+[m[32m unsigned int linkId;[m[41m [m iGmRequest * req;[m };[m [m [1mdiff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c[m [1mindex 45a8cf2d..44db3e5b 100644[m [1m--- a/src/ui/documentwidget.c[m [1m+++ b/src/ui/documentwidget.c[m [36m@@ -400,7 +400,18 @@[m [mvoid init_DocumentWidget(iDocumentWidget *d) {[m addAction_Widget(w, navigateRoot_KeyShortcut, "navigate.root");[m }[m [m [32m+[m[32mvoid cancelAllRequests_DocumentWidget(iDocumentWidget *d) {[m [32m+[m[32m iForEach(ObjectList, i, d->media) {[m [32m+[m[32m iMediaRequest *mr = i.object;[m [32m+[m[32m cancel_GmRequest(mr->req);[m [32m+[m[32m }[m [32m+[m[32m if (d->request) {[m [32m+[m[32m cancel_GmRequest(d->request);[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m void deinit_DocumentWidget(iDocumentWidget *d) {[m [32m+[m[32m cancelAllRequests_DocumentWidget(d);[m pauseAllPlayers_Media(media_GmDocument(d->doc), iTrue);[m removeTicker_App(animate_DocumentWidget_, d);[m removeTicker_App(prerender_DocumentWidget_, d);[m [36m@@ -564,7 +575,8 @@[m [mstatic void addVisible_DocumentWidget_(void *context, const iGmRun *run) {[m pushBack_PtrArray(&d->visibleWideRuns, run);[m }[m }[m [31m- if (run->mediaType == audio_GmRunMediaType || run->mediaType == download_GmRunMediaType) {[m [32m+[m[32m /* Image runs are static so they're drawn as part of the content. */[m [32m+[m[32m if (run->mediaType && run->mediaType != image_MediaType) {[m iAssert(run->mediaId);[m pushBack_PtrArray(&d->visibleMedia, run);[m }[m [36m@@ -758,14 +770,14 @@[m [mstatic uint32_t mediaUpdateInterval_DocumentWidget_(const iDocumentWidget *d) {[m uint32_t interval = invalidInterval_;[m iConstForEach(PtrArray, i, &d->visibleMedia) {[m const iGmRun *run = i.ptr;[m [31m- if (run->mediaType == audio_GmRunMediaType) {[m [31m- iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);[m [32m+[m[32m if (run->mediaType == audio_MediaType) {[m [32m+[m[32m iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));[m if (flags_Player(plr) & adjustingVolume_PlayerFlag ||[m (isStarted_Player(plr) && !isPaused_Player(plr))) {[m interval = iMin(interval, 1000 / 15);[m }[m }[m [31m- else if (run->mediaType == download_GmRunMediaType) {[m [32m+[m[32m else if (run->mediaType == download_MediaType) {[m interval = iMin(interval, 1000);[m }[m }[m [36m@@ -784,8 +796,8 @@[m [mstatic void updateMedia_DocumentWidget_(iDocumentWidget *d) {[m refresh_Widget(d);[m iConstForEach(PtrArray, i, &d->visibleMedia) {[m const iGmRun *run = i.ptr;[m [31m- if (run->mediaType == audio_GmRunMediaType) {[m [31m- iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->mediaId);[m [32m+[m[32m if (run->mediaType == audio_MediaType) {[m [32m+[m[32m iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), mediaId_GmRun(run));[m if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag &&[m flags_Player(plr) & adjustingVolume_PlayerFlag) {[m setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse);[m [36m@@ -1244,6 +1256,9 @@[m [mstatic const char *zipPageHeading_(const iRangecc mime) {[m if (equalCase_Rangecc(mime, "application/gpub+zip")) {[m return book_Icon " Gempub";[m }[m [32m+[m[32m else if (equalCase_Rangecc(mime, mimeType_FontPack)) {[m [32m+[m[32m return "\U0001f520 Fontpack";[m [32m+[m[32m }[m iRangecc type = iNullRange;[m nextSplit_Rangecc(mime, "/", &type); /* skip the part before the slash */[m nextSplit_Rangecc(mime, "/", &type);[m [36m@@ -1258,165 +1273,175 @@[m [mstatic const char *zipPageHeading_(const iRangecc mime) {[m [m static void postProcessRequestContent_DocumentWidget_(iDocumentWidget *d, iBool isCached) {[m iWidget *w = as_Widget(d);[m [31m- delete_Gempub(d->sourceGempub);[m [31m- d->sourceGempub = NULL;[m [31m- if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||[m [31m- !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||[m [31m- endsWithCase_String(d->mod.url, ".gpub")) {[m [31m- iGempub *gempub = new_Gempub();[m [31m- if (open_Gempub(gempub, &d->sourceContent)) {[m [31m- setBaseUrl_Gempub(gempub, d->mod.url);[m [31m- setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));[m [31m- setCStr_String(&d->sourceMime, mimeType_Gempub);[m [31m- d->sourceGempub = gempub;[m [31m- }[m [31m- else {[m [31m- delete_Gempub(gempub);[m [31m- }[m [31m- }[m [31m- if (!d->sourceGempub) {[m [31m- const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));[m [31m- iBool isInside = iFalse;[m [31m- if (localPath && !fileExists_FileInfo(localPath)) {[m [31m- /* This URL may refer to a file inside the archive. */[m [31m- localPath = findContainerArchive_Path(localPath);[m [31m- isInside = iTrue;[m [31m- }[m [31m- if (localPath && equal_CStr(mediaType_Path(localPath), "application/gpub+zip")) {[m [32m+[m[32m /* Gempub page behavior and footer actions. */ {[m [32m+[m[32m /* TODO: move this to gempub.c */[m [32m+[m[32m delete_Gempub(d->sourceGempub);[m [32m+[m[32m d->sourceGempub = NULL;[m [32m+[m[32m if (!cmpCase_String(&d->sourceMime, "application/octet-stream") ||[m [32m+[m[32m !cmpCase_String(&d->sourceMime, mimeType_Gempub) ||[m [32m+[m[32m endsWithCase_String(d->mod.url, ".gpub")) {[m iGempub *gempub = new_Gempub();[m [31m- if (openFile_Gempub(gempub, localPath)) {[m [31m- setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));[m [31m- if (!isInside) {[m [31m- setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));[m [31m- setCStr_String(&d->sourceMime, mimeType_Gempub);[m [31m- }[m [32m+[m[32m if (open_Gempub(gempub, &d->sourceContent)) {[m [32m+[m[32m setBaseUrl_Gempub(gempub, d->mod.url);[m [32m+[m[32m setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));[m [32m+[m[32m setCStr_String(&d->sourceMime, mimeType_Gempub);[m d->sourceGempub = gempub;[m }[m else {[m delete_Gempub(gempub);[m }[m }[m [31m- }[m [31m- if (d->sourceGempub) {[m [31m- if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {[m [31m- if (!isRemote_Gempub(d->sourceGempub)) {[m [31m- iArray *items = collectNew_Array(sizeof(iMenuItem));[m [31m- pushBack_Array([m [31m- items,[m [31m- &(iMenuItem){ book_Icon " ${gempub.cover.view}",[m [31m- 0,[m [31m- 0,[m [31m- format_CStr("!open url:%s",[m [31m- cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });[m [31m- if (navSize_Gempub(d->sourceGempub) > 0) {[m [31m- pushBack_Array([m [31m- items,[m [31m- &(iMenuItem){[m [31m- format_CStr(forwardArrow_Icon " %s",[m [31m- cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),[m [31m- SDLK_RIGHT,[m [31m- 0,[m [31m- format_CStr("!open url:%s",[m [31m- cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });[m [31m- }[m [31m- makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));[m [32m+[m[32m if (!d->sourceGempub) {[m [32m+[m[32m const iString *localPath = collect_String(localFilePathFromUrl_String(d->mod.url));[m [32m+[m[32m iBool isInside = iFalse;[m [32m+[m[32m if (localPath && !fileExists_FileInfo(localPath)) {[m [32m+[m[32m /* This URL may refer to a file inside the archive. */[m [32m+[m[32m localPath = findContainerArchive_Path(localPath);[m [32m+[m[32m isInside = iTrue;[m }[m [31m- else {[m [31m- makeFooterButtons_DocumentWidget_([m [31m- d,[m [31m- (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",[m [31m- SDLK_s,[m [31m- KMOD_PRIMARY | KMOD_SHIFT,[m [31m- "document.save open:1" },[m [31m- { download_Icon " " saveToDownloads_Label,[m [31m- SDLK_s,[m [31m- KMOD_PRIMARY,[m [31m- "document.save" } },[m [31m- 2);[m [31m- }[m [31m- if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {[m [31m- redoLayout_GmDocument(d->doc);[m [31m- updateVisible_DocumentWidget_(d);[m [31m- invalidate_DocumentWidget_(d);[m [32m+[m[32m if (localPath && equal_CStr(mediaType_Path(localPath), mimeType_Gempub)) {[m [32m+[m[32m iGempub *gempub = new_Gempub();[m [32m+[m[32m if (openFile_Gempub(gempub, localPath)) {[m [32m+[m[32m setBaseUrl_Gempub(gempub, collect_String(makeFileUrl_String(localPath)));[m [32m+[m[32m if (!isInside) {[m [32m+[m[32m setSource_DocumentWidget(d, collect_String(coverPageSource_Gempub(gempub)));[m [32m+[m[32m setCStr_String(&d->sourceMime, mimeType_Gempub);[m [32m+[m[32m }[m [32m+[m[32m d->sourceGempub = gempub;[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m delete_Gempub(gempub);[m [32m+[m[32m }[m }[m }[m [31m- else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {[m [31m- makeFooterButtons_DocumentWidget_([m [31m- d,[m [31m- (iMenuItem[]){ { format_CStr(book_Icon " %s",[m [31m- cstr_String(property_Gempub(d->sourceGempub,[m [31m- title_GempubProperty))),[m [31m- SDLK_LEFT,[m [31m- 0,[m [31m- format_CStr("!open url:%s",[m [31m- cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },[m [31m- 1);[m [31m- }[m [31m- else {[m [31m- /* Navigation buttons. */[m [31m- iArray *items = collectNew_Array(sizeof(iMenuItem));[m [31m- const size_t navIndex = navIndex_Gempub(d->sourceGempub, d->mod.url);[m [31m- if (navIndex != iInvalidPos) {[m [31m- if (navIndex < navSize_Gempub(d->sourceGempub) - 1) {[m [32m+[m[32m if (d->sourceGempub) {[m [32m+[m[32m if (equal_String(d->mod.url, coverPageUrl_Gempub(d->sourceGempub))) {[m [32m+[m[32m if (!isRemote_Gempub(d->sourceGempub)) {[m [32m+[m[32m iArray *items = collectNew_Array(sizeof(iMenuItem));[m pushBack_Array([m items,[m [31m- &(iMenuItem){[m [31m- format_CStr(forwardArrow_Icon " %s",[m [31m- cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex + 1))),[m [31m- SDLK_RIGHT,[m [31m- 0,[m [31m- format_CStr("!open url:%s",[m [31m- cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex + 1))) });[m [32m+[m[32m &(iMenuItem){ book_Icon " ${gempub.cover.view}",[m [32m+[m[32m 0,[m [32m+[m[32m 0,[m [32m+[m[32m format_CStr("!open url:%s",[m [32m+[m[32m cstr_String(indexPageUrl_Gempub(d->sourceGempub))) });[m [32m+[m[32m if (navSize_Gempub(d->sourceGempub) > 0) {[m [32m+[m[32m pushBack_Array([m [32m+[m[32m items,[m [32m+[m[32m &(iMenuItem){[m [32m+[m[32m format_CStr(forwardArrow_Icon " %s",[m [32m+[m[32m cstr_String(navLinkLabel_Gempub(d->sourceGempub, 0))),[m [32m+[m[32m SDLK_RIGHT,[m [32m+[m[32m 0,[m [32m+[m[32m format_CStr("!open url:%s",[m [32m+[m[32m cstr_String(navLinkUrl_Gempub(d->sourceGempub, 0))) });[m [32m+[m[32m }[m [32m+[m[32m makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items));[m }[m [31m- if (navIndex > 0) {[m [31m- pushBack_Array([m [31m- items,[m [31m- &(iMenuItem){[m [31m- format_CStr(backArrow_Icon " %s",[m [31m- cstr_String(navLinkLabel_Gempub(d->sourceGempub, navIndex - 1))),[m [31m- SDLK_LEFT,[m [31m- 0,[m [31m- format_CStr("!open url:%s",[m [31m- cstr_String(navLinkUrl_Gempub(d->sourceGempub, navIndex - 1))) });[m [32m+[m[32m else {[m [32m+[m[32m makeFooterButtons_DocumentWidget_([m [32m+[m[32m d,[m [32m+[m[32m (iMenuItem[]){ { book_Icon " ${menu.save.downloads.open}",[m [32m+[m[32m SDLK_s,[m [32m+[m[32m KMOD_PRIMARY | KMOD_SHIFT,[m [32m+[m[32m "document.save open:1" },[m [32m+[m[32m { download_Icon " " saveToDownloads_Label,[m [32m+[m[32m SDLK_s,[m [32m+[m[32m KMOD_PRIMARY,[m [32m+[m[32m "document.save" } },[m [32m+[m[32m 2);[m }[m [31m- else if (!equalCase_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {[m [31m- pushBack_Array([m [31m- items,[m [31m- &(iMenuItem){[m [31m- format_CStr(book_Icon " %s",[m [31m- cstr_String(property_Gempub(d->sourceGempub, title_GempubProperty))),[m [31m- SDLK_LEFT,[m [31m- 0,[m [31m- format_CStr("!open url:%s",[m [31m- cstr_String(coverPageUrl_Gempub(d->sourceGempub))) });[m [32m+[m[32m if (preloadCoverImage_Gempub(d->sourceGempub, d->doc)) {[m [32m+[m[32m redoLayout_GmDocument(d->doc);[m [32m+[m[32m updateVisible_DocumentWidget_(d);[m [32m+[m[32m invalidate_DocumentWidget_(d);[m }[m }[m [31m- if (!isEmpty_Array(items)) {[m [31m- makeFooterButtons_DocumentWidget_(d, constData_Array(items), size_Array(items)); [m [32m+[m[32m else if (equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {[m [32m+[m[32m makeFooterButtons_DocumentWidget_([m [32m+[m[32m d,[m [32m+[m[32m (iMenuItem[]){ { format_CStr(book_Icon " %s",[m [32m+[m[32m cstr_String(property_Gempub(d->sourceGempub,[m [32m+[m[32m title_GempubProperty))),[m [32m+[m[32m SDLK_LEFT,[m [32m+[m[32m 0,[m [32m+[m[32m format_CStr("!open url:%s",[m [32m+[m[32m cstr_String(coverPageUrl_Gempub(d->sourceGempub))) } },[m [32m+[m[32m 1);[m }[m [31m- }[m [31m- if (!isCached && prefs_App()->pinSplit &&[m [31m- equal_String(d->mod.url, indexPageUrl_Gempub(d->sourceGempub))) {[m [31m- const iString *navStart = navStartLinkUrl_Gempub(d->sourceGempub);[m [31m- if (navStart) {[m [31m- iWindow *win = get_Window();[m [31m- /* Auto-split to show index and the first navigation link. */[m [31m- if (numRoots_Window(win) == 2) {[m [31m- /* This document is showing the index page. */[m [31m- iRoot *other = otherRoot_Window(win, w->root);[m [31m- postCommandf_Root(other, "open url:%s", cstr_String(navStart));[m [31m- if (prefs_App()->pinSplit == 1 && w->root == win->roots[1]) {[m [31m- /* On the wrong side. */[m
(truncated output; full size was 131.02 KB)
text/gemini; charset=utf-8
This content has been proxied by September (ba2dc).