From 41ef1d89b3856736a50999b0ac24ae87c4e5a687 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 12:59:30 +0000 Subject: [PATCH] feat(ui): add the OpenTimestamps logo as a custom glyph and use it in the OTS pill Bake the official OpenTimestamps stamp logo (traced from opentimestamps/logo vector.svg; monochrome outline, tinted at render time like every other glyph) into the Material Symbols subset font at U+F8F0. New tools/material-symbols-subset/add_custom_glyphs.py converts the traced SVGs in custom/ into TrueType glyphs and is invoked by subset.sh after pyftsubset, so font regenerations keep them. With the logo identifying the pill, drop the verbose "OTS:" prefix: the pill now reads icon + "2y" (or icon + "Pending", new R.string.pending). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5 --- .../amethyst/ui/note/elements/DisplayOts.kt | 8 +- amethyst/src/main/res/values/strings.xml | 1 + .../font/material_symbols_outlined.ttf | Bin 464004 -> 466160 bytes .../commons/icons/symbols/MaterialSymbols.kt | 6 + tools/material-symbols-subset/README.md | 12 ++ .../add_custom_glyphs.py | 104 ++++++++++++++ .../custom/opentimestamps.svg | 130 ++++++++++++++++++ tools/material-symbols-subset/subset.sh | 4 + 8 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 tools/material-symbols-subset/add_custom_glyphs.py create mode 100644 tools/material-symbols-subset/custom/opentimestamps.svg diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt index 319293d69c..ea62ebdb9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayOts.kt @@ -61,8 +61,8 @@ fun DisplayOts( } HeaderPill( - symbol = MaterialSymbols.CheckCircle, - text = stringRes(R.string.existed_since, timeStr), + symbol = MaterialSymbols.OpenTimestamps, + text = timeStr, contentDescription = stringRes(R.string.ots_info_title), onClick = { accountViewModel.toastManager.toast( @@ -75,8 +75,8 @@ fun DisplayOts( }, whenPending = { HeaderPill( - symbol = MaterialSymbols.Schedule, - text = stringRes(id = R.string.timestamp_pending_short), + symbol = MaterialSymbols.OpenTimestamps, + text = stringRes(id = R.string.pending), contentDescription = stringRes(R.string.ots_info_title), ) }, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a3ef6e6b7d..71c5cba0c2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -45,6 +45,7 @@ Timestamp it Timestamp: Pending Confirmations OTS: Pending + Pending Request Deletion Block / Report diff --git a/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf b/commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf index 01bd58472eab53e9922f2ac64092cd463a60370c..fde397e9efc2d552e8205b3b0ec61759f5f3a6df 100644 GIT binary patch delta 2930 zcmY*bYlu|W9Y62KJ@?#uXXeh#x-Z>_JIQ9N>+C!rQ4*V)q@)j%Xt9N2m$+(ZR+qS~ zrmyM4#)U{LEG7LILMW6#5VkZa{h&zFQXAUlL+O`N8d0&0QlSmxL#X|oJFcep%(?gc zpTGa>EPL|j!#965e5TgB`Vk@Y^kZMx`Q(c)t?VM?+6p0j@bgb?ZSH(x|3i@dlo0Zj zr}pgJbL!^uON6}tD(JW7zdrvQiT`nykn4v*?|yo5ere&`7f%u*t`VXx9)$eGe=oic z`J0emKQw=A3Ezy6tN+2*9D4TogBSjM_K&1T5;#0}{tyg)D8IFlkQ=WOwe>TH7v>Li ze|zQv0vLut=P(%hk{$!S2lV9O#pM(GUfX*U^oNA7o6jD7dj7quwM&G!Uj%w_{=^bJ z&Fhe#fc~Z<^NR~#dh&bk5b}ouNHAVHdTjZ`Urv3uN65Q~ta_4AsId;mgk#d_`EG@W4)Ny7h991oE~#9M>Njbm(zjBYD9JNbAq) z*564(1V2yD5V=}ks_~Y`#Okzgl{GhS7b%$?vC+(8t#0cfWKyG_gPa(AhxI zEya6=c3?g^O8dD4K|;$x6dsxsbOx0={RreVDo}IH zfY(3Pwv7jV0ImiP&H8K+NFgosv}yx5Sm->25Duz68<>3}-&SY@a@%YOFPwrBfa-Kp!a|JojI&-2N4C?_yi2#r~tTY0A?GjF$H<(m#Sii z72LH8s5=t$HE@GPW2#q}gi=?Feg|#Hrdb^<@b2^y@NFd4>>#&V6LXN~H8ckp(-QH9 zP#RNH0TSHMv0A-8*9>W4ZD;N6Y^unuF*_~bJFiv)WW85|yc@EC{rJ?x1i%{st0yk@Ok^toFA6@>~`A*O!H!3kXHFk{_LVN>7-Cj|vR z@Dbb=RX`&GDjG6QKcR%Af(xZ0XO*OsTds|)OP-}lN0Mu+;^{;((QsbSycAnUDOHt; z=^?MeDH*NglNpZEkLCG2iJ+AEo^@%N3m*?xlWfdLVPm(kRP}XhI0e9r)-y)q$VDSd z^U8#ze6W%;DmOPKD;7TF=7Cn0^Z|xieJD2=JI<%e-nz|MWQB-q;x;&471~P8m{7J7 zdnFX>ZDgKN@5keq$9$Aq!CgL>0-Q<1Q|K5rkd1{1gyo~mNQP8Kx!}wJoI&Qe(GR~PS)ETs3>{yRi5UTZ64LHiUvO_WIY^ici(*S`1 z(8|I=iAPMvkq!a7(ty!L0NgSWml$#>X+2Pim6R;YzFgLblX7KZM!BJCpgsFw$5UWX z7SXBuKl#8EwIF&hIk}+p|Rzi!o+>niwTyF^Fr#$u}5o|F9j4>OJIrP0@9T(Md zWGw7u^1#TbWjJIHk5q~v(k-|QVrv=-Z6+tjHX=1v$w#P|E>{M86>*z;$}X;!bKu4C zNMvgpe2jT;@u0&ElADKydz(Eb%GlIYMylafVai6hlq!xZhzPFHZc*_u?N1ZHFoUWb zSDi|=vzR~11ZH5^nK)Z#Xn{L3A}Dj17_U*3;8>Y7@i^k@_6G^E^x}9jj6SB=3l=6q zz*ff*Iw~=c7~Le6EaJ{>@}+>Jol{hDRWn`}bq0t|A|n7tnez4i&A{M`Qe0L{+5{62 zt_77Usv%uOU&d{qKM^I-vjdd~n3A%XmA&fs*HC}&6Oa6!VfHLsmYFtaA+vcsLK+-6 z+C(&+`QqcB-nXU9S*#dcKy2+cvt{&L3Jfv#p~$FVr;!G?+cEIUc+9%PpDC{6AU?p7 z$TB#r7zJKrd2vNFE;vW27*jft>Ik#Kl`uSzhsmPUYxEt(@IYd@RfrO*XkHL9%(uXh z0^J(J)tdgz7S503-;d8-y0*6IB4&&oUmJgCp`0d1mllpJKeM=SYDb!J-=3GN hH!km9rI)Y&V8@kHJFZY{hRv}BwtQp974_)W{{d$pa4G-* delta 750 zcmYjOUr1A76hG(Q{nce<(?myE>>;EYMU+UO1N-w3WNH#c*k#U*a&6>V69!t@Tlezj}a|2g1?ah(iJj& znQ^Qz zE+sbOE${`{K|7I!t^AO;D2ehPLX*k>VWl~p^M<-N9$qmBViG5`TldGxJB(ky+s7fSk(^9 zq|xDFISitwuu`TSW+keNuu^LXb4^hLS;;sEZHR~DRM{?hk~IQ%VJLGGxAM`FX3aC^ zRI%Q8ugZ9Lac$rOXwJHgD@zhKG5+hSu?a#GSD?aH{tIE_#` to patch the existing font). diff --git a/tools/material-symbols-subset/add_custom_glyphs.py b/tools/material-symbols-subset/add_custom_glyphs.py new file mode 100644 index 0000000000..158c98e438 --- /dev/null +++ b/tools/material-symbols-subset/add_custom_glyphs.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Append custom (non-Material) glyphs to the subset Material Symbols font. + +Some icons Amethyst needs are third-party logos that Google's Material +Symbols font will never carry (e.g. the OpenTimestamps stamp). Their traced +SVG outlines live in `custom/` and this script bakes them into the subset +TTF at private-use codepoints, so `MaterialSymbols.kt` can reference them +exactly like any Material glyph. + +subset.sh runs this automatically after pyftsubset; it can also be run +directly against an existing font: + + python3 add_custom_glyphs.py + +The SVGs are potrace output: the raw path coordinates are y-up already (the +file's group transform re-flips them for SVG display), so they map into font +space with a plain uniform scale + translate. +""" + +import os +import re +import sys + +from fontTools.pens.boundsPen import BoundsPen +from fontTools.pens.cu2quPen import Cu2QuPen +from fontTools.pens.transformPen import TransformPen +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.svgLib.path import parse_path +from fontTools.ttLib import TTFont + +HERE = os.path.dirname(os.path.abspath(__file__)) + +# codepoint -> (glyph name, traced SVG). Keep codepoints at the very end of +# the BMP private-use area (U+F8xx tail) to stay clear of the range Material +# Symbols actually allocates. +CUSTOM = { + 0xF8F0: ("opentimestamps", os.path.join(HERE, "custom", "opentimestamps.svg")), +} + +# Fit content into the same square Material Symbols draw in (its check_circle +# glyph spans 80..880 in a 960 upm font). Scaled to the font's real upm. +BOX_MIN_FRAC = 80 / 960 +BOX_MAX_FRAC = 880 / 960 + + +def svg_path_data(svg_file): + with open(svg_file, "r", encoding="utf-8") as f: + svg = f.read() + return re.findall(r']*\bd="([^"]+)"', svg) + + +def draw_svg(paths, pen): + for d in paths: + parse_path(d, pen) + + +def build_glyph(paths, upm): + # First pass: raw outline bounds. + bounds = BoundsPen(None) + draw_svg(paths, bounds) + x_min, y_min, x_max, y_max = bounds.bounds + + # Uniform scale into the Material content box, centered on both axes. + box_min = BOX_MIN_FRAC * upm + box_max = BOX_MAX_FRAC * upm + box = box_max - box_min + scale = box / max(x_max - x_min, y_max - y_min) + tx = box_min + (box - (x_max - x_min) * scale) / 2 - x_min * scale + ty = box_min + (box - (y_max - y_min) * scale) / 2 - y_min * scale + + tt_pen = TTGlyphPen(None) + quad_pen = Cu2QuPen(tt_pen, max_err=1.0) + draw_svg(paths, TransformPen(quad_pen, (scale, 0, 0, scale, tx, ty))) + return tt_pen.glyph() + + +def main(font_path): + font = TTFont(font_path) + upm = font["head"].unitsPerEm + order = font.getGlyphOrder() + + for codepoint, (name, svg_file) in sorted(CUSTOM.items()): + glyph = build_glyph(svg_path_data(svg_file), upm) + + if name not in order: + order.append(name) + font.setGlyphOrder(order) + font["glyf"][name] = glyph + glyph.recalcBounds(font["glyf"]) + font["hmtx"][name] = (upm, glyph.xMin) + for table in font["cmap"].tables: + if table.isUnicode(): + table.cmap[codepoint] = name + print(f"Added {name} at U+{codepoint:04X}") + + font["maxp"].numGlyphs = len(order) + font.save(font_path) + print(f"Wrote {font_path}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(__doc__) + main(sys.argv[1]) diff --git a/tools/material-symbols-subset/custom/opentimestamps.svg b/tools/material-symbols-subset/custom/opentimestamps.svg new file mode 100644 index 0000000000..054387593f --- /dev/null +++ b/tools/material-symbols-subset/custom/opentimestamps.svg @@ -0,0 +1,130 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/tools/material-symbols-subset/subset.sh b/tools/material-symbols-subset/subset.sh index 0689a78aec..640964d342 100755 --- a/tools/material-symbols-subset/subset.sh +++ b/tools/material-symbols-subset/subset.sh @@ -59,4 +59,8 @@ pyftsubset "$SOURCE_TTF" \ --glyph-names \ --symbol-cmap +# Bake in the custom (non-Material) glyphs from custom/ — third-party logos +# like OpenTimestamps that the upstream font will never carry. +python3 "$REPO_ROOT/tools/material-symbols-subset/add_custom_glyphs.py" "$TARGET" + echo "Wrote $TARGET ($(du -h "$TARGET" | cut -f1))"