=> b35539d9181bbef06da4fe7f61e55cad822df756
[1mdiff --git a/.gitignore b/.gitignore[m [1mindex 05eba91e..9ef6983d 100644[m [1m--- a/.gitignore[m [1m+++ b/.gitignore[m [36m@@ -4,5 +4,4 @@[m build-*[m /.vsbuild[m /.vscode[m [31m-[m [31m-[m [32m+[m[32m/app[m [1mdiff --git a/CMakeLists.txt b/CMakeLists.txt[m [1mindex 9489f5a9..e13fc2d5 100644[m [1m--- a/CMakeLists.txt[m [1m+++ b/CMakeLists.txt[m [36m@@ -18,7 +18,7 @@[m cmake_minimum_required (VERSION 3.9)[m [m project (Lagrange[m [31m- VERSION 1.1.4[m [32m+[m[32m VERSION 1.2.0[m DESCRIPTION "A Beautiful Gemini Client"[m LANGUAGES C[m )[m [36m@@ -33,24 +33,11 @@[m [moption (ENABLE_RELATIVE_EMBED "Resources should always be found via relative p[m option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF)[m option (ENABLE_IDLE_SLEEP "While idle, sleep in the main thread instead of waiting for events" ON)[m option (ENABLE_DOWNLOAD_EDIT "Allow changing the Downloads directory" ON)[m [32m+[m[32moption (ENABLE_CUSTOM_FRAME "Draw a custom window frame (Windows)" OFF)[m [m include (BuildType.cmake)[m include (res/Embed.cmake)[m [31m-if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt)[m [31m- set (INSTALL_THE_FOUNDATION YES)[m [31m- find_package (the_Foundation REQUIRED)[m [31m-else ()[m [31m- set (INSTALL_THE_FOUNDATION NO)[m [31m- set (TFDN_STATIC_LIBRARY ON CACHE BOOL "")[m [31m- set (TFDN_ENABLE_INSTALL OFF CACHE BOOL "")[m [31m- set (TFDN_ENABLE_TESTS OFF CACHE BOOL "")[m [31m- set (TFDN_ENABLE_WEBREQUEST OFF CACHE BOOL "")[m [31m- add_subdirectory (lib/the_Foundation)[m [31m- add_library (the_Foundation::the_Foundation ALIAS the_Foundation)[m [31m-endif ()[m [31m-find_package (PkgConfig REQUIRED)[m [31m-pkg_check_modules (SDL2 REQUIRED sdl2)[m [31m-pkg_check_modules (MPG123 IMPORTED_TARGET libmpg123)[m [32m+[m[32minclude (Depends.cmake)[m [m # Embedded resources are written to a generated source file.[m message (STATUS "Preparing embedded resources...")[m [36m@@ -83,7 +70,7 @@[m [mset (EMBED_RESOURCES[m res/fonts/SourceSansPro-Bold.ttf[m res/fonts/Symbola.ttf[m )[m [31m-if (UNIX AND NOT APPLE)[m [32m+[m[32mif ((UNIX AND NOT APPLE) OR MSYS)[m list (APPEND EMBED_RESOURCES res/lagrange-64.png)[m endif ()[m embed_make (${EMBED_RESOURCES})[m [36m@@ -122,6 +109,7 @@[m [mset (SOURCES[m src/prefs.c[m src/prefs.h[m src/stb_image.h[m [32m+[m[32m src/stb_image_resize.h[m src/stb_truetype.h[m src/visited.c[m src/visited.h[m [36m@@ -154,14 +142,16 @@[m [mset (SOURCES[m src/ui/metrics.h[m src/ui/paint.c[m src/ui/paint.h[m [31m- src/ui/playerui.c[m [31m- src/ui/playerui.h[m [32m+[m[32m src/ui/mediaui.c[m [32m+[m[32m src/ui/mediaui.h[m src/ui/scrollwidget.c[m src/ui/scrollwidget.h[m src/ui/sidebarwidget.c[m src/ui/sidebarwidget.h[m src/ui/text.c[m src/ui/text.h[m [32m+[m[32m src/ui/touch.c[m [32m+[m[32m src/ui/touch.h[m src/ui/util.c[m src/ui/util.h[m src/ui/visbuf.c[m [36m@@ -184,7 +174,18 @@[m [mset (SOURCES[m ${CMAKE_CURRENT_BINARY_DIR}/embedded.h[m )[m if (IOS)[m [32m+[m[32m add_definitions (-DiPlatformAppleMobile=1)[m [32m+[m[32m list (APPEND SOURCES[m [32m+[m[32m src/ios.m[m [32m+[m[32m src/ios.h[m [32m+[m[32m app/Images.xcassets[m [32m+[m[32m res/LaunchScreen.storyboard[m [32m+[m[32m )[m [32m+[m[32m set_source_files_properties(app/Images.xcassets PROPERTIES[m [32m+[m[32m MACOSX_PACKAGE_LOCATION Resources[m [32m+[m[32m )[m elseif (APPLE)[m [32m+[m[32m add_definitions (-DiPlatformAppleDesktop=1)[m list (APPEND SOURCES src/macos.m src/macos.h)[m list (APPEND RESOURCES "res/Lagrange.icns")[m endif ()[m [36m@@ -231,6 +232,9 @@[m [mendif ()[m if (ENABLE_DOWNLOAD_EDIT)[m target_compile_definitions (app PUBLIC LAGRANGE_DOWNLOAD_EDIT=1)[m endif ()[m [32m+[m[32mif (ENABLE_CUSTOM_FRAME AND MSYS)[m [32m+[m[32m target_compile_definitions (app PUBLIC LAGRANGE_CUSTOM_FRAME=1)[m [32m+[m[32mendif ()[m target_link_libraries (app PUBLIC the_Foundation::the_Foundation)[m target_link_libraries (app PUBLIC ${SDL2_LDFLAGS})[m if (APPLE)[m [36m@@ -239,13 +243,15 @@[m [mif (APPLE)[m else ()[m target_link_libraries (app PUBLIC "-framework AppKit")[m endif ()[m [31m- if (CMAKE_OSX_DEPLOYMENT_TARGET)[m [32m+[m[32m if (CMAKE_OSX_DEPLOYMENT_TARGET AND NOT IOS)[m target_compile_options (app PUBLIC -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})[m target_link_options (app PUBLIC -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})[m endif ()[m [32m+[m[32m if (SDL2_LIBRARY_DIRS)[m [32m+[m[32m set_property (TARGET app PROPERTY BUILD_RPATH ${SDL2_LIBRARY_DIRS})[m [32m+[m[32m endif ()[m set_target_properties (app PROPERTIES[m OUTPUT_NAME "Lagrange"[m [31m- BUILD_RPATH ${SDL2_LIBRARY_DIRS}[m MACOSX_BUNDLE YES[m MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/res/MacOSXBundleInfo.plist.in"[m MACOSX_BUNDLE_BUNDLE_NAME "Lagrange"[m [36m@@ -258,9 +264,21 @@[m [mif (APPLE)[m MACOSX_BUNDLE_COPYRIGHT "© ${COPYRIGHT_YEAR} Jaakko Keränen"[m XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "fi.skyjake.Lagrange"[m )[m [32m+[m[32m if (IOS)[m [32m+[m[32m set_target_properties (app PROPERTIES[m [32m+[m[32m XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"[m [32m+[m[32m XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon"[m [32m+[m[32m MACOSX_BUNDLE_ICON_FILE "AppIcon"[m [32m+[m[32m )[m [32m+[m[32m endif ()[m [32m+[m[32m if (XCODE_DEVELOPMENT_TEAM)[m [32m+[m[32m set_property (TARGET app PROPERTY[m [32m+[m[32m XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${XCODE_DEVELOPMENT_TEAM}[m [32m+[m[32m )[m [32m+[m[32m endif ()[m endif ()[m if (MSYS)[m [31m- target_link_libraries (app PUBLIC d2d1 uuid) # querying DPI[m [32m+[m[32m target_link_libraries (app PUBLIC d2d1 uuid dwmapi) # querying DPI[m endif ()[m if (UNIX)[m target_link_libraries (app PUBLIC m)[m [1mdiff --git a/Depends-iOS.cmake b/Depends-iOS.cmake[m [1mnew file mode 100644[m [1mindex 00000000..013ee09a[m [1m--- /dev/null[m [1m+++ b/Depends-iOS.cmake[m [36m@@ -0,0 +1,21 @@[m [32m+[m[32mmessage (STATUS "iOS dependency directory: ${IOS_DIR}")[m [32m+[m [32m+[m[32mfind_package (the_Foundation REQUIRED)[m [32m+[m [32m+[m[32mset (SDL2_INCLUDE_DIRS ${IOS_DIR}/include/SDL2)[m [32m+[m[32mset (SDL2_LDFLAGS[m[41m [m [32m+[m[32m ${IOS_DIR}/lib/libSDL2.a[m [32m+[m[32m "-framework AudioToolbox"[m [32m+[m[32m "-framework AVFoundation"[m [32m+[m[32m "-framework CoreAudio"[m [32m+[m[32m "-framework CoreGraphics"[m [32m+[m[32m "-framework CoreHaptics"[m [32m+[m[32m "-framework CoreMotion"[m [32m+[m[32m "-framework Foundation"[m [32m+[m[32m "-framework Foundation"[m [32m+[m[32m "-framework GameController"[m [32m+[m[32m "-framework Metal"[m [32m+[m[32m "-framework OpenGLES"[m [32m+[m[32m "-framework QuartzCore"[m [32m+[m[32m "-framework UIKit"[m [32m+[m[32m)[m [1mdiff --git a/Depends.cmake b/Depends.cmake[m [1mnew file mode 100644[m [1mindex 00000000..b4dacf7c[m [1m--- /dev/null[m [1m+++ b/Depends.cmake[m [36m@@ -0,0 +1,20 @@[m [32m+[m[32mif (IOS)[m [32m+[m[32m include (Depends-iOS.cmake)[m [32m+[m[32m return ()[m [32m+[m[32mendif ()[m [32m+[m [32m+[m[32mif (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt)[m [32m+[m[32m set (INSTALL_THE_FOUNDATION YES)[m [32m+[m[32m find_package (the_Foundation REQUIRED)[m [32m+[m[32melse ()[m [32m+[m[32m set (INSTALL_THE_FOUNDATION NO)[m [32m+[m[32m set (TFDN_STATIC_LIBRARY ON CACHE BOOL "")[m [32m+[m[32m set (TFDN_ENABLE_INSTALL OFF CACHE BOOL "")[m [32m+[m[32m set (TFDN_ENABLE_TESTS OFF CACHE BOOL "")[m [32m+[m[32m set (TFDN_ENABLE_WEBREQUEST OFF CACHE BOOL "")[m [32m+[m[32m add_subdirectory (lib/the_Foundation)[m [32m+[m[32m add_library (the_Foundation::the_Foundation ALIAS the_Foundation)[m [32m+[m[32mendif ()[m [32m+[m[32mfind_package (PkgConfig REQUIRED)[m [32m+[m[32mpkg_check_modules (SDL2 REQUIRED sdl2)[m [32m+[m[32mpkg_check_modules (MPG123 IMPORTED_TARGET libmpg123)[m [1mdiff --git a/res/LaunchScreen.storyboard b/res/LaunchScreen.storyboard[m [1mnew file mode 100644[m [1mindex 00000000..f9a048ed[m [1m--- /dev/null[m [1m+++ b/res/LaunchScreen.storyboard[m [36m@@ -0,0 +1,7 @@[m [32m+[m[32m[m [32m+[m[32m[m [32m+[m[32m [m [1mdiff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in[m [1mindex 1d769768..186e733b 100644[m [1m--- a/res/MacOSXBundleInfo.plist.in[m [1m+++ b/res/MacOSXBundleInfo.plist.in[m [36m@@ -34,6 +34,21 @@[m[m [32m+[m[32m [m [32m+[m[32m[m [32m+[m[32m [m [32m+[m[32m [m NSRequiresAquaSystemAppearance [m[m [32m+[m [32m UISupportedInterfaceOrientations [m [32m+[m [32m[m [32m+[m [32m [m [32m+[m [32mUIInterfaceOrientationPortrait [m [32m+[m [32mUIInterfaceOrientationLandscapeLeft [m [32m+[m [32mUIInterfaceOrientationLandscapeRight [m [32m+[m [32mUISupportedInterfaceOrientations~ipad [m [32m+[m [32m[m [32m+[m [32m [m [32m+[m [32mUIInterfaceOrientationPortrait [m [32m+[m [32mUIInterfaceOrientationPortraitUpsideDown [m [32m+[m [32mUIInterfaceOrientationLandscapeLeft [m [32m+[m [32mUIInterfaceOrientationLandscapeRight [m [32m+[m [32mUILaunchStoryboardName [m [32m+[m [32mLaunchScreen [mCFBundleDocumentTypes [m[m [m [1mdiff --git a/res/about/help.gmi b/res/about/help.gmi[m [1mindex fcdc8239..15485571 100644[m [1m--- a/res/about/help.gmi[m [1m+++ b/res/about/help.gmi[m [36m@@ -6,6 +6,13 @@[m ```[m # Help[m [m [32m+[m[32m## What is Gemini[m [32m+[m [32m+[m[32mGemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with.[m [32m+[m [32m+[m[32m=> gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ[m [32m+[m[32m=> gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and 'text/gemini' specification[m [32m+[m ## What is Lagrange[m [m Lagrange is a GUI client for browsing Geminispace. It offers modern conveniences familiar from web browsers, such as smooth scrolling, inline image viewing, multiple tabs, visual themes, Unicode fonts, bookmarks, history, and page outlines.[m [36m@@ -24,11 +31,12 @@[m [mLike Gemini, Lagrange has been designed with minimalism in mind. It depends on a[m * Sidebar for page outline, managing bookmarks and identities, and viewing history[m * Multiple tabs[m * Identity management — create and use TLS client certificates[m [31m-* Subscribe to Gemini feeds[m [32m+[m[32m* Subscribe to Gemini and Atom feeds[m * Use Gemini pages as a source of bookmarks[m * Light and dark color themes[m * Select and copy text with the mouse[m * Find text on the page[m [32m+[m[32m* Search engine integration[m * Open image links inline on the same page[m * Audio playback: MP3, Ogg Vorbis, WAV[m * Open links via keyboard shortcuts[m [36m@@ -41,13 +49,6 @@[m [mLike Gemini, Lagrange has been designed with minimalism in mind. It depends on a[m * Built-in support for Gopher[m * Use proxy servers for HTTP, Gopher, or Gemini content[m [m [31m-## What is Gemini[m [31m-[m [31m-Gemini is a simple protocol for serving content over the internet. It specifies a Markdown inspired format allowing basic plain text document markup. Compared to HTTP and HTML, Gemini is vastly simpler and easier to work with.[m [31m-[m [31m-=> gemini://gemini.circumlunar.space/docs/faq.gmi Project Gemini FAQ[m [31m-=> gemini://gemini.circumlunar.space/docs/specification.gmi Protocol and 'text/gemini' specification[m [31m-[m ## Why not just use a web browser[m [m Modern web browsers are complex beasts. In fact, they are so complex that one can create a fully functional virtual machine inside one and run another operating system![m [36m@@ -88,6 +89,8 @@[m [mSearch within cached pages is limited to the (small) set of pages that Lagrange[m [m Note that the navigation stack is saved to a file when Lagrange is shut down and restored on the next launch. This means the next time you launch Lagrange, you can still search the contents of past pages. However, navigation stacks are tab-specific, so closing a tab will delete its history as well.[m [m [32m+[m[32mYou can also make online search queries via the URL input field. When a search URL is configured on the "Network" tab of Preferences, text entered in the URL field is passed to the search URL as a query parameter. A search query will only occur when Enter is pressed while the [⇒ Search Query] indicator is visible.[m [32m+[m ### 1.1.2 Links[m [m The type and destination of a link are indicated by the link's icon and color: ➤ links to the same domain, and 🌐 to a different domain. The colors are:[m [36m@@ -127,7 +130,13 @@[m [mYou can give it a try now with the link below. Either hold down ${ALT}, or press[m [m Press ${CTRL+}T to open a new tab, and ${CTRL+}W to close the current tab. Right-clicking on buttons in the tab bar shows a context menu for additional tab-related functions.[m [m [31m-The set of open tabs is restored when you launch Lagrange.[m [32m+[m[32mThe set of open tabs and their full contents are saved when you quit the application and restored when relaunch it.[m [32m+[m [32m+[m[32m### 1.2.1 Auto-reloading[m [32m+[m [32m+[m[32mA tab can be set to auto-reload at given intervals. The setting is remembered until the tab is closed. This is helpful if you keep a periodically updated page open for longer periods of time.[m [32m+[m [32m+[m[32mThe feature can be found in the page context menu: right-click and select "Set Auto-Reload...".[m [m ## 1.3 Sidebars[m [m [36m@@ -149,6 +158,8 @@[m [mBookmarks are listed in alphabetic order in the sidebar. There is no support for[m [m In addition to a title, bookmarks can have tags. Some tags have a special meaning, but you are free to enter whatever you want in the tags field. In quick search results, tags are given extra weight so they appear higher in results.[m [m [32m+[m[32mBy default bookmarks are assigned random icons. You can enter a custom icon for a bookmark in the bookmark edit dialog to make it easier to recognize a particular site at a glance. The icon must be a single Unicode character. It will appear in the bookmark list, tab titles, feed entry list, quick search results, and page top banners.[m [32m+[m ### 1.4.1 Exporting and importing[m [m => about:bookmarks The special page "about:bookmarks" is used for exporting bookmarks out of Lagrange.[m [36m@@ -179,13 +190,15 @@[m [mNote that remote bookmarks are read-only: they cannot be edited or tagged. This[m * "headings" can be used together with "subscribed" to subscribe to new headings instead of Gemini feed links.[m * The "remotesource" tag marks the bookmark as a source of remote bookmarks. All links on the source pages are shown as remote bookmarks in the Bookmarks list.[m * The "remote" tag is used for remote bookmarks. These bookmarks cannot be edited or tagged, but you can make a local duplicate to turn it into a regular bookmark.[m [32m+[m[32m* The "usericon" tag prevents a random icon to be selected for the bookmark. This tag is automatically applied when an icon character is entered in the bookmark editor.[m [m ## 1.5 Subscribing to feeds[m [m [31m-You may be familiar with RSS and Atom XML feeds from the web. Lagrange does _not_ support RSS or Atom, only Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way.[m [31m-[m [32m+[m[32mYou may be familiar with XML-based RSS and Atom feeds from the web. The Gemini equivalent of these is Gemini feeds. A Gemini feed is simply a regular 'text/gemini' page that contains one or more links whose labels are formatted in a particular way. This makes it very easy to write pages that clients can subscribe to.[m => gemini://gemini.circumlunar.space/docs/companion/subscription.gmi See "Subscribing to Gemini pages" for more information.[m [m [32m+[m[32mLagrange supports Gemini and Atom feed subscriptions. Atom feeds are automatically translated to the Gemini feed format so they can be viewed and subscribed to like a normal 'text/gemini' page. RSS feeds are not supported.[m [32m+[m Subscriptions are managed via bookmarks. When you subscribe to a feed page, a bookmark is created and the special "subscribed" tag is applied on it. In the Bookmarks list, this is indicated by a ★ icon. There is no other difference between normal bookmarks and feed subscriptions — you may tag any bookmark as "subscribed" and Lagrange will look through it for feed-style links. The bookmark title is used as the feed title. This defaults to the top heading of the feed index page, but you can edit it to suit your needs.[m [m Feeds are refreshed periodically while Lagrange is running, and also immediately after launching if it has been a while since the previous refresh. You may also manually refresh all feeds via the menus or by pressing ${SHIFT+}${CTRL+}R.[m [36m@@ -259,6 +272,8 @@[m [mThere are four different color schemes for the UI: two for dark mode, and two fo[m [m On macOS, Lagrange will automatically switch between dark and light modes if the "Use system theme" setting is enabled. On other platforms you'll need to switch manually.[m [m [32m+[m[32mOn Windows, an option is provided to use a custom window frame. This provides a more consistent visual style for the application. However, the custom frame overrides default behaviors of the window, so if you are a Windows power user expect it to not support all the special interactions. For example, when pressing the Windows+Left/Right key, the window is resized to take over one half of the screen. Normally, Windows would then prompt to select another window to fill the rest of the screen, but this does not happen with the custom window frame.[m [32m+[m Options for wide window sizes:[m [m * Site icon: If there is room on the left side of the page text, the site icon appears there along with the top level heading. This allows one to keep better track of the current reading position in long documents.[m [36m@@ -281,12 +296,14 @@[m [mThe "Style" tab of the Preferences dialog lets you customize the appearance and[m [m The fonts for headings and other text are selected separately. This way one can achieve a greater number of style variations. Also, headings that are in a different font are more visually distinct and thus easier for one's eyes to scan.[m [m [31m-There are two sans-serif and two serif fonts:[m [32m+[m[32mThere are six fonts available:[m [m * "Nunito" is a friendly rounded sans-serif font.[m * "Fira Sans" is a bolder, narrower, and more angular sans-serif font.[m * "Literata" is a heavy and serious serif font.[m * "Tinos" is a lighter serif font.[m [32m+[m[32m* "Source Sans Pro" is the Lagrange UI font.[m [32m+[m[32m* "Iosevka" is the monospace font.[m [m Other style options:[m [m [36m@@ -387,7 +404,7 @@[m [mAny output that does not follow this format is considered to mean that the hook[m ## 4.2 mimehooks.txt syntax[m [m Like other Lagrange configuration lines, mimehooks.txt has a simple line-oriented syntax. Lagrange must be restarted for changes to the configuration file to take effect.[m [31m-[m [32m+[m[32m≈[m Each hook is specified as three lines:[m * A human-readable label (for reporting to the user)[m * MIME type/parameter regular expression[m [1mdiff --git a/res/about/version.gmi b/res/about/version.gmi[m [1mindex 7a08503b..8bd30dfb 100644[m [1m--- a/res/about/version.gmi[m [1m+++ b/res/about/version.gmi[m [36m@@ -6,6 +6,49 @@[m ```[m # Release notes[m [m [32m+[m[32m## 1.2[m [32m+[m [32m+[m[32mNew features:[m [32m+[m[32m* Atom feed subscriptions: Atom XML documents are automatically converted to Gemini feed index pages. This is a built-in version of the Atom-to-Gemini example on the Help page.[m [32m+[m[32m* Inline downloads: right-click on any link that is openable inside Lagrange and select "Download Linked File".[m [32m+[m[32m* Editable bookmark icons: use your favorite Unicode character as the symbol to represent a bookmarked site.[m [32m+[m[32m* Searching via URL field: non-URL text entered in the field is passed onto the configured search query URL (Preferences > Network). An indicator is shown if a query will take place.[m [32m+[m[32m* Tab auto-reloading: configure a reloading interval using the page context menu ("Set Auto-Reload..."). Auto-reloading is part of the persistent state of the tab.[m [32m+[m[32m* "Iosevka" and "Source Sans Pro" (the UI font) can be used as heading and body fonts.[m [32m+[m[32m* User preference for aligning all pages to the top of the window.[m [32m+[m[32m* Keybinding (F11) for toggling fullscreen mode. On macOS, the shortcut is ⌃⌘F as before.[m [32m+[m[32m* Keybinding for finding text on page.[m [32m+[m [32m+[m[32mUI design:[m [32m+[m[32m* Enhanced navbar: adjusted spacing, URL field has a maximum width, tab titles have less pronounced borders.[m [32m+[m[32m* Improved sidebar appearance: bold subheadings, larger feed icons, adjusted spacing, background color.[m [32m+[m[32m* Font consistency: all UI elements use the same font (i.e., no more monospace input fields).[m [32m+[m[32m* Added setting for UI accent color (teal, orange).[m [32m+[m[32m* General fine-tuning of the color palette.[m [32m+[m[32m* Dialog buttons are aligned to the right edge, leaving room for additional action buttons on the left.[m [32m+[m[32m* Page Information button is embedded in the URL field.[m [32m+[m[32m* Page Information dialog is attached to its button.[m [32m+[m[32m* Site icons use a different color in tab titles for visual distinction.[m [32m+[m[32m* Fade background behind modal dialogs.[m [32m+[m[32m* Responsive page margins.[m [32m+[m[32m* Windows: Added a custom window frame to replace the default Windows one. This looks nicer but does not behave exactly like a native window frame. Added a setting to Preferences for switching back to the default frame.[m [32m+[m [32m+[m[32mOther changes:[m [32m+[m[32m* Help is opened on first run instead of the "About Lagrange" page to make it easier to discover important Gemini links like the FAQ.[m [32m+[m[32m* "Go to Root" respects a user name found in the URL. One can still "Go to Parent" to get above the user level.[m [32m+[m[32m* Feed entries are sorted by refresh time if they are published on the same date.[m [32m+[m[32m* Don't show future-dated feed entries in Feeds.[m [32m+[m[32m* Middle-clicking on links: open new tab in background or foreground depending on the Shift key.[m [32m+[m[32m* Shift+Insert can be used for pasting clipboard contents into input fields.[m [32m+[m[32m* Removed a strange violet-on-green color theme pairing.[m [32m+[m [32m+[m[32mBug fixes:[m [32m+[m[32m* Fixed text prompt dialogs closing and accepting the entered text when switching focus away from the app.[m [32m+[m[32m* Scroll position remains fixed while horizontally resizing the window or sidebars.[m [32m+[m[32m* Fixed a crash when opening the audio player menu.[m [32m+[m[32m* Fixed Gopher requests that were using URL (percent) encoded characters.[m [32m+[m[32m* Windows: Fixed a flash of white when the window is first opened.[m [32m+[m ## 1.1.4[m * Fixed feed entry highlight/read status issue in the sidebar.[m * Fixed Gopher menu links that contain spaces.[m [36m@@ -251,7 +294,6 @@[m [m ## 0.5[m * Added MP3 support in the audio player (using mpg123).[m [31m-=> https://mpg123.org/ mpg123: MPEG audio player and decoder library[m * Added volume control in the audio player.[m * Metadata in Vorbis and MP3 audio content (title, artist, etc.) is shown in the audio player menu.[m * Added new serif fonts: EB Garamond and Literata.[m [36m@@ -261,6 +303,7 @@[m * Open links in new tab with middle mouse button.[m * Fixed failure to find resources when launching via PATH.[m * Fixed color saturation setting not affecting the default color theme.[m [32m+[m[32m=> https://mpg123.org/ mpg123: MPEG audio player and decoder library[m [m ## 0.4.1[m * Set keyboard focus to URL input field after opening a new tab.[m [36m@@ -272,8 +315,7 @@[m * Windows: All binaries are signed.[m [m ## 0.4[m [31m-* Added audio playback with support for streaming. Supported audio formats in this release are WAV (PCM, mono/stereo, 8/16/24/32 integer/float) and Ogg Vorbis. Shoutout to Sean Barrett et al. for stb_vorbis:[m [31m-=> https://github.com/nothings/stb stb: single-file public domain libraries for C/C++[m [32m+[m[32m* Added audio playback with support for streaming. Supported audio formats in this release are WAV (PCM, mono/stereo, 8/16/24/32 integer/float) and Ogg Vorbis. Shoutout to Sean Barrett et al. for stb_vorbis.[m * Added inline audio player that works like inline images. Clicking on an audio link opens the audio player below the link (works for URLs that have file extension .wav/.ogg).[m * Visual fine-tuning: increased Fira Sans line spacing; list bullets use an accent color; adjusted accent colors in the light mode palette.[m * Sidebar has a maximum width — the document must remain visible.[m [36m@@ -281,6 +323,7 @@[m * macOS: Use OpenGL on 10.13 for potentially better compatibility.[m * Fixed a memory leak when closing tabs.[m * Fixed unnecessary continual window redrawing related to the scrollbar hover outline.[m [32m+[m[32m=> https://github.com/nothings/stb stb: single-file public domain libraries for C/C++[m [m ## 0.3[m * Added style customization.[m [1mdiff --git a/res/fi.skyjake.Lagrange.appdata.xml b/res/fi.skyjake.Lagrange.appdata.xml[m [1mindex 689a609a..355bf18d 100644[m [1m--- a/res/fi.skyjake.Lagrange.appdata.xml[m [1m+++ b/res/fi.skyjake.Lagrange.appdata.xml[m [36m@@ -45,6 +45,26 @@[m jaakko.keranen@iki.fi [m [m[m [32m+[m[32m [m [32m+[m[32m [m[m [32m+[m[32m [m [32m+[m[32mThis is a major feature update that also has a number of user[m[41m [m [32m+[m[32m interface design changes.[m [32m+[m[32m
[m [32m+[m[32mNew features include viewing and subscribing to Atom feeds,[m [32m+[m[32m downloading any link as a file, editable bookmark icons,[m [32m+[m[32m search engine integration, tab auto-reloading, fullscreen mode,[m [32m+[m[32m and new font options for page content.[m [32m+[m[32m
[m [32m+[m[32mUI enhancements include improved navbar and sidebar appearance,[m [32m+[m[32m setting for UI accent color, and placement of dialog[m [32m+[m[32m buttons.[m [32m+[m[32m
[m [32m+[m[32mThe full release notes can be viewed inside the app by opening[m [32m+[m[32m the "about:version" page.[m [32m+[m[32m
[m [32m+[m[32mhttps://github.com/skyjake/lagrange/releases/tag/v1.2.0 [m[41m [m [32m+[m[32m[m [m Bug fixes:
[m [1mdiff --git a/src/app.c b/src/app.c[m [1mindex f5833c95..17b51dd4 100644[m [1m--- a/src/app.c[m [1m+++ b/src/app.c[m [36m@@ -61,9 +61,12 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m #include[m #include [m [m [31m-#if defined (iPlatformApple) && !defined (iPlatformIOS)[m [32m+[m[32m#if defined (iPlatformAppleDesktop)[m # include "macos.h"[m #endif[m [32m+[m[32m#if defined (iPlatformAppleMobile)[m [32m+[m[32m# include "ios.h"[m [32m+[m[32m#endif[m #if defined (iPlatformMsys)[m # include "win32.h"[m #endif[m [36m@@ -73,10 +76,14 @@[m [mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [m iDeclareType(App)[m [m [31m-#if defined (iPlatformApple)[m [32m+[m[32m#if defined (iPlatformAppleDesktop)[m #define EMB_BIN "../../Resources/resources.lgr"[m static const char *defaultDataDir_App_ = "~/Library/Application Support/fi.skyjake.Lagrange";[m #endif[m [32m+[m[32m#if defined (iPlatformAppleMobile)[m [32m+[m[32m#define EMB_BIN "../../Resources/resources.lgr"[m [32m+[m[32mstatic const char *defaultDataDir_App_ = "~/Library/Application Support";[m [32m+[m[32m#endif[m #if defined (iPlatformMsys)[m #define EMB_BIN "../resources.lgr"[m static const char *defaultDataDir_App_ = "~/AppData/Roaming/fi.skyjake.Lagrange";[m [36m@@ -119,6 +126,7 @@[m [mstruct Impl_App {[m iStringList *launchCommands;[m iBool isFinishedLaunching;[m iTime lastDropTime; /* for detecting drops of multiple items */[m [32m+[m[32m int autoReloadTimer;[m /* Preferences: */[m iBool commandEcho; /* --echo */[m iBool forceSoftwareRender; /* --sw */[m [36m@@ -154,33 +162,45 @@[m [mstatic iString *serializePrefs_App_(const iApp *d) {[m iString *str = new_String();[m const iSidebarWidget *sidebar = findWidget_App("sidebar");[m const iSidebarWidget *sidebar2 = findWidget_App("sidebar2");[m [32m+[m[32m#if defined (LAGRANGE_CUSTOM_FRAME)[m [32m+[m[32m appendFormat_String(str, "customframe arg:%d\n", d->prefs.customFrame);[m [32m+[m[32m#endif[m appendFormat_String(str, "window.retain arg:%d\n", d->prefs.retainWindowSize);[m if (d->prefs.retainWindowSize) {[m [31m- const iBool isMaximized = (SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MAXIMIZED) != 0;[m int w, h, x, y;[m [31m- x = d->window->lastRect.pos.x;[m [31m- y = d->window->lastRect.pos.y;[m [31m- w = d->window->lastRect.size.x;[m [31m- h = d->window->lastRect.size.y;[m [32m+[m[32m x = d->window->place.normalRect.pos.x;[m [32m+[m[32m y = d->window->place.normalRect.pos.y;[m [32m+[m[32m w = d->window->place.normalRect.size.x;[m [32m+[m[32m h = d->window->place.normalRect.size.y;[m appendFormat_String(str, "window.setrect width:%d height:%d coord:%d %d\n", w, h, x, y);[m appendFormat_String(str, "sidebar.width arg:%d\n", width_SidebarWidget(sidebar));[m appendFormat_String(str, "sidebar2.width arg:%d\n", width_SidebarWidget(sidebar2));[m /* On macOS, maximization should be applied at creation time or the window will take[m a moment to animate to its maximized size. */[m [31m-#if !defined (iPlatformApple)[m [31m- if (isMaximized) {[m [32m+[m[32m#if defined (LAGRANGE_CUSTOM_FRAME)[m [32m+[m[32m if (snap_Window(d->window)) {[m [32m+[m[32m if (~SDL_GetWindowFlags(d->window->win) & SDL_WINDOW_MINIMIZED) {[m [32m+[m[32m /* Save the actual visible window position, too, because snapped windows may[m [32m+[m[32m still be resized/moved without affecting normalRect. */[m [32m+[m[32m SDL_GetWindowPosition(d->window->win, &x, &y);[m [32m+[m[32m SDL_GetWindowSize(d->window->win, &w, &h);[m [32m+[m[32m appendFormat_String([m [32m+[m[32m str, "~window.setrect snap:%d width:%d height:%d coord:%d %d\n",[m [32m+[m[32m snap_Window(d->window), w, h, x, y);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m#elif !defined (iPlatformApple)[m [32m+[m[32m if (snap_Window(d->window) == maximized_WindowSnap) {[m appendFormat_String(str, "~window.maximize\n");[m }[m [31m-#else[m [31m- iUnused(isMaximized);[m #endif[m }[m /* Sidebars. */ {[m [31m- if (isVisible_Widget(sidebar)) {[m [32m+[m[32m if (isVisible_Widget(sidebar) && deviceType_App() != phone_AppDeviceType) {[m appendCStr_String(str, "sidebar.toggle\n");[m }[m appendFormat_String(str, "sidebar.mode arg:%d\n", mode_SidebarWidget(sidebar));[m [31m- if (isVisible_Widget(sidebar2)) {[m [32m+[m[32m if (isVisible_Widget(sidebar2) && deviceType_App() != phone_AppDeviceType) {[m appendCStr_String(str, "sidebar2.toggle\n");[m }[m appendFormat_String(str, "sidebar2.mode arg:%d\n", mode_SidebarWidget(sidebar2));[m [36m@@ -199,9 +219,11 @@[m [mstatic iString *serializePrefs_App_(const iApp *d) {[m appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth);[m appendFormat_String(str, "prefs.biglede.changed arg:%d\n", d->prefs.bigFirstParagraph);[m appendFormat_String(str, "prefs.sideicon.changed arg:%d\n", d->prefs.sideIcon);[m [32m+[m[32m appendFormat_String(str, "prefs.centershort.changed arg:%d\n", d->prefs.centerShortDocs);[m appendFormat_String(str, "quoteicon.set arg:%d\n", d->prefs.quoteIcon ? 1 : 0);[m appendFormat_String(str, "prefs.hoverlink.changed arg:%d\n", d->prefs.hoverLink);[m appendFormat_String(str, "theme.set arg:%d auto:1\n", d->prefs.theme);[m [32m+[m[32m appendFormat_String(str, "accent.set arg:%d\n", d->prefs.accent);[m appendFormat_String(str, "ostheme arg:%d\n", d->prefs.useSystemTheme);[m appendFormat_String(str, "doctheme.dark.set arg:%d\n", d->prefs.docThemeDark);[m appendFormat_String(str, "doctheme.light.set arg:%d\n", d->prefs.docThemeLight);[m [36m@@ -210,6 +232,7 @@[m [mstatic iString *serializePrefs_App_(const iApp *d) {[m appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->prefs.gopherProxy));[m appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->prefs.httpProxy));[m appendFormat_String(str, "downloads path:%s\n", cstr_String(&d->prefs.downloadDir));[m [32m+[m[32m appendFormat_String(str, "searchurl address:%s\n", cstr_String(&d->prefs.searchUrl));[m return str;[m }[m [m [36m@@ -270,7 +293,10 @@[m [mstatic void loadPrefs_App_(iApp *d) {[m if (equal_Command(cmd, "uiscale")) {[m setUiScale_Window(get_Window(), argf_Command(cmd));[m }[m [31m- else if (equal_Command(cmd, "window.setrect")) {[m [32m+[m[32m else if (equal_Command(cmd, "customframe")) {[m [32m+[m[32m d->prefs.customFrame = arg_Command(cmd);[m [32m+[m[32m }[m [32m+[m[32m else if (equal_Command(cmd, "window.setrect") && !argLabel_Command(cmd, "snap")) {[m const iInt2 pos = coord_Command(cmd);[m d->initialWindowRect = init_Rect([m pos.x, pos.y, argLabel_Command(cmd, "width"), argLabel_Command(cmd, "height"));[m [36m@@ -285,6 +311,9 @@[m [mstatic void loadPrefs_App_(iApp *d) {[m else {[m /* default preference values */[m }[m [32m+[m[32m#if !defined (LAGRANGE_CUSTOM_FRAME)[m [32m+[m[32m d->prefs.customFrame = iFalse;[m [32m+[m[32m#endif[m iRelease(f);[m }[m [m [36m@@ -370,6 +399,9 @@[m [mstatic void saveState_App_(const iApp *d) {[m serializeState_DocumentWidget(i.object, stream_File(f));[m }[m }[m [32m+[m[32m else {[m [32m+[m[32m fprintf(stderr, "[App] failed to save state: %s\n", strerror(errno));[m [32m+[m[32m }[m iRelease(f);[m }[m [m [36m@@ -384,6 +416,12 @@[m [mstatic uint32_t checkAsleep_App_(uint32_t interval, void *param) {[m }[m #endif[m [m [32m+[m[32mstatic uint32_t postAutoReloadCommand_App_(uint32_t interval, void *param) {[m [32m+[m[32m iUnused(param);[m [32m+[m[32m postCommand_App("document.autoreload");[m [32m+[m[32m return interval;[m [32m+[m[32m}[m [32m+[m static void init_App_(iApp *d, int argc, char **argv) {[m init_CommandLine(&d->args, argc, argv);[m /* Where was the app started from? We ask SDL first because the command line alone is[m [36m@@ -438,8 +476,11 @@[m [mstatic void init_App_(iApp *d, int argc, char **argv) {[m d->lastEventTime = 0;[m d->sleepTimer = SDL_AddTimer(1000, checkAsleep_App_, d);[m #endif[m [31m-#if defined (iPlatformApple)[m [32m+[m[32m#if defined (iPlatformAppleDesktop)[m setupApplication_MacOS();[m [32m+[m[32m#if defined (iPlatformAppleMobile)[m [32m+[m[32m#endif[m [32m+[m[32m setupApplication_iOS();[m #endif[m init_Keys();[m loadPrefs_App_(d);[m [36m@@ -476,9 +517,11 @@[m [mstatic void init_App_(iApp *d, int argc, char **argv) {[m /* Widget state init. */[m processEvents_App(postedEventsOnly_AppEventMode);[m if (!loadState_App_(d)) {[m [31m- postCommand_App("navigate.home");[m [32m+[m[32m postCommand_App("open url:about:help");[m }[m postCommand_App("window.unfreeze");[m [32m+[m[32m d->autoReloadTimer = SDL_AddTimer(60 * 1000, postAutoReloadCommand_App_, NULL);[m [32m+[m[32m postCommand_App("document.autoreload");[m d->isFinishedLaunching = iTrue;[m /* Run any commands that were pending completion of launch. */ {[m iForEach(StringList, i, d->launchCommands) {[m [36m@@ -539,6 +582,61 @@[m [mconst iString *downloadDir_App(void) {[m return collect_String(cleaned_Path(&app_.prefs.downloadDir));[m }[m [m [32m+[m[32mconst iString *downloadPathForUrl_App(const iString *url, const iString *mime) {[m [32m+[m[32m /* Figure out a file name from the URL. */[m [32m+[m[32m iUrl parts;[m [32m+[m[32m init_Url(&parts, url);[m [32m+[m[32m while (startsWith_Rangecc(parts.path, "/")) {[m [32m+[m[32m parts.path.start++;[m [32m+[m[32m }[m [32m+[m[32m while (endsWith_Rangecc(parts.path, "/")) {[m [32m+[m[32m parts.path.end--;[m [32m+[m[32m }[m [32m+[m[32m iString *name = collectNewCStr_String("pagecontent");[m [32m+[m[32m if (isEmpty_Range(&parts.path)) {[m [32m+[m[32m if (!isEmpty_Range(&parts.host)) {[m [32m+[m[32m setRange_String(name, parts.host);[m [32m+[m[32m replace_Block(&name->chars, '.', '_');[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m iRangecc fn = { parts.path.start + lastIndexOfCStr_Rangecc(parts.path, "/") + 1,[m [32m+[m[32m parts.path.end };[m [32m+[m[32m if (!isEmpty_Range(&fn)) {[m [32m+[m[32m setRange_String(name, fn);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m if (startsWith_String(name, "~")) {[m [32m+[m[32m /* This would be interpreted as a reference to a home directory. */[m [32m+[m[32m remove_Block(&name->chars, 0, 1);[m [32m+[m[32m }[m [32m+[m[32m iString *savePath = concat_Path(downloadDir_App(), name);[m [32m+[m[32m if (lastIndexOfCStr_String(savePath, ".") == iInvalidPos) {[m [32m+[m[32m /* No extension specified in URL. */[m [32m+[m[32m if (startsWith_String(mime, "text/gemini")) {[m [32m+[m[32m appendCStr_String(savePath, ".gmi");[m [32m+[m[32m }[m [32m+[m[32m else if (startsWith_String(mime, "text/")) {[m [32m+[m[32m appendCStr_String(savePath, ".txt");[m [32m+[m[32m }[m [32m+[m[32m else if (startsWith_String(mime, "image/")) {[m [32m+[m[32m appendCStr_String(savePath, cstr_String(mime) + 6);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m if (fileExists_FileInfo(savePath)) {[m [32m+[m[32m /* Make it unique. */[m [32m+[m[32m iDate now;[m [32m+[m[32m initCurrent_Date(&now);[m [32m+[m[32m size_t insPos = lastIndexOfCStr_String(savePath, ".");[m [32m+[m[32m if (insPos == iInvalidPos) {[m [32m+[m[32m insPos = size_String(savePath);[m [32m+[m[32m }[m [32m+[m[32m const iString *date = collect_String(format_Date(&now, "_%Y-%m-%d_%H%M%S"));[m [32m+[m[32m insertData_Block(&savePath->chars, insPos, cstr_String(date), size_String(date));[m [32m+[m[32m }[m [32m+[m[32m return collect_String(savePath);[m [32m+[m[32m}[m [32m+[m const iString *debugInfo_App(void) {[m extern char **environ; /* The environment variables. */[m iApp *d = &app_;[m [36m@@ -570,6 +668,39 @@[m [mconst iString *debugInfo_App(void) {[m return msg;[m }[m [m [32m+[m[32mstatic void clearCache_App_(void) {[m [32m+[m[32m iForEach(ObjectList, i, iClob(listDocuments_App())) {[m [32m+[m[32m clearCache_History(history_DocumentWidget(i.object));[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m [32m+[m[32mvoid trimCache_App(void) {[m [32m+[m[32m iApp *d = &app_;[m [32m+[m[32m size_t cacheSize = 0;[m [32m+[m[32m const size_t limit = d->prefs.maxCacheSize * 1000000;[m [32m+[m[32m iObjectList *docs = listDocuments_App();[m [32m+[m[32m iForEach(ObjectList, i, docs) {[m [32m+[m[32m cacheSize += cacheSize_History(history_DocumentWidget(i.object));[m [32m+[m[32m }[m [32m+[m[32m init_ObjectListIterator(&i, docs);[m [32m+[m[32m iBool wasPruned = iFalse;[m [32m+[m[32m while (cacheSize > limit) {[m [32m+[m[32m iDocumentWidget *doc = i.object;[m [32m+[m[32m const size_t pruned = pruneLeastImportant_History(history_DocumentWidget(doc));[m [32m+[m[32m if (pruned) {[m [32m+[m[32m cacheSize -= pruned;[m [32m+[m[32m wasPruned = iTrue;[m [32m+[m[32m }[m [32m+[m[32m next_ObjectListIterator(&i);[m [32m+[m[32m if (!i.value) {[m [32m+[m[32m if (!wasPruned) break;[m [32m+[m[32m wasPruned = iFalse;[m [32m+[m[32m init_ObjectListIterator(&i, docs);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m iRelease(docs);[m [32m+[m[32m}[m [32m+[m iLocalDef iBool isWaitingAllowed_App_(iApp *d) {[m #if defined (LAGRANGE_IDLE_SLEEP)[m if (d->isIdling) {[m [36m@@ -587,10 +718,31 @@[m [mvoid processEvents_App(enum iAppEventMode eventMode) {[m SDL_WaitEvent(&ev)) ||[m ((!isWaitingAllowed_App_(d) || eventMode == postedEventsOnly_AppEventMode) &&[m SDL_PollEvent(&ev))) {[m [32m+[m[32m#if defined (iPlatformAppleMobile)[m [32m+[m[32m if (processEvent_iOS(&ev)) {[m [32m+[m[32m continue;[m [32m+[m[32m }[m [32m+[m[32m#endif[m switch (ev.type) {[m case SDL_QUIT:[m d->isRunning = iFalse;[m [32m+[m[32m if (findWidget_App("prefs")) {[m [32m+[m[32m /* Make sure changed preferences get saved. */[m [32m+[m[32m postCommand_App("prefs.dismiss");[m [32m+[m[32m processEvents_App(postedEventsOnly_AppEventMode);[m [32m+[m[32m }[m goto backToMainLoop;[m [32m+[m[32m case SDL_APP_LOWMEMORY:[m [32m+[m[32m clearCache_App_();[m [32m+[m[32m break;[m [32m+[m[32m case SDL_APP_WILLENTERFOREGROUND:[m [32m+[m[32m postRefresh_App();[m [32m+[m[32m break;[m [32m+[m[32m case SDL_APP_TERMINATING:[m [32m+[m[32m case SDL_APP_WILLENTERBACKGROUND:[m [32m+[m[32m savePrefs_App_(d);[m [32m+[m[32m saveState_App_(d);[m [32m+[m[32m break;[m case SDL_DROPFILE: {[m iBool wasUsed = processEvent_Window(d->window, &ev);[m if (!wasUsed) {[m [36m@@ -637,7 +789,7 @@[m [mvoid processEvents_App(enum iAppEventMode eventMode) {[m wasUsed = processEvent_Keys(&ev);[m }[m if (ev.type == SDL_USEREVENT && ev.user.code == command_UserEventCode) {[m [31m-#if defined (iPlatformApple) && !defined (iPlatformIOS)[m [32m+[m[32m#if defined (iPlatformAppleDesktop)[m handleCommand_MacOS(command_UserEvent(&ev));[m #endif[m if (isCommand_UserEvent(&ev, "metrics.changed")) {[m [36m@@ -857,6 +1009,20 @@[m [miMimeHooks *mimeHooks_App(void) {[m return app_.mimehooks;[m }[m [m [32m+[m[32miBool isLandscape_App(void) {[m [32m+[m[32m const iApp *d = &app_;[m [32m+[m[32m const iInt2 size = rootSize_Window(d->window);[m [32m+[m[32m return size.x > size.y;[m [32m+[m[32m}[m [32m+[m [32m+[m[32menum iAppDeviceType deviceType_App(void) {[m [32m+[m[32m#if defined (iPlatformAppleMobile)[m [32m+[m[32m return isPhone_iOS() ? phone_AppDeviceType : tablet_AppDeviceType;[m [32m+[m[32m#else[m [32m+[m[32m return desktop_AppDeviceType;[m [32m+[m[32m#endif[m [32m+[m[32m}[m [32m+[m iGmCerts *certs_App(void) {[m return app_.certs;[m }[m [36m@@ -875,20 +1041,34 @@[m [mstatic void updatePrefsThemeButtons_(iWidget *d) {[m selected_WidgetFlag,[m colorTheme_App() == i);[m }[m [32m+[m[32m for (size_t i = 0; i < max_ColorAccent; i++) {[m [32m+[m[32m setFlags_Widget(findChild_Widget(d, format_CStr("prefs.accent.%u", i)),[m [32m+[m[32m selected_WidgetFlag,[m [32m+[m[32m prefs_App()->accent == i);[m [32m+[m[32m }[m }[m [m [31m-static void updateColorThemeButton_(iLabelWidget *button, int theme) {[m [31m- const char *mode = strstr(cstr_String(id_Widget(as_Widget(button))), ".dark") ? "dark" : "light";[m [31m- const char *command = format_CStr("doctheme.%s.set arg:%d", mode, theme);[m [31m- iForEach(ObjectList, i, children_Widget(findChild_Widget(as_Widget(button), "menu"))) {[m [32m+[m[32mstatic void updateDropdownSelection_(iLabelWidget *dropButton, const char *selectedCommand) {[m [32m+[m[32m iForEach(ObjectList, i, children_Widget(findChild_Widget(as_Widget(dropButton), "menu"))) {[m iLabelWidget *item = i.object;[m [31m- if (!cmp_String(command_LabelWidget(item), command)) {[m [31m- updateText_LabelWidget(button, text_LabelWidget(item));[m [31m- break;[m [32m+[m[32m const iBool isSelected = endsWith_String(command_LabelWidget(item), selectedCommand);[m [32m+[m[32m setFlags_Widget(as_Widget(item), selected_WidgetFlag, isSelected);[m [32m+[m[32m if (isSelected) {[m [32m+[m[32m updateText_LabelWidget(dropButton, text_LabelWidget(item));[m }[m }[m }[m [m [32m+[m[32mstatic void updateColorThemeButton_(iLabelWidget *button, int theme) {[m [32m+[m[32m// const char *mode = strstr(cstr_String(id_Widget(as_Widget(button))), ".dark")[m [32m+[m[32m// ? "dark" : "light";[m [32m+[m[32m updateDropdownSelection_(button, format_CStr(".set arg:%d", theme));[m [32m+[m[32m}[m [32m+[m [32m+[m[32mstatic void updateFontButton_(iLabelWidget *button, int font) {[m [32m+[m[32m updateDropdownSelection_(button, format_CStr(".set arg:%d", font));[m [32m+[m[32m}[m [32m+[m static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {[m if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) {[m setUiScale_Window(get_Window(),[m [36m@@ -897,6 +1077,8 @@[m [mstatic iBool handlePrefsCommands_(iWidget *d, const char *cmd) {[m postCommandf_App("downloads path:%s",[m cstr_String(text_InputWidget(findChild_Widget(d, "prefs.downloads"))));[m #endif[m [32m+[m[32m postCommandf_App("customframe arg:%d",[m [32m+[m[32m isSelected_Widget(findChild_Widget(d, "prefs.customframe")));[m postCommandf_App("window.retain arg:%d",[m isSelected_Widget(findChild_Widget(d, "prefs.retainwindow")));[m postCommandf_App("smoothscroll arg:%d",[m [36m@@ -907,6 +1089,8 @@[m [mstatic iBool handlePrefsCommands_(iWidget *d, const char *cmd) {[m isSelected_Widget(findChild_Widget(d, "prefs.ostheme")));[m postCommandf_App("decodeurls arg:%d",[m isSelected_Widget(findChild_Widget(d, "prefs.decodeurls")));[m [32m+[m[32m postCommandf_App("searchurl address:%s",[m [32m+[m[32m cstr_String(text_InputWidget(findChild_Widget(d, "prefs.searchurl"))));[m postCommandf_App("cachesize.set arg:%d",[m toInt_String(text_InputWidget(findChild_Widget(d, "prefs.cachesize"))));[m postCommandf_App("proxy.gemini address:%s",[m [36m@@ -919,6 +1103,7 @@[m [mstatic iBool handlePrefsCommands_(iWidget *d, const char *cmd) {[m postCommandf_App("prefs.dialogtab arg:%u",[m tabPageIndex_Widget(tabs, currentTabPage_Widget(tabs)));[m destroy_Widget(d);[m [32m+[m[32m postCommand_App("prefs.changed");[m return iTrue;[m }[m else if (equal_Command(cmd, "quoteicon.set")) {[m [36m@@ -935,6 +1120,14 @@[m [mstatic iBool handlePrefsCommands_(iWidget *d, const char *cmd) {[m updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.light"), arg_Command(cmd));[m return iFalse;[m }[m [32m+[m[32m else if (equal_Command(cmd, "font.set")) {[m [32m+[m[32m updateFontButton_(findChild_Widget(d, "prefs.font"), arg_Command(cmd));[m [32m+[m[32m else if (equal_Command(cmd, "headingfont.set")) {[m [32m+[m[32m return iFalse;[m [32m+[m[32m }[m [32m+[m[32m updateFontButton_(findChild_Widget(d, "prefs.headingfont"), arg_Command(cmd));[m [32m+[m[32m return iFalse;[m [32m+[m[32m }[m else if (equal_Command(cmd, "prefs.ostheme.changed")) {[m postCommandf_App("ostheme arg:%d", arg_Command(cmd));[m }[m [36m@@ -992,33 +1185,6 @@[m [miDocumentWidget *newTab_App(const iDocumentWidget *duplicateOf, iBool switchToNe[m return doc;[m }[m [m [31m-void trimCache_App(void) {[m [31m- iApp *d = &app_;[m [31m- size_t cacheSize = 0;[m [31m- const size_t limit = d->prefs.maxCacheSize * 1000000;[m [31m- iObjectList *docs = listDocuments_App();[m [31m- iForEach(ObjectList, i, docs) {[m [31m- cacheSize += cacheSize_History(history_DocumentWidget(i.object));[m [31m- }[m [31m- init_ObjectListIterator(&i, docs);[m [31m- iBool wasPruned = iFalse;[m [31m- while (cacheSize > limit) {[m [31m- iDocumentWidget *doc = i.object;[m [31m- const size_t pruned = pruneLeastImportant_History(history_DocumentWidget(doc));[m [31m- if (pruned) {[m [31m- cacheSize -= pruned;[m [31m- wasPruned = iTrue;[m [31m- }[m [31m- next_ObjectListIterator(&i);[m [31m- if (!i.value) {[m [31m- if (!wasPruned) break;[m [31m- wasPruned = iFalse;[m [31m- init_ObjectListIterator(&i, docs);[m [31m- }[m [31m- }[m [31m- iRelease(docs);[m [31m-}[m [31m-[m static iBool handleIdentityCreationCommands_(iWidget *dlg, const char *cmd) {[m iApp *d = &app_;[m if (equal_Command(cmd, "ident.temp.changed")) {[m [36m@@ -1092,6 +1258,15 @@[m [miBool willUseProxy_App(const iRangecc scheme) {[m return schemeProxy_App(scheme) != NULL;[m }[m [m [32m+[m[32mconst iString *searchQueryUrl_App(const iString *queryStringUnescaped) {[m [32m+[m[32m iApp *d = &app_;[m [32m+[m[32m if (isEmpty_String(&d->prefs.searchUrl)) {[m [32m+[m[32m return collectNew_String();[m [32m+[m[32m }[m [32m+[m[32m const iString *escaped = urlEncode_String(queryStringUnescaped);[m [32m+[m[32m return collectNewFormat_String("%s?%s", cstr_String(&d->prefs.searchUrl), cstr_String(escaped));[m [32m+[m[32m}[m [32m+[m iBool handleCommand_App(const char *cmd) {[m iApp *d = &app_;[m if (equal_Command(cmd, "config.error")) {[m [36m@@ -1100,6 +1275,10 @@[m [miBool handleCommand_App(const char *cmd) {[m suffixPtr_Command(cmd, "where")));[m return iTrue;[m }[m [32m+[m[32m else if (equal_Command(cmd, "prefs.changed")) {[m [32m+[m[32m savePrefs_App_(d);[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m else if (equal_Command(cmd, "prefs.dialogtab")) {[m d->prefs.dialogTab = arg_Command(cmd);[m return iTrue;[m [36m@@ -1108,8 +1287,24 @@[m [miBool handleCommand_App(const char *cmd) {[m d->prefs.retainWindowSize = arg_Command(cmd);[m return iTrue;[m }[m [32m+[m[32m else if (equal_Command(cmd, "customframe")) {[m [32m+[m[32m d->prefs.customFrame = arg_Command(cmd);[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m else if (equal_Command(cmd, "window.maximize")) {[m [31m- SDL_MaximizeWindow(d->window->win);[m [32m+[m[32m if (!argLabel_Command(cmd, "toggle")) {[m [32m+[m[32m setSnap_Window(d->window, maximized_WindowSnap);[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m setSnap_Window(d->window, snap_Window(d->window) == maximized_WindowSnap ? 0 :[m [32m+[m[32m maximized_WindowSnap);[m [32m+[m[32m }[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m else if (equal_Command(cmd, "window.fullscreen")) {[m [32m+[m[32m const iBool wasFull = snap_Window(d->window) == fullscreen_WindowSnap;[m [32m+[m[32m setSnap_Window(d->window, wasFull ? 0 : fullscreen_WindowSnap);[m [32m+[m[32m postCommandf_App("window.fullscreen.changed arg:%d", !wasFull);[m return iTrue;[m }[m else if (equal_Command(cmd, "font.set")) {[m [36m@@ -1170,6 +1365,12 @@[m [miBool handleCommand_App(const char *cmd) {[m postCommandf_App("theme.changed auto:%d", isAuto);[m return iTrue;[m }[m [32m+[m[32m else if (equal_Command(cmd, "accent.set")) {[m [32m+[m[32m d->prefs.accent = arg_Command(cmd);[m [32m+[m[32m setThemePalette_Color(d->prefs.theme);[m [32m+[m[32m postCommandf_App("theme.changed auto:1");[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m else if (equal_Command(cmd, "ostheme")) {[m d->prefs.useSystemTheme = arg_Command(cmd);[m return iTrue;[m [36m@@ -1219,6 +1420,11 @@[m [miBool handleCommand_App(const char *cmd) {[m postRefresh_App();[m return iTrue;[m }[m [32m+[m[32m else if (equal_Command(cmd, "prefs.centershort.changed")) {[m [32m+[m[32m d->prefs.centerShortDocs = arg_Command(cmd) != 0;[m [32m+[m[32m postCommand_App("theme.changed");[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m else if (equal_Command(cmd, "prefs.hoverlink.changed")) {[m d->prefs.hoverLink = arg_Command(cmd) != 0;[m postRefresh_App();[m [36m@@ -1241,6 +1447,17 @@[m [miBool handleCommand_App(const char *cmd) {[m }[m return iTrue;[m }[m [32m+[m[32m else if (equal_Command(cmd, "searchurl")) {[m [32m+[m[32m iString *url = &d->prefs.searchUrl;[m [32m+[m[32m setCStr_String(url, suffixPtr_Command(cmd, "address"));[m [32m+[m[32m if (startsWith_String(url, "//")) {[m [32m+[m[32m prependCStr_String(url, "gemini:");[m [32m+[m[32m }[m [32m+[m[32m if (!isEmpty_String(url) && !startsWithCase_String(url, "gemini://")) {[m [32m+[m[32m prependCStr_String(url, "gemini://");[m [32m+[m[32m }[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m else if (equal_Command(cmd, "proxy.gemini")) {[m setCStr_String(&d->prefs.geminiProxy, suffixPtr_Command(cmd, "address"));[m return iTrue;[m [36m@@ -1332,7 +1549,13 @@[m [miBool handleCommand_App(const char *cmd) {[m return iTrue;[m }[m else if (equal_Command(cmd, "tabs.close")) {[m [31m- iWidget * tabs = findWidget_App("doctabs");[m [32m+[m[32m iWidget *tabs = findWidget_App("doctabs");[m [32m+[m[32m#if defined (iPlatformAppleMobile)[m [32m+[m[32m /* Can't close the last on mobile. */[m [32m+[m[32m if (tabCount_Widget(tabs) == 1) {[m [32m+[m[32m return iTrue;[m [32m+[m[32m }[m [32m+[m[32m#endif[m const iRangecc tabId = range_Command(cmd, "id");[m iWidget * doc = !isEmpty_Range(&tabId) ? findWidget_App(cstr_Rangecc(tabId))[m : document_App();[m [36m@@ -1385,6 +1608,7 @@[m [miBool handleCommand_App(const char *cmd) {[m setToggle_Widget(findChild_Widget(dlg, "prefs.smoothscroll"), d->prefs.smoothScrolling);[m setToggle_Widget(findChild_Widget(dlg, "prefs.imageloadscroll"), d->prefs.loadImageInsteadOfScrolling);[m setToggle_Widget(findChild_Widget(dlg, "prefs.ostheme"), d->prefs.useSystemTheme);[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "prefs.customframe"), d->prefs.customFrame);[m setToggle_Widget(findChild_Widget(dlg, "prefs.retainwindow"), d->prefs.retainWindowSize);[m setText_InputWidget(findChild_Widget(dlg, "prefs.uiscale"),[m collectNewFormat_String("%g", uiScale_Window(d->window)));[m [36m@@ -1411,8 +1635,11 @@[m [miBool handleCommand_App(const char *cmd) {[m iTrue);[m setToggle_Widget(findChild_Widget(dlg, "prefs.biglede"), d->prefs.bigFirstParagraph);[m setToggle_Widget(findChild_Widget(dlg, "prefs.sideicon"), d->prefs.sideIcon);[m [32m+[m[32m setToggle_Widget(findChild_Widget(dlg, "prefs.centershort"), d->prefs.centerShortDocs);[m updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.dark"), d->prefs.docThemeDark);[m updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.light"), d->prefs.docThemeLight);[m [32m+[m[32m updateFontButton_(findChild_Widget(dlg, "prefs.font"), d->prefs.font);[m [32m+[m[32m updateFontButton_(findChild_Widget(dlg, "prefs.headingfont"), d->prefs.headingFont);[m setFlags_Widget([m findChild_Widget([m dlg, format_CStr("prefs.saturation.%d", (int) (d->prefs.saturation * 3.99f))),[m [36m@@ -1421,6 +1648,7 @@[m [miBool handleCommand_App(const char *cmd) {[m setText_InputWidget(findChild_Widget(dlg, "prefs.cachesize"),[m collectNewFormat_String("%d", d->prefs.maxCacheSize));[m setToggle_Widget(findChild_Widget(dlg, "prefs.decodeurls"), d->prefs.decodeUserVisibleURLs);[m [32m+[m[32m setText_InputWidget(findChild_Widget(dlg, "prefs.searchurl"), &d->prefs.searchUrl);[m setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gemini"), &d->prefs.geminiProxy);[m setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.gopher"), &d->prefs.gopherProxy);[m setText_InputWidget(findChild_Widget(dlg, "prefs.proxy.http"), &d->prefs.httpProxy);[m [36m@@ -1568,9 +1796,10 @@[m [mvoid openInDefaultBrowser_App(const iString *url) {[m return;[m }[m #endif[m [32m+[m[32m#if !defined (iPlatformAppleMobile)[m iProcess *proc = new_Process();[m setArguments_Process(proc,[m [31m-#if defined (iPlatformApple)[m [32m+[m[32m#if defined (iPlatformAppleDesktop)[m iClob(newStringsCStr_StringList("/usr/bin/env", "open", cstr_String(url), NULL))[m #elif defined (iPlatformLinux) || defined (iPlatformOther)[m iClob(newStringsCStr_StringList("/usr/bin/env", "xdg-open", cstr_String(url), NULL))[m [36m@@ -1584,10 +1813,11 @@[m [mvoid openInDefaultBrowser_App(const iString *url) {[m );[m start_Process(proc);[m iRelease(proc);[m [32m+[m[32m#endif[m }[m [m void revealPath_App(const iString *path) {[m [31m-#if defined (iPlatformApple)[m [32m+[m[32m#if defined (iPlatformAppleDesktop)[m const char *scriptPath = concatPath_CStr(dataDir_App_(), "revealfile.scpt");[m iFile *f = newCStr_File(scriptPath);[m if (open_File(f, writeOnly_FileMode | text_FileMode)) {[m [1mdiff --git a/src/app.h b/src/app.h[m [1mindex efaf0a3e..9a68c362 100644[m [1m--- a/src/app.h[m [1m+++ b/src/app.h[m [36m@@ -38,6 +38,12 @@[m [miDeclareType(MimeHooks)[m iDeclareType(Visited)[m iDeclareType(Window)[m [m [32m+[m[32menum iAppDeviceType {[m [32m+[m[32m desktop_AppDeviceType,[m [32m+[m[32m tablet_AppDeviceType,[m [32m+[m[32m phone_AppDeviceType,[m [32m+[m[32m};[m [32m+[m enum iAppEventMode {[m waitForNewEvents_AppEventMode,[m postedEventsOnly_AppEventMode,[m [36m@@ -61,6 +67,9 @@[m [mvoid refresh_App (void);[m iBool isRefreshPending_App (void);[m uint32_t elapsedSinceLastTicker_App (void); /* milliseconds */[m [m [32m+[m[32miBool isLandscape_App (void);[m [32m+[m[32miLocalDef iBool isPortrait_App (void) { return !isLandscape_App(); }[m [32m+[m[32menum iAppDeviceType deviceType_App (void);[m iGmCerts * certs_App (void);[m iVisited * visited_App (void);[m iBookmarks * bookmarks_App (void);[m [36m@@ -75,6 +84,8 @@[m [miBool forceSoftwareRender_App(void);[m enum iColorTheme colorTheme_App (void);[m const iString * schemeProxy_App (iRangecc scheme);[m iBool willUseProxy_App (const iRangecc scheme);[m [32m+[m[32mconst iString * searchQueryUrl_App (const iString *queryStringUnescaped);[m [32m+[m[32mconst iString * downloadPathForUrl_App(const iString *url, const iString *mime);[m [m typedef void (*iTickerFunc)(iAny *);[m [m [1mdiff --git a/src/audio/player.c b/src/audio/player.c[m [1mindex 1c8538b4..d2ec9870 100644[m [1m--- a/src/audio/player.c[m [1m+++ b/src/audio/player.c[m [36m@@ -566,6 +566,9 @@[m [mstatic iContentSpec contentSpec_Player_(const iPlayer *d) {[m stb_vorbis *vrb = stb_vorbis_open_pushdata([m constData_Block(&d->data->data), size_Block(&d->data->data), &consumed, &error, NULL);[m if (!vrb) {[m [32m+[m[32m if (error != VORBIS_need_more_data) {[m [32m+[m[32m content.type = none_DecoderType;[m [32m+[m[32m }[m return content;[m }[m const stb_vorbis_info info = stb_vorbis_get_info(vrb);[m [36m@@ -793,8 +796,10 @@[m [miString *metadataLabel_Player(const iPlayer *d) {[m }[m unlock_Mutex(&d->decoder->tagMutex);[m }[m [31m- appendFormat_String(meta, "%d-bit %s %d Hz", SDL_AUDIO_BITSIZE(d->decoder->inputFormat),[m [31m- SDL_AUDIO_ISFLOAT(d->decoder->inputFormat) ? "float" : "integer",[m [31m- d->spec.freq);[m [32m+[m[32m if (d->decoder) {[m [32m+[m[32m appendFormat_String(meta, "%d-bit %s %d Hz", SDL_AUDIO_BITSIZE(d->decoder->inputFormat),[m [32m+[m[32m SDL_AUDIO_ISFLOAT(d->decoder->inputFormat) ? "float" : "integer",[m [32m+[m[32m d->spec.freq);[m [32m+[m[32m }[m return meta;[m }[m [1mdiff --git a/src/bookmarks.c b/src/bookmarks.c[m [1mindex 1fc24a67..91280f3c 100644[m [1m--- a/src/bookmarks.c[m [1m+++ b/src/bookmarks.c[m [36m@@ -65,8 +65,10 @@[m [mvoid addTag_Bookmark(iBookmark *d, const char *tag) {[m [m void removeTag_Bookmark(iBookmark *d, const char *tag) {[m const size_t pos = indexOfCStr_String(&d->tags, tag);[m [31m- remove_Block(&d->tags.chars, pos, strlen(tag));[m [31m- trim_String(&d->tags);[m [32m+[m[32m if (pos != iInvalidPos) {[m [32m+[m[32m remove_Block(&d->tags.chars, pos, strlen(tag));[m [32m+[m[32m trim_String(&d->tags);[m [32m+[m[32m }[m }[m [m iDefineTypeConstruction(Bookmark)[m [36m@@ -237,7 +239,7 @@[m [miBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon[m const uint32_t id = findUrl_Bookmarks(d, url);[m if (id) {[m iBookmark *bm = get_Bookmarks(d, id);[m [31m- if (!hasTag_Bookmark(bm, "remote")) {[m [32m+[m[32m if (!hasTag_Bookmark(bm, "remote") && !hasTag_Bookmark(bm, "usericon")) {[m if (icon != bm->icon) {[m bm->icon = icon;[m changed = iTrue;[m [36m@@ -248,6 +250,37 @@[m [miBool updateBookmarkIcon_Bookmarks(iBookmarks *d, const iString *url, iChar icon[m return changed;[m }[m [m [32m+[m[32miChar siteIcon_Bookmarks(const iBookmarks *d, const iString *url) {[m [32m+[m[32m if (isEmpty_String(url)) {[m [32m+[m[32m return 0;[m [32m+[m[32m }[m [32m+[m[32m static iRegExp *tagPattern_;[m [32m+[m[32m if (!tagPattern_) {[m [32m+[m[32m tagPattern_ = new_RegExp("\\busericon\\b", caseSensitive_RegExpOption);[m [32m+[m[32m }[m [32m+[m[32m const iRangecc urlRoot = urlRoot_String(url);[m [32m+[m[32m size_t matchingSize = iInvalidSize; /* we'll pick the shortest matching */[m [32m+[m[32m iChar icon = 0;[m [32m+[m[32m lock_Mutex(d->mtx);[m [32m+[m[32m iConstForEach(Hash, i, &d->bookmarks) {[m [32m+[m[32m const iBookmark *bm = (const iBookmark *) i.value;[m [32m+[m[32m iRegExpMatch m;[m [32m+[m[32m init_RegExpMatch(&m);[m [32m+[m[32m if (bm->icon && matchString_RegExp(tagPattern_, &bm->tags, &m)) {[m [32m+[m[32m const iRangecc bmRoot = urlRoot_String(&bm->url);[m [32m+[m[32m if (equalRangeCase_Rangecc(urlRoot, bmRoot)) {[m [32m+[m[32m const size_t n = size_String(&bm->url);[m [32m+[m[32m if (n < matchingSize) {[m [32m+[m[32m matchingSize = n;[m [32m+[m[32m icon = bm->icon;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m unlock_Mutex(d->mtx);[m [32m+[m[32m return icon;[m [32m+[m[32m}[m [32m+[m iBookmark *get_Bookmarks(iBookmarks *d, uint32_t id) {[m return (iBookmark *) value_Hash(&d->bookmarks, id);[m }[m [1mdiff --git a/src/bookmarks.h b/src/bookmarks.h[m [1mindex d5182b48..ab9c683b 100644[m [1m--- a/src/bookmarks.h[m [1m+++ b/src/bookmarks.h[m [36m@@ -62,6 +62,7 @@[m [miBookmark * get_Bookmarks (iBookmarks *, uint32_t id);[m void fetchRemote_Bookmarks (iBookmarks *);[m void requestFinished_Bookmarks (iBookmarks *, iGmRequest *req);[m iBool updateBookmarkIcon_Bookmarks(iBookmarks *, const iString *url, iChar icon);[m [32m+[m[32miChar siteIcon_Bookmarks (const iBookmarks *, const iString *url);[m [m void save_Bookmarks (const iBookmarks *, const char *dirPath);[m uint32_t findUrl_Bookmarks (const iBookmarks *, const iString *url); /* O(n) */[m [1mdiff --git a/src/feeds.c b/src/feeds.c[m [1mindex 3fb05d14..c66b2b84 100644[m [1m--- a/src/feeds.c[m [1m+++ b/src/feeds.c[m [36m@@ -598,7 +598,10 @@[m [mvoid removeEntries_Feeds(uint32_t feedBookmarkId) {[m [m static int cmpTimeDescending_FeedEntryPtr_(const void *a, const void *b) {[m const iFeedEntry * const *e1 = a, * const *e2 = b;[m [31m- return -cmp_Time(&(*e1)->posted, &(*e2)->posted);[m [32m+[m[32m const int cmpPosted = -cmp_Time(&(*e1)->posted, &(*e2)->posted);[m [32m+[m[32m if (cmpPosted) return cmpPosted;[m [32m+[m[32m /* Posting timestamps may only be accurate to a day, so also sort by discovery time. */[m [32m+[m[32m return -cmp_Time(&(*e1)->discovered, &(*e2)->discovered);[m }[m [m const iPtrArray *listEntries_Feeds(void) {[m [1mdiff --git a/src/gmdocument.c b/src/gmdocument.c[m [1mindex f73b7dc4..4e76a22a 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/metrics.h"[m #include "ui/window.h"[m #include "visited.h"[m [32m+[m[32m#include "bookmarks.h"[m #include "app.h"[m [m #include [m [36m@@ -254,6 +255,15 @@[m [mstatic iBool isForcedMonospace_GmDocument_(const iGmDocument *d) {[m return iFalse;[m }[m [m [32m+[m[32mstatic void linkContentLaidOut_GmDocument_(iGmDocument *d, const iGmMediaInfo *mediaInfo,[m [32m+[m[32m uint16_t linkId) {[m [32m+[m[32m iGmLink *link = at_PtrArray(&d->links, linkId - 1);[m [32m+[m[32m link->flags |= content_GmLinkFlag;[m [32m+[m[32m if (mediaInfo && mediaInfo->isPermanent) {[m [32m+[m[32m link->flags |= permanent_GmLinkFlag;[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m static void doLayout_GmDocument_(iGmDocument *d) {[m const iBool isMono = isForcedMonospace_GmDocument_(d);[m /* TODO: Collect these parameters into a GmTheme. */[m [36m@@ -281,10 +291,10 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m 5, 10, 5, 10, 0, 0, 0, 5[m };[m static const float topMargin[max_GmLineType] = {[m [31m- 0.0f, 0.333f, 1.0f, 0.5f, 2.0f, 1.5f, 1.0f, 0.5f[m [32m+[m[32m 0.0f, 0.333f, 1.0f, 0.5f, 2.0f, 1.5f, 1.0f, 0.25f[m };[m static const float bottomMargin[max_GmLineType] = {[m [31m- 0.0f, 0.333f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f[m [32m+[m[32m 0.0f, 0.333f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.25f[m };[m static const char *arrow = "\u27a4";[m static const char *envelope = "\U0001f4e7";[m [36m@@ -294,7 +304,6 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m static const char *quote = "\u201c";[m static const char *magnifyingGlass = "\U0001f50d";[m static const char *pointingFinger = "\U0001f449";[m [31m- const float midRunSkip = 0; /*0.120f;*/ /* extra space between wrapped text/quote lines */[m const iPrefs *prefs = prefs_App();[m clear_Array(&d->layout);[m clearLinks_GmDocument_(d);[m [36m@@ -325,6 +334,7 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m iGmRun run = { .color = white_ColorId };[m enum iGmLineType type;[m int indent = 0;[m [32m+[m[32m int rightMargin = 0;[m /* Detect the type of the line. */[m if (!isPreformat) {[m type = lineType_GmDocument_(d, line);[m [36m@@ -431,8 +441,7 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m if ((type == link_GmLineType && prevType == link_GmLineType) ||[m (type == quote_GmLineType && prevType == quote_GmLineType)) {[m /* No margin between consecutive links/quote lines. */[m [31m- required =[m [31m- (type == link_GmLineType ? midRunSkip * lineHeight_Text(paragraph_FontId) : 0);[m [32m+[m[32m required = 0;[m }[m if (isEmpty_Array(&d->layout)) {[m required = 0; /* top of document */[m [36m@@ -522,17 +531,13 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m if (!prefs->quoteIcon && type == quote_GmLineType) {[m run.flags |= quoteBorder_GmRunFlag;[m }[m [32m+[m[32m rightMargin = (type == text_GmLineType || type == bullet_GmLineType ||[m [32m+[m[32m type == quote_GmLineType ? 4 : 0);[m iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */[m while (!isEmpty_Range(&runLine)) {[m [31m- /* Little bit of breathing space between wrapped lines. */[m [31m- if ((type == text_GmLineType || type == quote_GmLineType ||[m [31m- type == bullet_GmLineType) &&[m [31m- runLine.start != line.start) {[m [31m- pos.y += midRunSkip * lineHeight_Text(run.font);[m [31m- }[m run.bounds.pos = addX_I2(pos, indent * gap_Text);[m const char *contPos;[m [31m- const int avail = isPreformat ? 0 : (d->size.x - run.bounds.pos.x);[m [32m+[m[32m const int avail = isPreformat ? 0 : (d->size.x - run.bounds.pos.x - rightMargin * gap_Text);[m const iInt2 dims = tryAdvance_Text(run.font, runLine, avail, &contPos);[m iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x));[m run.bounds.size.x = iMax(avail, dims.x); /* Extends to the right edge for selection. */[m [36m@@ -562,48 +567,40 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m if (type == link_GmLineType) {[m const iMediaId imageId = findLinkImage_Media(d->media, run.linkId);[m const iMediaId audioId = !imageId ? findLinkAudio_Media(d->media, run.linkId) : 0;[m [32m+[m[32m const iMediaId downloadId = !imageId && !audioId ? findLinkDownload_Media(d->media, run.linkId) : 0;[m if (imageId) {[m [31m- iGmImageInfo img;[m [32m+[m[32m iGmMediaInfo img;[m imageInfo_Media(d->media, imageId, &img);[m [31m- /* Mark the link as having content. */ {[m [31m- iGmLink *link = at_PtrArray(&d->links, run.linkId - 1);[m [31m- link->flags |= content_GmLinkFlag;[m [31m- if (img.isPermanent) {[m [31m- link->flags |= permanent_GmLinkFlag;[m [31m- }[m [31m- }[m [32m+[m[32m const iInt2 imgSize = imageSize_Media(d->media, imageId);[m [32m+[m[32m linkContentLaidOut_GmDocument_(d, &img, run.linkId);[m const int margin = lineHeight_Text(paragraph_FontId) / 2;[m pos.y += margin;[m run.bounds.pos = pos;[m run.bounds.size.x = d->size.x;[m [31m- const float aspect = (float) img.size.y / (float) img.size.x;[m [32m+[m[32m const float aspect = (float) imgSize.y / (float) imgSize.x;[m run.bounds.size.y = d->size.x * aspect;[m run.visBounds = run.bounds;[m [31m- const iInt2 maxSize = mulf_I2(img.size, get_Window()->pixelRatio);[m [32m+[m[32m const iInt2 maxSize = mulf_I2(imgSize, get_Window()->pixelRatio);[m if (width_Rect(run.visBounds) > maxSize.x) {[m /* Don't scale the image up. */[m [31m- run.visBounds.size.y = run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);[m [32m+[m[32m run.visBounds.size.y =[m [32m+[m[32m run.visBounds.size.y * maxSize.x / width_Rect(run.visBounds);[m 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 [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 }[m [31m- run.text = iNullRange;[m [31m- run.font = 0;[m [31m- run.color = 0;[m [31m- run.imageId = imageId;[m [32m+[m[32m run.text = iNullRange;[m [32m+[m[32m run.font = 0;[m [32m+[m[32m run.color = 0;[m [32m+[m[32m run.mediaType = image_GmRunMediaType;[m [32m+[m[32m run.mediaId = imageId;[m pushBack_Array(&d->layout, &run);[m pos.y += run.bounds.size.y + margin;[m }[m else if (audioId) {[m [31m- iGmAudioInfo info;[m [32m+[m[32m iGmMediaInfo info;[m audioInfo_Media(d->media, audioId, &info);[m [31m- /* Mark the link as having content. */ {[m [31m- iGmLink *link = at_PtrArray(&d->links, run.linkId - 1);[m [31m- link->flags |= content_GmLinkFlag;[m [31m- if (info.isPermanent) {[m [31m- link->flags |= permanent_GmLinkFlag;[m [31m- }[m [31m- }[m [32m+[m[32m linkContentLaidOut_GmDocument_(d, &info, run.linkId);[m const int margin = lineHeight_Text(paragraph_FontId) / 2;[m pos.y += margin;[m run.bounds.pos = pos;[m [36m@@ -612,7 +609,25 @@[m [mstatic void doLayout_GmDocument_(iGmDocument *d) {[m run.visBounds = run.bounds;[m run.text = iNullRange;[m run.color = 0;[m [31m- run.audioId = audioId;[m [32m+[m[32m run.mediaType = audio_GmRunMediaType;[m [32m+[m[32m run.mediaId = audioId;[m [32m+[m[32m pushBack_Array(&d->layout, &run);[m [32m+[m[32m pos.y += run.bounds.size.y + margin;[m [32m+[m[32m }[m [32m+[m[32m else if (downloadId) {[m [32m+[m[32m iGmMediaInfo info;[m [32m+[m[32m downloadInfo_Media(d->media, downloadId, &info);[m [32m+[m[32m linkContentLaidOut_GmDocument_(d, &info, run.linkId);[m [32m+[m[32m const int margin = lineHeight_Text(paragraph_FontId) / 2;[m [32m+[m[32m pos.y += margin;[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 run.text = iNullRange;[m [32m+[m[32m run.color = 0;[m [32m+[m[32m run.mediaType = download_GmRunMediaType;[m [32m+[m[32m run.mediaId = downloadId;[m pushBack_Array(&d->layout, &run);[m pos.y += run.bounds.size.y + margin;[m }[m [36m@@ -719,6 +734,13 @@[m [mstatic void setDerivedThemeColors_(enum iGmDocumentTheme theme) {[m }[m }[m [m [32m+[m[32mstatic void updateIconBasedOnUrl_GmDocument_(iGmDocument *d) {[m [32m+[m[32m const iChar userIcon = siteIcon_Bookmarks(bookmarks_App(), &d->url);[m [32m+[m[32m if (userIcon) {[m [32m+[m[32m d->siteIcon = userIcon;[m [32m+[m[32m }[m [32m+[m[32m}[m [32m+[m void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m const iPrefs * prefs = prefs_App();[m enum iGmDocumentTheme theme =[m [36m@@ -803,8 +825,8 @@[m [mvoid setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m set_Color(tmHeading2_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.67f));[m set_Color(tmHeading3_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.55f));[m setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0, -0.1f));[m [31m- setHsl_Color(tmBannerIcon_ColorId, addSatLum_HSLColor(base, 0, -0.2f));[m [31m- setHsl_Color(tmBannerTitle_ColorId, addSatLum_HSLColor(base, 0, -0.2f));[m [32m+[m[32m setHsl_Color(tmBannerIcon_ColorId, addSatLum_HSLColor(base, 0, -0.4f));[m [32m+[m[32m setHsl_Color(tmBannerTitle_ColorId, addSatLum_HSLColor(base, 0, -0.4f));[m setHsl_Color(tmLinkIcon_ColorId, addSatLum_HSLColor(get_HSLColor(teal_ColorId), 0, 0));[m set_Color(tmLinkIconVisited_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(teal_ColorId), 0.35f));[m set_Color(tmLinkDomain_ColorId, get_Color(teal_ColorId));[m [36m@@ -914,7 +936,7 @@[m [mvoid setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m violet_Hue,[m pink_Hue[m };[m [31m- static const float hues[] = { 5, 25, 40, 56, 80, 120, 160, 180, 208, 231, 270, 324 };[m [32m+[m[32m static const float hues[] = { 5, 25, 40, 56, 80 + 15, 120, 160, 180, 208, 231, 270, 324 };[m static const struct {[m int index[2];[m } altHues[iElemCount(hues)] = {[m [36m@@ -922,7 +944,7 @@[m [mvoid setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m { 8, 3 }, /* reddish orange */[m { 7, 9 }, /* yellowish orange */[m { 5, 7 }, /* yellow */[m [31m- { 11, 2 }, /* greenish yellow */[m [32m+[m[32m { 6, 2 }, /* greenish yellow */[m { 1, 3 }, /* green */[m { 2, 4 }, /* bluish green */[m { 2, 11 }, /* cyan */[m [36m@@ -968,6 +990,8 @@[m [mvoid setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m setHsl_Color(tmHeading2_ColorId, setLum_HSLColor(altBase, titleLum + 0.70f));[m setHsl_Color(tmHeading3_ColorId, setLum_HSLColor(altBase, titleLum + 0.60f));[m [m [32m+[m[32m// printf("titleLum: %f\n", titleLum);[m [32m+[m setHsl_Color(tmParagraph_ColorId, addSatLum_HSLColor(base, 0.1f, 0.6f));[m [m // printf("heading3: %d,%d,%d\n", get_Color(tmHeading3_ColorId).r, get_Color(tmHeading3_ColorId).g, get_Color(tmHeading3_ColorId).b);[m [36m@@ -976,11 +1000,8 @@[m [mvoid setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m [m if (delta_Color(get_Color(tmHeading3_ColorId), get_Color(tmParagraph_ColorId)) <= 80) {[m /* Smallest headings may be too close to body text color. */[m [31m-// iHSLColor clr = get_HSLColor(tmParagraph_ColorId);[m [31m-// clr.lum = iMax(0.5f, clr.lum - 0.15f);[m [31m- //setHsl_Color(tmParagraph_ColorId, clr);[m [31m- setHsl_Color(tmHeading3_ColorId,[m [31m- addSatLum_HSLColor(get_HSLColor(tmHeading3_ColorId), 0, 0.15f));[m [32m+[m[32m setHsl_Color(tmHeading2_ColorId, addSatLum_HSLColor(get_HSLColor(tmHeading2_ColorId), 0.5f, -0.12f));[m [32m+[m[32m setHsl_Color(tmHeading3_ColorId, addSatLum_HSLColor(get_HSLColor(tmHeading3_ColorId), 0.5f, -0.2f));[m }[m [m setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0.2f, 0.72f));[m [36m@@ -1091,6 +1112,7 @@[m [mvoid setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) {[m if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) {[m d->siteIcon = 0x264a; /* gemini symbol */[m }[m [32m+[m[32m updateIconBasedOnUrl_GmDocument_(d);[m }[m #if 0[m for (int i = tmFirst_ColorId; i < max_ColorId; ++i) {[m [36m@@ -1192,6 +1214,7 @@[m [mvoid setUrl_GmDocument(iGmDocument *d, const iString *url) {[m iUrl parts;[m init_Url(&parts, url);[m setRange_String(&d->localHost, parts.host);[m [32m+[m[32m updateIconBasedOnUrl_GmDocument_(d);[m }[m [m void setSource_GmDocument(iGmDocument *d, const iString *source, int width) {[m [1mdiff --git a/src/gmdocument.h b/src/gmdocument.h[m [1mindex e2c7e10c..16127ea3 100644[m [1m--- a/src/gmdocument.h[m [1m+++ b/src/gmdocument.h[m [36m@@ -85,17 +85,24 @@[m [menum iGmRunFlags {[m wide_GmRunFlag = iBit(6), /* horizontally scrollable */[m };[m [m [32m+[m[32menum iGmRunMediaType {[m [32m+[m[32m none_GmRunMediaType,[m [32m+[m[32m image_GmRunMediaType,[m [32m+[m[32m audio_GmRunMediaType,[m [32m+[m[32m download_GmRunMediaType,[m [32m+[m[32m};[m [32m+[m struct Impl_GmRun {[m iRangecc text;[m uint8_t font;[m uint8_t color;[m uint8_t flags;[m [32m+[m[32m uint8_t mediaType;[m iRect bounds; /* used for hit testing, may extend to edges */[m iRect visBounds; /* actual visual bounds */[m uint16_t preId; /* preformatted block ID (sequential) */[m iGmLinkId linkId; /* zero for non-links */[m [31m- uint16_t imageId; /* zero if not an image */[m [31m- uint16_t audioId; /* zero if not audio */[m [32m+[m[32m uint16_t mediaId; /* zero if not an image */[m };[m [m iDeclareType(GmRunRange)[m [1mdiff --git a/src/gmrequest.c b/src/gmrequest.c[m [1mindex 8626403f..0208dc94 100644[m [1m--- a/src/gmrequest.c[m [1m+++ b/src/gmrequest.c[m [36m@@ -133,6 +133,7 @@[m [mstruct Impl_GmRequest {[m iTlsRequest * req;[m iGopher gopher;[m iGmResponse * resp;[m [32m+[m[32m iBool isFilterEnabled;[m iBool isRespLocked;[m iBool isRespFiltered;[m iAtomicInt allowUpdate;[m [36m@@ -208,7 +209,7 @@[m [mstatic int processIncomingData_GmRequest_(iGmRequest *d, const iBlock *data) {[m resp->statusCode = code;[m d->state = receivingBody_GmRequestState;[m notifyUpdate = iTrue;[m [31m- if (willTryFilter_MimeHooks(mimeHooks_App(), &resp->meta)) {[m [32m+[m[32m if (d->isFilterEnabled && willTryFilter_MimeHooks(mimeHooks_App(), &resp->meta)) {[m d->isRespFiltered = iTrue;[m }[m }[m [36m@@ -226,7 +227,12 @@[m [mstatic int processIncomingData_GmRequest_(iGmRequest *d, const iBlock *data) {[m static void readIncoming_GmRequest_(iGmRequest *d, iTlsRequest *req) {[m lock_Mutex(d->mtx);[m iGmResponse *resp = d->resp;[m [31m- iAssert(d->state != finished_GmRequestState); /* notifications out of order? */[m [32m+[m[32m if (d->state == finished_GmRequestState || d->state == failure_GmRequestState) {[m [32m+[m[32m /* The request has already finished or been aborted (e.g., invalid header). */[m [32m+[m[32m delete_Block(readAll_TlsRequest(req));[m [32m+[m[32m unlock_Mutex(d->mtx);[m [32m+[m[32m return;[m [32m+[m[32m }[m iBlock * data = readAll_TlsRequest(req);[m const int ubits = processIncomingData_GmRequest_(d, data);[m iBool notifyUpdate = (ubits & 1) != 0;[m [36m@@ -457,8 +463,9 @@[m [mstatic void beginGopherConnection_GmRequest_(iGmRequest *d, const iString *host,[m void init_GmRequest(iGmRequest *d, iGmCerts *certs) {[m d->mtx = new_Mutex();[m d->resp = new_GmResponse();[m [31m- d->isRespLocked = iFalse;[m [31m- d->isRespFiltered = iFalse;[m [32m+[m[32m d->isFilterEnabled = iTrue;[m [32m+[m[32m d->isRespLocked = iFalse;[m [32m+[m[32m d->isRespFiltered = iFalse;[m set_Atomic(&d->allowUpdate, iTrue);[m init_String(&d->url);[m init_Gopher(&d->gopher);[m [36m@@ -492,6 +499,10 @@[m [mvoid deinit_GmRequest(iGmRequest *d) {[m delete_Mutex(d->mtx);[m }[m [m [32m+[m[32mvoid enableFilters_GmRequest(iGmRequest *d, iBool enable) {[m [32m+[m[32m d->isFilterEnabled = enable;[m [32m+[m[32m}[m [32m+[m void setUrl_GmRequest(iGmRequest *d, const iString *url) {[m set_String(&d->url, urlFragmentStripped_String(url));[m /* Encode hostname to Punycode here because we want to submit the Punycode domain name[m [36m@@ -546,7 +557,7 @@[m [mvoid submit_GmRequest(iGmRequest *d) {[m remove_Block(&path->chars, 0, 1);[m }[m #endif[m [31m- iFile * f = new_File(path);[m [32m+[m[32m iFile *f = new_File(path);[m if (open_File(f, readOnly_FileMode)) {[m /* TODO: Check supported file types: images, audio */[m /* TODO: Detect text files based on contents? E.g., is the content valid UTF-8. */[m [1mdiff --git a/src/gmrequest.h b/src/gmrequest.h[m [1mindex bd340cf1..6d4eb2f8 100644[m [1m--- a/src/gmrequest.h[m [1m+++ b/src/gmrequest.h[m [36m@@ -64,6 +64,7 @@[m [miDeclareNotifyFunc(GmRequest, Finished)[m iDeclareAudienceGetter(GmRequest, updated)[m iDeclareAudienceGetter(GmRequest, finished)[m [m [32m+[m[32mvoid enableFilters_GmRequest (iGmRequest *, iBool enable);[m void setUrl_GmRequest (iGmRequest *, const iString *url);[m void submit_GmRequest (iGmRequest *);[m void cancel_GmRequest (iGmRequest *);[m [1mdiff --git a/src/gmutil.c b/src/gmutil.c[m [1mindex 44bbabfd..2b40367d 100644[m [1m--- a/src/gmutil.c[m [1m+++ b/src/gmutil.c[m [36m@@ -76,6 +76,9 @@[m [miLocalDef iBool isDef_(iRangecc cc) {[m [m static iRangecc prevPathSeg_(const char *end, const char *start) {[m iRangecc seg = { end, end };[m [32m+[m[32m if (start == end) {[m [32m+[m[32m return seg;[m [32m+[m[32m }[m do {[m seg.start--;[m } while (*seg.start != '/' && seg.start != start);[m [36m@@ -149,6 +152,37 @@[m [miRangecc urlHost_String(const iString *d) {[m return url.host;[m }[m [m [32m+[m[32miRangecc urlUser_String(const iString *d) {[m [32m+[m[32m static iRegExp *userPats_[2];[m [32m+[m[32m if (!userPats_[0]) {[m [32m+[m[32m userPats_[0] = new_RegExp("~([^/?]+)", 0);[m [32m+[m[32m userPats_[1] = new_RegExp("/users/([^/?]+)", caseInsensitive_RegExpOption);[m [32m+[m[32m }[m [32m+[m[32m iRegExpMatch m;[m [32m+[m[32m init_RegExpMatch(&m);[m [32m+[m[32m iRangecc found = iNullRange;[m [32m+[m[32m iForIndices(i, userPats_) {[m [32m+[m[32m if (matchString_RegExp(userPats_[i], d, &m)) {[m [32m+[m[32m found = capturedRange_RegExpMatch(&m, 1);[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m return found;[m [32m+[m[32m}[m [32m+[m [32m+[m[32miRangecc urlRoot_String(const iString *d) {[m [32m+[m[32m const char *rootEnd;[m [32m+[m[32m const iRangecc user = urlUser_String(d);[m [32m+[m[32m if (!isEmpty_Range(&user)) {[m [32m+[m[32m rootEnd = user.end;[m [32m+[m[32m }[m [32m+[m[32m else {[m [32m+[m[32m iUrl parts;[m [32m+[m[32m init_Url(&parts, d);[m [32m+[m[32m rootEnd = parts.path.start;[m [32m+[m[32m }[m [32m+[m[32m return (iRangecc){ constBegin_String(d), rootEnd };[m [32m+[m[32m}[m [32m+[m static iBool isAbsolutePath_(iRangecc path) {[m return isAbsolute_Path(collect_String(urlDecode_String(collect_String(newRange_String(path)))));[m }[m [36m@@ -197,7 +231,7 @@[m [mvoid urlEncodePath_String(iString *d) {[m return;[m }[m iString *encoded = new_String();[m [31m- appendRange_String(encoded , (iRangecc){ constBegin_String(d), url.path.start });[m [32m+[m[32m appendRange_String(encoded, (iRangecc){ constBegin_String(d), url.path.start });[m iString *path = newRange_String(url.path);[m iString *encPath = urlEncodeExclude_String(path, "%/ ");[m append_String(encoded, encPath);[m [36m@@ -272,6 +306,21 @@[m [mconst iString *absoluteUrl_String(const iString *d, const iString *urlMaybeRelat[m return absolute;[m }[m [m [32m+[m[32miBool isLikelyUrl_String(const iString *d) {[m [32m+[m[32m /* Guess whether a human intends the string to be an URL. This is supposed to be fuzzy;[m [32m+[m[32m not completely per-spec: a) begins with a scheme; b) has something that looks like a[m [32m+[m[32m hostname */[m [32m+[m[32m iRegExp *pattern = new_RegExp("^([a-z]+:)?//.*|"[m [32m+[m[32m "^(//)?([^/?#: ]+)([/?#:].*)$|"[m [32m+[m[32m "^(\\w+(\\.\\w+)+|localhost)$",[m [32m+[m[32m caseInsensitive_RegExpOption);[m [32m+[m[32m iRegExpMatch m;[m [32m+[m[32m init_RegExpMatch(&m);[m [32m+[m[32m const iBool likelyUrl = matchString_RegExp(pattern, d, &m);[m [32m+[m[32m iRelease(pattern);[m [32m+[m[32m return likelyUrl;[m [32m+[m[32m}[m [32m+[m static iBool equalPuny_(const iString *d, iRangecc orig) {[m if (!endsWith_String(d, "-")) {[m return iFalse; /* This is a sufficient condition? */[m [1mdiff --git a/src/gmutil.h b/src/gmutil.h[m [1mindex 1caf2445..b2cee61a 100644[m [1m--- a/src/gmutil.h[m [1m+++ b/src/gmutil.h[m [36m@@ -103,7 +103,10 @@[m [mvoid init_Url (iUrl *, const iString *text);[m [m iRangecc urlScheme_String (const iString *);[m iRangecc urlHost_String (const iString *);[m [32m+[m[32miRangecc urlUser_String (const iString *);[m [32m+[m[32miRangecc urlRoot_String (const iString *);[m const iString * absoluteUrl_String (const iString *, const iString *urlMaybeRelative);[m [32m+[m[32miBool isLikelyUrl_String (const iString *);[m void punyEncodeUrlHost_String(iString *);[m void stripDefaultUrlPort_String(iString *);[m const iString * urlFragmentStripped_String(const iString *);[m [1mdiff --git a/src/gopher.c b/src/gopher.c[m [1mindex 0a7489ba..fa8495d7 100644[m [1m--- a/src/gopher.c[m [1m+++ b/src/gopher.c[m [36m@@ -160,11 +160,11 @@[m [mvoid open_Gopher(iGopher *d, const iString *url) {[m d->type = '0';[m }[m else if (parts.path.start < parts.path.end) {[m [31m- d->type = *parts.path.start;[m [31m- parts.path.start++;[m [32m+[m[32m d->type = *parts.path.start;[m [32m+[m[32m parts.path.start++;[m }[m else {[m [31m- d->type = '1';[m [32m+[m[32m d->type = '1';[m }[m if (d->type == '7' && isEmpty_Range(&parts.query)) {[m /* Ask for the query parameters first. */[m [36m@@ -204,12 +204,16 @@[m [mvoid open_Gopher(iGopher *d, const iString *url) {[m }[m d->isPre = iFalse;[m open_Socket(d->socket);[m [31m- writeData_Socket(d->socket, parts.path.start, size_Range(&parts.path));[m [32m+[m[32m const iString *reqPath =[m [32m+[m[32m collect_String(urlDecodeExclude_String(collectNewRange_String(parts.path), "\t"));[m [32m+[m[32m writeData_Socket(d->socket, cstr_String(reqPath), size_String(reqPath));[m if (!isEmpty_Range(&parts.query)) {[m iAssert(*parts.query.start == '?');[m parts.query.start++;[m writeData_Socket(d->socket, "\t", 1);[m [31m- writeData_Socket(d->socket, parts.query.start, size_Range(&parts.query));[m [32m+[m[32m const iString *reqQuery =[m [32m+[m[32m collect_String(urlDecode_String(collectNewRange_String(parts.query)));[m [32m+[m[32m writeData_Socket(d->socket, cstr_String(reqQuery), size_String(reqQuery));[m }[m writeData_Socket(d->socket, "\r\n", 2);[m }[m [1mdiff --git a/src/history.c b/src/history.c[m [1mindex 59d515dc..6876d8e3 100644[m [1m--- a/src/history.c[m [1m+++ b/src/history.c[m [36m@@ -299,6 +299,18 @@[m [msize_t cacheSize_History(const iHistory *d) {[m return cached;[m }[m [m [32m+[m[32mvoid clearCache_History(iHistory *d) {[m [32m+[m[32m lock_Mutex(d->mtx);[m [32m+[m[32m iForEach(Array, i, &d->recent) {[m [32m+[m[32m iRecentUrl *url = i.value;[m [32m+[m[32m if (url->cachedResponse) {[m [32m+[m[32m delete_GmResponse(url->cachedResponse);[m [32m+[m[32m url->cachedResponse = NULL;[m [32m+[m[32m }[m [32m+[m[32m }[m [32m+[m[32m unlock_Mutex(d->mtx);[m [32m+[m[32m}[m [32m+[m size_t pruneLeastImportant_History(iHistory *d) {[m size_t delta = 0;[m size_t chosen = iInvalidPos;[m [1mdiff --git a/src/history.h b/src/history.h[m [1mindex 7c2684f1..ce3b8e47 100644[m [1m--- a/src/history.h[m [1m+++ b/src/history.h[m [36m@@ -56,6 +56,7 @@[m [miBool goForward_History (iHistory *);[m iRecentUrl *recentUrl_History (iHistory *, size_t pos);[m iRecentUrl *mostRecentUrl_History (iHistory *);[m iRecentUrl *findUrl_History (iHistory *, const iString *url);[m [32m+[m[32mvoid clearCache_History (iHistory *);[m size_t pruneLeastImportant_History (iHistory *);[m [m const iStringArray * searchContents_History (const iHistory *, const iRegExp *pattern); /* chronologically ascending */[m [1mdiff --git a/src/ios.h b/src/ios.h[m [1mnew file mode 100644[m [1mindex 00000000..60841aee[m [1m--- /dev/null[m [1m+++ b/src/ios.h[m [36m@@ -0,0 +1,33 @@[m [32m+[m[32m/* Copyright 2021 Jaakko Keränen [m [32m+[m [32m+[m[32mRedistribution and use in source and binary forms, with or without[m [32m+[m[32mmodification, are permitted provided that the following conditions are met:[m [32m+[m [32m+[m[32m1. Redistributions of source code must retain the above copyright notice, this[m [32m+[m[32m list of conditions and the following disclaimer.[m [32m+[m[32m2. Redistributions in binary form must reproduce the above copyright notice,[m [32m+[m[32m this list of conditions and the following disclaimer in the documentation[m [32m+[m[32m and/or other materials provided with the distribution.[m [32m+[m [32m+[m[32mTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND[m [32m+[m[32mANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED[m [32m+[m[32mWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE[m [32m+[m[32mDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR[m [32m+[m[32mANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES[m [32m+[m[32m(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;[m [32m+[m[32mLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON[m [32m+[m[32mANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT[m [32m+[m[32m(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS[m [32m+[m[32mSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */[m [32m+[m [32m+[m[32m#pragma once[m [32m+[m [32m+[m[32m#include "ui/util.h"[m [32m+[m [32m+[m[32miDeclareType(Window)[m [32m+[m [32m+[m[32mvoid setupApplication_iOS (void);[m [32m+[m[32m
(truncated output; full size was 648.53 KB)
text/gemini; charset=utf-8
This content has been proxied by September (ba2dc).