From dbf9560486dd0373b40d28c3746c687b450a24a6 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sat, 13 Jun 2026 10:37:25 +0000 Subject: [PATCH] Add release tooling and generated launcher icon --- README.md | 25 +- .../runtime/render/ClockLayout.java | 13 +- .../res/drawable/ic_launcher_background.xml | 164 +----- .../res/drawable/ic_launcher_foreground.xml | 125 ++++- .../res/drawable/ic_launcher_monochrome.xml | 100 ++++ .../main/res/mipmap-anydpi/ic_launcher.xml | 4 +- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 1404 -> 0 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 982 -> 0 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 1900 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 2884 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 3844 -> 0 bytes scripts/generate_launcher_icon.py | 505 ++++++++++++++++++ scripts/release-gitea.sh | 185 +++++++ 13 files changed, 928 insertions(+), 193 deletions(-) create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100755 scripts/generate_launcher_icon.py create mode 100755 scripts/release-gitea.sh diff --git a/README.md b/README.md index 22d1571..c47924e 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,28 @@ The project is a standard Gradle Android project: ./gradlew assembleDebug ``` -For local release builds, `build.sh` uses the Android SDK and JDK available on -the machine, writes `local.properties`, builds a release APK, and copies it to a -local sync folder if present. +Release builds require local signing configuration: + +```bash +./gradlew assembleRelease +``` + +## Releases + +Public releases use two-part versions such as `0.1`, `0.2`, and eventually +`1.0`. + +Gitea automatically provides source archives for release tags, so the release +assets only need the APK and its checksum. A release can be created with: + +```bash +mkdir -p local +printf '%s' 'your-gitea-token' > local/gitea-token + +GITEA_BASE_URL=git.ajpanton.se \ +GITEA_TOKEN_FILE=local/gitea-token \ +scripts/release-gitea.sh 0.1 +``` ## Development Note diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java index e4569e8..05c7116 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java @@ -12,6 +12,7 @@ import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition; +import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; final class ClockLayout { private static final int MIDDLE_COLLISION_MARGIN_PX = 2; @@ -103,6 +104,7 @@ final class ClockLayout { measuredRows = positionRowsFromLogicalSteps( measuredRows, Math.max(1, autoGapSteps), + Math.max(1, baseHeightSteps), Math.max(1, statusbarHeight), verticalOffsetSteps, bottomRowBaselineAnchorY); @@ -216,6 +218,7 @@ final class ClockLayout { private static ArrayList positionRowsFromLogicalSteps( ArrayList measuredRows, int autoGapSteps, + int baseHeightSteps, int statusbarHeight, int verticalOffsetSteps, int bottomRowBaselineAnchorY @@ -238,7 +241,10 @@ final class ClockLayout { } } else { MeasuredClockRow nextRow = measuredRows.get(rowIndex + 1); - int gapSteps = resolveGapSteps(nextRow.source.gapBeforePx, autoGapSteps); + int gapSteps = resolveGapSteps( + nextRow.source.gapBeforePx, + autoGapSteps, + baseHeightSteps); topSteps = nextBottomSteps - gapSteps - Math.max(1, row.logicalHeightSteps); bottomPx = nextBottomPx - stepsToPx(gapSteps, statusbarHeight); } @@ -657,11 +663,12 @@ final class ClockLayout { return max > 0f ? max : 1f; } - private static int resolveGapSteps(int gapBeforeSteps, int autoGapSteps) { + private static int resolveGapSteps(int gapBeforeSteps, int autoGapSteps, int baseHeightSteps) { if (gapBeforeSteps == ClockLayoutTextFactory.ROW_GAP_AUTO) { return Math.max(1, autoGapSteps); } - return gapBeforeSteps; + int defaultSteps = Math.max(1, SbtDefaults.CLOCK_TEXT_SIZE_STEPS_DEFAULT); + return Math.round(gapBeforeSteps * Math.max(1, baseHeightSteps) / (float) defaultSteps); } private static int stepsToPx(int steps, int statusbarHeight) { diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..5dd4ed1 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,166 +5,6 @@ android:viewportWidth="108" android:viewportHeight="108"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:fillColor="#0D0B08" + android:pathData="M0,0 L108,0 L108,108 L0,108 Z" /> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2b068d1..0357924 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,109 @@ + - - - - - - - - + + + + + + + + + + - \ No newline at end of file + android:strokeLineCap="round" + android:strokeLineJoin="round" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..8befb02 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml index 6f3b755..8fde456 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -2,5 +2,5 @@ - - \ No newline at end of file + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ecd372343283f4157dcfd918ec5165bb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e58ba64d180ce43ee13bf9a17835fbca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!To6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9f036a47549d47db79c16788749dca10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2884 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 str: + if r <= 0: + return rect(x, y, w, h) + r = min(r, w / 2, h / 2) + return ( + f"M{x + r:g},{y:g} L{x + w - r:g},{y:g} " + f"A{r:g},{r:g} 0,0 1 {x + w:g},{y + r:g} " + f"L{x + w:g},{y + h - r:g} " + f"A{r:g},{r:g} 0,0 1 {x + w - r:g},{y + h:g} " + f"L{x + r:g},{y + h:g} " + f"A{r:g},{r:g} 0,0 1 {x:g},{y + h - r:g} " + f"L{x:g},{y + r:g} " + f"A{r:g},{r:g} 0,0 1 {x + r:g},{y:g} Z" + ) + + +def rect(x: float, y: float, w: float, h: float) -> str: + return f"M{x:g},{y:g} L{x + w:g},{y:g} L{x + w:g},{y + h:g} L{x:g},{y + h:g} Z" + + +def circle(cx: float, cy: float, r: float) -> str: + return ( + f"M{cx - r:g},{cy:g} " + f"A{r:g},{r:g} 0,1 0 {cx + r:g},{cy:g} " + f"A{r:g},{r:g} 0,1 0 {cx - r:g},{cy:g}" + ) + + +def path(data: str, fill: str = "#00000000", *, stroke: str | None = None, + stroke_width: float | None = None, line_cap: str | None = None, + line_join: str | None = None, comment: str | None = None) -> IconPath: + return IconPath(data, PathStyle(fill, stroke, stroke_width, line_cap, line_join), comment) + + +def stroke_path(data: str, color: str, width: float, *, comment: str | None = None) -> IconPath: + return path( + data, + stroke=color, + stroke_width=width, + line_cap="round", + line_join="round", + comment=comment, + ) + + +def seven_segment_digit(x: float, y: float, digit: str, scale: float) -> str: + segments = { + "0": "abcfed", + "1": "bc", + "2": "abged", + "3": "abgcd", + "4": "fgbc", + "5": "afgcd", + "6": "afgecd", + "7": "abc", + "8": "abcdefg", + "9": "abfgcd", + }[digit] + w = 5 * scale + h = 10 * scale + mid = y + h / 2 + left = x + right = x + w + top = y + bottom = y + h + coords = { + "a": f"M{left:g},{top:g} L{right:g},{top:g}", + "b": f"M{right:g},{top:g} L{right:g},{mid:g}", + "c": f"M{right:g},{mid:g} L{right:g},{bottom:g}", + "d": f"M{left:g},{bottom:g} L{right:g},{bottom:g}", + "e": f"M{left:g},{mid:g} L{left:g},{bottom:g}", + "f": f"M{left:g},{top:g} L{left:g},{mid:g}", + "g": f"M{left:g},{mid:g} L{right:g},{mid:g}", + } + return " ".join(coords[s] for s in segments) + + +def colon(x: float, y: float, scale: float) -> str: + r = 1.1 * scale + return circle(x, y + 3 * scale, r) + " " + circle(x, y + 7 * scale, r) + + +def cutout_path() -> str: + cx = CUTOUT_CX + top = CUTOUT_TOP + bottom = CUTOUT_BOTTOM + stem_half = CUTOUT_STEM_HALF_WIDTH + return ( + f"M{cx - stem_half:g},{top:g} " + f"L{cx + stem_half:g},{top:g} " + f"L{cx + stem_half:g},{bottom - stem_half:g} " + f"C{cx + stem_half:g},{bottom:g} {cx - stem_half:g},{bottom:g} {cx - stem_half:g},{bottom - stem_half:g} " + f"Z" + ) + + +def envelope_path() -> str: + x = ENVELOPE_X + y = ENVELOPE_Y + w = ENVELOPE_W + h = ENVELOPE_H + return ( + f"M{x:g},{y:g} L{x + w:g},{y:g} L{x + w:g},{y + h:g} L{x:g},{y + h:g} Z " + f"M{x:g},{y:g} L{x + w / 2:g},{y + h * 0.64:g} L{x + w:g},{y:g}" + ) + + +def battery_icon_paths(fill: str, stroke: str) -> list[IconPath]: + x = BATTERY_ICON_X + y = BATTERY_ICON_Y + w = BATTERY_ICON_W + h = BATTERY_ICON_H + r = BATTERY_ICON_RADIUS + protrusion_w = BATTERY_ICON_PROTRUSION_W + protrusion_h = BATTERY_ICON_PROTRUSION_H + gauge_pad = max(1.0, BATTERY_ICON_STROKE + 0.4) + gauge_w = max(0.0, (w - gauge_pad * 2) * BATTERY_ICON_LEVEL) + protrusion_y = y + (h - protrusion_h) / 2 + return [ + stroke_path( + round_rect(x, y, w, h, r) + + " " + + rect(x + w, protrusion_y, protrusion_w, protrusion_h), + stroke, + BATTERY_ICON_STROKE, + comment="Battery outline", + ), + path(rect(x + gauge_pad, y + gauge_pad, gauge_w, max(0.0, h - gauge_pad * 2)), fill, + comment="Battery fill"), + ] + + +def day_text_path() -> str: + x = DAY_TEXT_X + y = DAY_TEXT_Y + h = DAY_TEXT_H + lower_h = h * DAY_TEXT_LOWERCASE_SCALE + lower_top = y + h - lower_h + spacing = DAY_TEXT_LETTER_SPACING + + m_w = h * 0.62 + o_w = lower_h * 0.58 + n_w = lower_h * 0.58 + o_x = x + m_w + spacing + n_x = o_x + o_w + spacing + return ( + # M + f"M{x:g},{y + h:g} L{x:g},{y:g} L{x + m_w / 2:g},{y + h * 0.58:g} " + f"L{x + m_w:g},{y:g} L{x + m_w:g},{y + h:g} " + # o + f"M{o_x:g},{lower_top + lower_h / 2:g} " + f"A{o_w / 2:g},{lower_h / 2:g} 0,1 0 {o_x + o_w:g},{lower_top + lower_h / 2:g} " + f"A{o_w / 2:g},{lower_h / 2:g} 0,1 0 {o_x:g},{lower_top + lower_h / 2:g} " + # n + f"M{n_x:g},{y + h:g} L{n_x:g},{lower_top:g} " + f"C{n_x + n_w * 0.55:g},{lower_top:g} {n_x + n_w:g},{lower_top + lower_h * 0.2:g} " + f"{n_x + n_w:g},{y + h:g}" + ) + + +def wifi_paths(color: str) -> list[IconPath]: + x = WIFI_X + y = WIFI_Y + w = WIFI_W + h = WIFI_H + return [ + stroke_path( + f"M{x:g},{y:g} C{x + w * 0.28:g},{y - h * 0.75:g} " + f"{x + w * 0.72:g},{y - h * 0.75:g} {x + w:g},{y:g} " + f"M{x + w * 0.22:g},{y + h * 0.5:g} C{x + w * 0.4:g},{y + h * 0.1:g} " + f"{x + w * 0.6:g},{y + h * 0.1:g} {x + w * 0.78:g},{y + h * 0.5:g}", + color, + WIFI_STROKE, + comment="Wi-Fi", + ), + path(circle(x + w / 2, y + h, WIFI_DOT_R), color, comment="Wi-Fi dot"), + ] + + +def signal_path() -> str: + pieces = [] + for index, height in enumerate(SIGNAL_BAR_HEIGHTS): + x = SIGNAL_X + index * (SIGNAL_BAR_W + SIGNAL_BAR_SPACING) + pieces.append(rect(x, SIGNAL_BOTTOM - height, SIGNAL_BAR_W, height)) + return " ".join(pieces) + + +def clock_paths() -> list[IconPath]: + y = 42.5 + x = 25 + s = 1.0 + small = 0.72 + pieces = [ + seven_segment_digit(x, y, "1", s), + seven_segment_digit(x + 8, y, "2", s), + seven_segment_digit(x + 20, y, "3", s), + seven_segment_digit(x + 28, y, "4", s), + ] + small_x = x + 40 + pieces.extend([ + seven_segment_digit(small_x, y + 2.6, "5", small), + seven_segment_digit(small_x + 6, y + 2.6, "6", small), + ]) + return [ + stroke_path(" ".join(pieces), COLOR_WHITE, 1.55, comment="Clock 12:34"), + path(colon(x + 16.5, y, s) + " " + colon(small_x - 3.5, y + 2.6, small), COLOR_WHITE, + comment="Clock colons"), + ] + + +def statusbar_paths(monochrome: bool) -> list[IconPath]: + white = COLOR_WHITE + status_fill = COLOR_WHITE if monochrome else COLOR_STATUS + cutout_fill = COLOR_WHITE if monochrome else COLOR_CUTOUT + paths: list[IconPath] = [] + paths.append(path(rect(0, 0, VIEWPORT, STATUS_BOTTOM), status_fill, comment="Statusbar panel")) + paths.append(path(cutout_path(), cutout_fill, comment="Camera cutout")) + + # Row 1. + paths.append(stroke_path(envelope_path(), white, ENVELOPE_STROKE, comment="Envelope")) + paths.extend(battery_icon_paths(white, white)) + + # Row 2. + paths.append(stroke_path(day_text_path(), white, DAY_TEXT_STROKE, comment="Mon")) + paths.extend(wifi_paths(white)) + paths.append(path(signal_path(), white, comment="Cellular bars")) + + # Row 3. + paths.extend(clock_paths()) + return paths + + +def battery_bar_paths(monochrome: bool) -> list[IconPath]: + green = COLOR_WHITE if monochrome else COLOR_GREEN + track = COLOR_WHITE if monochrome else COLOR_BAR_TRACK + track_w = max(0.0, BATTERY_BAR_RIGHT - BATTERY_BAR_LEFT) + fill_w = track_w * BATTERY_BAR_LEVEL + return [ + path(round_rect(BATTERY_BAR_LEFT, BATTERY_BAR_Y, track_w, BATTERY_BAR_H, 3), track, + comment="Battery bar track"), + path(round_rect(BATTERY_BAR_LEFT + 1, BATTERY_BAR_Y + 1, max(0.0, fill_w - 2), BATTERY_BAR_H - 2, 2), green, + comment="Battery bar fill"), + ] + + +def wordmark_paths(monochrome: bool) -> list[IconPath]: + x = WORDMARK_X + y = WORDMARK_Y + w = WORDMARK_W + h = WORDMARK_H + gap = w * 0.075 + s_w = w * 0.22 + b_w = w * 0.24 + t_w = w * 0.26 + s_x = x + b_x = s_x + s_w + gap + t_x = b_x + b_w + gap + mid = y + h * 0.50 + cut = min(b_w * 0.28, h * 0.18) + data = ( + f"M{s_x + s_w:g},{y:g} L{s_x:g},{y:g} L{s_x:g},{mid:g} L{s_x + s_w:g},{mid:g} " + f"L{s_x + s_w:g},{y + h:g} L{s_x:g},{y + h:g} " + f"M{b_x:g},{y:g} L{b_x:g},{y + h:g} " + f"M{b_x:g},{y:g} L{b_x + b_w - cut:g},{y:g} L{b_x + b_w:g},{y + cut:g} " + f"L{b_x + b_w:g},{mid - cut:g} L{b_x + b_w - cut:g},{mid:g} L{b_x:g},{mid:g} " + f"M{b_x:g},{mid:g} L{b_x + b_w - cut:g},{mid:g} L{b_x + b_w:g},{mid + cut:g} " + f"L{b_x + b_w:g},{y + h - cut:g} L{b_x + b_w - cut:g},{y + h:g} L{b_x:g},{y + h:g} " + f"M{t_x:g},{y:g} L{t_x + t_w:g},{y:g} M{t_x + t_w / 2:g},{y:g} L{t_x + t_w / 2:g},{y + h:g}" + ) + if monochrome: + return [stroke_path(data, COLOR_WHITE, WORDMARK_STROKE_OUTER, comment="SBT wordmark")] + return [ + stroke_path(data, COLOR_ORANGE, WORDMARK_STROKE_OUTER, comment="SBT wordmark outer"), + stroke_path(data, COLOR_WARM_WHITE, WORDMARK_STROKE_INNER, comment="SBT wordmark inner"), + ] + + +def foreground_paths(monochrome: bool = False) -> list[IconPath]: + return statusbar_paths(monochrome) + battery_bar_paths(monochrome) + wordmark_paths(monochrome) + + +def background_paths() -> list[IconPath]: + return [ + path(rect(0, 0, VIEWPORT, VIEWPORT), COLOR_BG), + ] + + +def android_path_xml(icon_path: IconPath) -> str: + attrs = [ + f'android:fillColor="{icon_path.style.fill}"', + f'android:pathData="{escape(icon_path.data)}"', + ] + if icon_path.style.stroke: + attrs.append(f'android:strokeColor="{icon_path.style.stroke}"') + if icon_path.style.stroke_width is not None: + attrs.append(f'android:strokeWidth="{icon_path.style.stroke_width:g}"') + if icon_path.style.line_cap: + attrs.append(f'android:strokeLineCap="{icon_path.style.line_cap}"') + if icon_path.style.line_join: + attrs.append(f'android:strokeLineJoin="{icon_path.style.line_join}"') + return " " + + +def write_vector(destination: Path, paths: list[IconPath]) -> None: + body = "\n".join( + (f"\n \n" if p.comment else "\n") + android_path_xml(p) + for p in paths + ) + destination.write_text( + '\n' + '' + f'{body}\n' + '\n', + encoding="utf-8", + ) + + +def svg_path_xml(icon_path: IconPath) -> str: + style = [f'fill="{icon_path.style.fill}"'] + if icon_path.style.stroke: + style.append(f'stroke="{icon_path.style.stroke}"') + if icon_path.style.stroke_width is not None: + style.append(f'stroke-width="{icon_path.style.stroke_width:g}"') + if icon_path.style.line_cap: + style.append(f'stroke-linecap="{icon_path.style.line_cap}"') + if icon_path.style.line_join: + style.append(f'stroke-linejoin="{icon_path.style.line_join}"') + return f'' + + +def write_preview() -> None: + PREVIEW.parent.mkdir(parents=True, exist_ok=True) + paths = background_paths() + foreground_paths(False) + body = "\n ".join(svg_path_xml(p) for p in paths) + panel_bg = '' + square = f'\n {panel_bg}\n {body}\n' + rounded = ( + '\n' + ' \n' + f' {panel_bg}\n {body}\n' + ' \n' + '\n' + '' + ) + circle_preview = ( + '\n' + ' \n' + f' {panel_bg}\n {body}\n' + ' \n' + '\n' + f'' + ) + PREVIEW.write_text( + '\n' + ' \n' + ' \n' + f' \n' + ' \n' + ' \n' + f' {square}\n' + f' {rounded}\n' + f' {circle_preview}\n' + ' square\n' + ' rounded\n' + ' circle\n' + '\n', + encoding="utf-8", + ) + + +def main() -> None: + write_vector(BACKGROUND, background_paths()) + write_vector(FOREGROUND, foreground_paths(False)) + write_vector(MONOCHROME, foreground_paths(True)) + write_preview() + print(f"Wrote {FOREGROUND.relative_to(ROOT)}") + print(f"Wrote {MONOCHROME.relative_to(ROOT)}") + print(f"Wrote {BACKGROUND.relative_to(ROOT)}") + print(f"Wrote {PREVIEW.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/scripts/release-gitea.sh b/scripts/release-gitea.sh new file mode 100755 index 0000000..3d57f30 --- /dev/null +++ b/scripts/release-gitea.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: + GITEA_BASE_URL=git.example.com GITEA_TOKEN_FILE=local/gitea-token scripts/release-gitea.sh 0.1 + +Creates a Gitea release for the current HEAD and uploads: + - the signed release APK + - a SHA-256 checksum file for the APK + +Environment: + GITEA_BASE_URL Base URL of the Gitea instance, for example git.example.com + GITEA_TOKEN Gitea access token with repository write/release permissions + GITEA_TOKEN_FILE Optional file containing the token, used when GITEA_TOKEN is unset + GITEA_OWNER Optional repository owner override. Defaults to origin remote owner. + GITEA_REPO Optional repository name override. Defaults to origin remote repo. + VERSION_CODE Optional Android versionCode override. + +The script requires a clean working tree and pushes the current branch and tag. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +version="${1:-}" +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+$ ]]; then + usage >&2 + exit 2 +fi + +: "${GITEA_BASE_URL:?Set GITEA_BASE_URL, for example git.example.com}" + +if [[ -z "${GITEA_TOKEN:-}" && -n "${GITEA_TOKEN_FILE:-}" ]]; then + if [[ ! -f "$GITEA_TOKEN_FILE" ]]; then + echo "GITEA_TOKEN_FILE does not exist: ${GITEA_TOKEN_FILE}" >&2 + exit 1 + fi + GITEA_TOKEN="$(tr -d '\r\n' < "$GITEA_TOKEN_FILE")" +fi +: "${GITEA_TOKEN:?Set GITEA_TOKEN or GITEA_TOKEN_FILE to a Gitea access token}" + +require_clean_tree() { + if [[ -n "$(git status --porcelain)" ]]; then + echo "Working tree is not clean. Commit or stash changes before releasing." >&2 + exit 1 + fi +} + +remote_repo_path() { + local remote_url + remote_url="$(git remote get-url origin)" + remote_url="${remote_url%.git}" + case "$remote_url" in + ssh://*) + remote_url="${remote_url#ssh://}" + remote_url="${remote_url#*/}" + ;; + http://*|https://*) + remote_url="${remote_url#http://}" + remote_url="${remote_url#https://}" + remote_url="${remote_url#*/}" + ;; + *@*:*) + remote_url="${remote_url#*:}" + ;; + esac + printf '%s\n' "$remote_url" +} + +json_string() { + python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1" +} + +extract_json_id() { + python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' +} + +version_code_for() { + local major minor + IFS=. read -r major minor <<<"$1" + printf '%d\n' "$((major * 10000 + minor * 100))" +} + +urlencode() { + python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$1" +} + +normalize_base_url() { + local base_url + base_url="${1%/}" + case "$base_url" in + http://*|https://*) ;; + *) base_url="https://${base_url}" ;; + esac + printf '%s\n' "$base_url" +} + +require_clean_tree + +tag="v${version}" +release_name="StatusBarTweak ${version}" +version_code="${VERSION_CODE:-$(version_code_for "$version")}" +repo_path="$(remote_repo_path)" +owner="${GITEA_OWNER:-${repo_path%%/*}}" +repo="${GITEA_REPO:-${repo_path##*/}}" +gitea_base_url="$(normalize_base_url "$GITEA_BASE_URL")" +api_base="${gitea_base_url}/api/v1" + +if [[ "$owner" == "$repo_path" || -z "$owner" || -z "$repo" ]]; then + echo "Could not infer owner/repo from origin. Set GITEA_OWNER and GITEA_REPO." >&2 + exit 1 +fi + +git fetch origin --tags +if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "Tag ${tag} already exists." >&2 + exit 1 +fi + +./gradlew clean assembleRelease \ + -PsbtVersionName="$version" \ + -PsbtVersionCode="$version_code" + +apk_source="app/build/outputs/apk/release/app-release.apk" +if [[ ! -f "$apk_source" ]]; then + echo "Release APK not found at ${apk_source}." >&2 + exit 1 +fi + +release_dir="build/release/${tag}" +apk_name="StatusBarTweak-${version}.apk" +checksum_name="${apk_name}.sha256" +mkdir -p "$release_dir" +cp "$apk_source" "${release_dir}/${apk_name}" +(cd "$release_dir" && sha256sum "$apk_name" > "$checksum_name") + +git tag -a "$tag" -m "$release_name" +git push origin HEAD +git push origin "$tag" + +body="Tested primarily on Samsung Galaxy Fold 5 running One UI 8.0 / Android 16." +payload="$( + cat </dev/null +} + +upload_asset "${release_dir}/${apk_name}" +upload_asset "${release_dir}/${checksum_name}" + +echo "Created release ${tag}: ${gitea_base_url}/${owner}/${repo}/releases/tag/${tag}"