Add release tooling and generated launcher icon

This commit is contained in:
ajp_anton
2026-06-13 10:37:25 +00:00
parent fbab6dac6a
commit dbf9560486
13 changed files with 928 additions and 193 deletions
+22 -3
View File
@@ -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
@@ -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<MeasuredClockRow> positionRowsFromLogicalSteps(
ArrayList<MeasuredClockRow> 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) {
@@ -5,166 +5,6 @@
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
android:fillColor="#0D0B08"
android:pathData="M0,0 L108,0 L108,108 L0,108 Z" />
</vector>
@@ -1,30 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<!-- Statusbar panel -->
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:fillColor="#77746E"
android:pathData="M0,0 L108,0 L108,54 L0,54 Z" />
<!-- Camera cutout -->
<path
android:fillColor="#050403"
android:pathData="M51.8,0 L56.2,0 L56.2,36.8 C56.2,39 51.8,39 51.8,36.8 Z" />
<!-- Envelope -->
<path
android:fillColor="#00000000"
android:pathData="M39,22.5 L50,22.5 L50,30.5 L39,30.5 Z M39,22.5 L44.5,27.62 L50,22.5"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.3"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Battery outline -->
<path
android:fillColor="#00000000"
android:pathData="M59,23 L70,23 A1,1 0,0 1 71,24 L71,29.7 A1,1 0,0 1 70,30.7 L59,30.7 A1,1 0,0 1 58,29.7 L58,24 A1,1 0,0 1 59,23 Z M71,25.35 L73,25.35 L73,28.35 L71,28.35 Z"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1"
android:strokeColor="#00000000" />
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Battery fill -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M59.4,24.4 L66.54,24.4 L66.54,29.3 L59.4,29.3 Z" />
<!-- Mon -->
<path
android:fillColor="#00000000"
android:pathData="M31,39.6 L31,31.6 L33.48,36.24 L35.96,31.6 L35.96,39.6 M37.96,36.32 A1.9024,3.28 0,1 0 41.7648,36.32 A1.9024,3.28 0,1 0 37.96,36.32 M43.7648,39.6 L43.7648,33.04 C45.8574,33.04 47.5696,34.352 47.5696,39.6"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.1"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Wi-Fi -->
<path
android:fillColor="#00000000"
android:pathData="M61,36.5 C63.8,32.75 68.2,32.75 71,36.5 M63.2,39 C65,37 67,37 68.8,39"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.3"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Wi-Fi dot -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M65.1,41.5 A0.9,0.9 0,1 0 66.9,41.5 A0.9,0.9 0,1 0 65.1,41.5" />
<!-- Cellular bars -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M73,39.7 L74.8,39.7 L74.8,42.5 L73,42.5 Z M76,36.9 L77.8,36.9 L77.8,42.5 L76,42.5 Z M79,34.1 L80.8,34.1 L80.8,42.5 L79,42.5 Z" />
<!-- Clock 12:34 -->
<path
android:fillColor="#00000000"
android:pathData="M30,42.5 L30,47.5 M30,47.5 L30,52.5 M33,42.5 L38,42.5 M38,42.5 L38,47.5 M33,47.5 L38,47.5 M33,47.5 L33,52.5 M33,52.5 L38,52.5 M45,42.5 L50,42.5 M50,42.5 L50,47.5 M45,47.5 L50,47.5 M50,47.5 L50,52.5 M45,52.5 L50,52.5 M53,42.5 L53,47.5 M53,47.5 L58,47.5 M58,42.5 L58,47.5 M58,47.5 L58,52.5 M65,45.1 L68.6,45.1 M65,45.1 L65,48.7 M65,48.7 L68.6,48.7 M68.6,48.7 L68.6,52.3 M65,52.3 L68.6,52.3 M71,45.1 L74.6,45.1 M71,45.1 L71,48.7 M71,48.7 L74.6,48.7 M71,48.7 L71,52.3 M74.6,48.7 L74.6,52.3 M71,52.3 L74.6,52.3"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.55"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Clock colons -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M40.4,45.5 A1.1,1.1 0,1 0 42.6,45.5 A1.1,1.1 0,1 0 40.4,45.5 M40.4,49.5 A1.1,1.1 0,1 0 42.6,49.5 A1.1,1.1 0,1 0 40.4,49.5 M60.708,47.26 A0.792,0.792 0,1 0 62.292,47.26 A0.792,0.792 0,1 0 60.708,47.26 M60.708,50.14 A0.792,0.792 0,1 0 62.292,50.14 A0.792,0.792 0,1 0 60.708,50.14" />
<!-- Battery bar track -->
<path
android:fillColor="#2A2116"
android:pathData="M3,54 L105,54 A3,3 0,0 1 108,57 L108,57 A3,3 0,0 1 105,60 L3,60 A3,3 0,0 1 0,57 L0,57 A3,3 0,0 1 3,54 Z" />
<!-- Battery bar fill -->
<path
android:fillColor="#2FDB72"
android:pathData="M3,55 L72.6,55 A2,2 0,0 1 74.6,57 L74.6,57 A2,2 0,0 1 72.6,59 L3,59 A2,2 0,0 1 1,57 L1,57 A2,2 0,0 1 3,55 Z" />
<!-- SBT wordmark outer -->
<path
android:fillColor="#00000000"
android:pathData="M46,62.8 L35,62.8 L35,72.8 L46,72.8 L46,82.8 L35,82.8 M49.75,62.8 L49.75,82.8 M49.75,62.8 L58.39,62.8 L61.75,66.16 L61.75,69.44 L58.39,72.8 L49.75,72.8 M49.75,72.8 L58.39,72.8 L61.75,76.16 L61.75,79.44 L58.39,82.8 L49.75,82.8 M65.5,62.8 L78.5,62.8 M72,62.8 L72,82.8"
android:strokeColor="#FF9F1A"
android:strokeWidth="5"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- SBT wordmark inner -->
<path
android:fillColor="#00000000"
android:pathData="M46,62.8 L35,62.8 L35,72.8 L46,72.8 L46,82.8 L35,82.8 M49.75,62.8 L49.75,82.8 M49.75,62.8 L58.39,62.8 L61.75,66.16 L61.75,69.44 L58.39,72.8 L49.75,72.8 M49.75,72.8 L58.39,72.8 L61.75,76.16 L61.75,79.44 L58.39,82.8 L49.75,82.8 M65.5,62.8 L78.5,62.8 M72,62.8 L72,82.8"
android:strokeColor="#FFF5DC"
android:strokeWidth="1.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Statusbar panel -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M0,0 L108,0 L108,54 L0,54 Z" />
<!-- Camera cutout -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M51.8,0 L56.2,0 L56.2,36.8 C56.2,39 51.8,39 51.8,36.8 Z" />
<!-- Envelope -->
<path
android:fillColor="#00000000"
android:pathData="M39,22.5 L50,22.5 L50,30.5 L39,30.5 Z M39,22.5 L44.5,27.62 L50,22.5"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.3"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Battery outline -->
<path
android:fillColor="#00000000"
android:pathData="M59,23 L70,23 A1,1 0,0 1 71,24 L71,29.7 A1,1 0,0 1 70,30.7 L59,30.7 A1,1 0,0 1 58,29.7 L58,24 A1,1 0,0 1 59,23 Z M71,25.35 L73,25.35 L73,28.35 L71,28.35 Z"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Battery fill -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M59.4,24.4 L66.54,24.4 L66.54,29.3 L59.4,29.3 Z" />
<!-- Mon -->
<path
android:fillColor="#00000000"
android:pathData="M31,39.6 L31,31.6 L33.48,36.24 L35.96,31.6 L35.96,39.6 M37.96,36.32 A1.9024,3.28 0,1 0 41.7648,36.32 A1.9024,3.28 0,1 0 37.96,36.32 M43.7648,39.6 L43.7648,33.04 C45.8574,33.04 47.5696,34.352 47.5696,39.6"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.1"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Wi-Fi -->
<path
android:fillColor="#00000000"
android:pathData="M61,36.5 C63.8,32.75 68.2,32.75 71,36.5 M63.2,39 C65,37 67,37 68.8,39"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.3"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Wi-Fi dot -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M65.1,41.5 A0.9,0.9 0,1 0 66.9,41.5 A0.9,0.9 0,1 0 65.1,41.5" />
<!-- Cellular bars -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M73,39.7 L74.8,39.7 L74.8,42.5 L73,42.5 Z M76,36.9 L77.8,36.9 L77.8,42.5 L76,42.5 Z M79,34.1 L80.8,34.1 L80.8,42.5 L79,42.5 Z" />
<!-- Clock 12:34 -->
<path
android:fillColor="#00000000"
android:pathData="M30,42.5 L30,47.5 M30,47.5 L30,52.5 M33,42.5 L38,42.5 M38,42.5 L38,47.5 M33,47.5 L38,47.5 M33,47.5 L33,52.5 M33,52.5 L38,52.5 M45,42.5 L50,42.5 M50,42.5 L50,47.5 M45,47.5 L50,47.5 M50,47.5 L50,52.5 M45,52.5 L50,52.5 M53,42.5 L53,47.5 M53,47.5 L58,47.5 M58,42.5 L58,47.5 M58,47.5 L58,52.5 M65,45.1 L68.6,45.1 M65,45.1 L65,48.7 M65,48.7 L68.6,48.7 M68.6,48.7 L68.6,52.3 M65,52.3 L68.6,52.3 M71,45.1 L74.6,45.1 M71,45.1 L71,48.7 M71,48.7 L74.6,48.7 M71,48.7 L71,52.3 M74.6,48.7 L74.6,52.3 M71,52.3 L74.6,52.3"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="1.55"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- Clock colons -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M40.4,45.5 A1.1,1.1 0,1 0 42.6,45.5 A1.1,1.1 0,1 0 40.4,45.5 M40.4,49.5 A1.1,1.1 0,1 0 42.6,49.5 A1.1,1.1 0,1 0 40.4,49.5 M60.708,47.26 A0.792,0.792 0,1 0 62.292,47.26 A0.792,0.792 0,1 0 60.708,47.26 M60.708,50.14 A0.792,0.792 0,1 0 62.292,50.14 A0.792,0.792 0,1 0 60.708,50.14" />
<!-- Battery bar track -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,54 L105,54 A3,3 0,0 1 108,57 L108,57 A3,3 0,0 1 105,60 L3,60 A3,3 0,0 1 0,57 L0,57 A3,3 0,0 1 3,54 Z" />
<!-- Battery bar fill -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M3,55 L72.6,55 A2,2 0,0 1 74.6,57 L74.6,57 A2,2 0,0 1 72.6,59 L3,59 A2,2 0,0 1 1,57 L1,57 A2,2 0,0 1 3,55 Z" />
<!-- SBT wordmark -->
<path
android:fillColor="#00000000"
android:pathData="M46,62.8 L35,62.8 L35,72.8 L46,72.8 L46,82.8 L35,82.8 M49.75,62.8 L49.75,82.8 M49.75,62.8 L58.39,62.8 L61.75,66.16 L61.75,69.44 L58.39,72.8 L49.75,72.8 M49.75,72.8 L58.39,72.8 L61.75,76.16 L61.75,79.44 L58.39,82.8 L49.75,82.8 M65.5,62.8 L78.5,62.8 M72,62.8 L72,82.8"
android:strokeColor="#FFFFFFFF"
android:strokeWidth="5"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -2,5 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

+505
View File
@@ -0,0 +1,505 @@
#!/usr/bin/env python3
"""Generate the launcher icon VectorDrawable files and a local SVG preview.
This keeps the icon tweakable without hand-editing dense Android vector XML.
Edit the constants below, run this script, then open local/launcher-icon-preview.svg.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from xml.sax.saxutils import escape
ROOT = Path(__file__).resolve().parents[1]
FOREGROUND = ROOT / "app/src/main/res/drawable/ic_launcher_foreground.xml"
MONOCHROME = ROOT / "app/src/main/res/drawable/ic_launcher_monochrome.xml"
BACKGROUND = ROOT / "app/src/main/res/drawable/ic_launcher_background.xml"
PREVIEW = ROOT / "local/launcher-icon-preview.svg"
VIEWPORT = 108
# Main geometry knobs. Coordinates are in Android/SVG viewport units.
# Open local/launcher-icon-preview.svg after regenerating to compare the
# square, rounded, and circular launcher masks.
# Statusbar panel. It fills the full top width.
STATUS_BOTTOM = 54
CUTOUT_CX = 54
CUTOUT_TOP = 0
CUTOUT_BOTTOM = 39
CUTOUT_STEM_HALF_WIDTH = 2.2
# Android adaptive icons use a 108x108 viewport, but only the central 72x72
# area is guaranteed to survive launcher masks. The preview uses this safe-zone
# circle, not the full viewport circle.
ADAPTIVE_SAFE_RADIUS = 36
# Row 1.
ENVELOPE_X = 39
ENVELOPE_Y = 22.5
ENVELOPE_W = 11
ENVELOPE_H = 8
ENVELOPE_STROKE = 1.3
BATTERY_ICON_X = 58
BATTERY_ICON_Y = 23
BATTERY_ICON_W = 13
BATTERY_ICON_H = 7.7
BATTERY_ICON_RADIUS = 1
BATTERY_ICON_PROTRUSION_W = 2
BATTERY_ICON_PROTRUSION_H = 3
BATTERY_ICON_LEVEL = 0.70
BATTERY_ICON_STROKE = 1.0
# Row 2.
DAY_TEXT_X = 31
DAY_TEXT_Y = 31.6
DAY_TEXT_H = 8
DAY_TEXT_STROKE = 1.1
DAY_TEXT_LETTER_SPACING = 2.0
DAY_TEXT_LOWERCASE_SCALE = 0.82
WIFI_X = 61
WIFI_Y = 36.5
WIFI_W = 10
WIFI_H = 5
WIFI_STROKE = 1.3
WIFI_DOT_R = 0.9
SIGNAL_X = 73
SIGNAL_BOTTOM = 42.5
SIGNAL_BAR_W = 1.8
SIGNAL_BAR_SPACING = 1.2
SIGNAL_BAR_HEIGHTS = (2.8, 5.6, 8.4)
# The divider between the statusbar illustration and the SBT wordmark.
BATTERY_BAR_LEFT = 0
BATTERY_BAR_RIGHT = 108
BATTERY_BAR_Y = STATUS_BOTTOM + 0.0
BATTERY_BAR_H = 6
BATTERY_BAR_LEVEL = 0.70
WORDMARK_X = 35
WORDMARK_Y = 62.8
WORDMARK_W = 50
WORDMARK_H = 20
WORDMARK_STROKE_OUTER = 5.0
WORDMARK_STROKE_INNER = 1.8
COLOR_STATUS = "#77746E"
COLOR_CUTOUT = "#050403"
COLOR_WHITE = "#FFFFFFFF"
COLOR_WARM_WHITE = "#FFF5DC"
COLOR_ORANGE = "#FF9F1A"
COLOR_GREEN = "#2FDB72"
COLOR_BAR_TRACK = "#2A2116"
COLOR_BG = "#0D0B08"
@dataclass(frozen=True)
class PathStyle:
fill: str = "#00000000"
stroke: str | None = None
stroke_width: float | None = None
line_cap: str | None = None
line_join: str | None = None
@dataclass(frozen=True)
class IconPath:
data: str
style: PathStyle
comment: str | None = None
def round_rect(x: float, y: float, w: float, h: float, r: float) -> 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 " <path\n " + "\n ".join(attrs) + " />"
def write_vector(destination: Path, paths: list[IconPath]) -> None:
body = "\n".join(
(f"\n <!-- {escape(p.comment)} -->\n" if p.comment else "\n") + android_path_xml(p)
for p in paths
)
destination.write_text(
'<?xml version="1.0" encoding="utf-8"?>\n'
'<vector xmlns:android="http://schemas.android.com/apk/res/android"\n'
' android:width="108dp"\n'
' android:height="108dp"\n'
' android:viewportWidth="108"\n'
' android:viewportHeight="108">'
f'{body}\n'
'</vector>\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'<path d="{escape(icon_path.data)}" {" ".join(style)} />'
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 = '<rect width="108" height="108" fill="#050403" />'
square = f'<g transform="translate(0,0)">\n {panel_bg}\n {body}\n</g>'
rounded = (
'<g clip-path="url(#rounded-mask)">\n'
' <g transform="translate(126,0)">\n'
f' {panel_bg}\n {body}\n'
' </g>\n'
'</g>\n'
'<rect x="126" y="0" width="108" height="108" rx="27" fill="none" '
'stroke="#66FFFFFF" stroke-width="0.8" />'
)
circle_preview = (
'<g clip-path="url(#circle-mask)">\n'
' <g transform="translate(252,0)">\n'
f' {panel_bg}\n {body}\n'
' </g>\n'
'</g>\n'
f'<circle cx="306" cy="54" r="{ADAPTIVE_SAFE_RADIUS}" fill="none" '
'stroke="#66FFFFFF" stroke-width="0.8" />'
)
PREVIEW.write_text(
'<svg xmlns="http://www.w3.org/2000/svg" width="1368" height="432" viewBox="0 0 360 114">\n'
' <defs>\n'
' <clipPath id="rounded-mask"><rect x="126" y="0" width="108" height="108" rx="27" /></clipPath>\n'
f' <clipPath id="circle-mask"><circle cx="306" cy="54" r="{ADAPTIVE_SAFE_RADIUS}" /></clipPath>\n'
' </defs>\n'
' <rect width="360" height="114" fill="#18130C" />\n'
f' {square}\n'
f' {rounded}\n'
f' {circle_preview}\n'
' <text x="54" y="113" fill="#DDD4C2" font-size="4" text-anchor="middle">square</text>\n'
' <text x="180" y="113" fill="#DDD4C2" font-size="4" text-anchor="middle">rounded</text>\n'
' <text x="306" y="113" fill="#DDD4C2" font-size="4" text-anchor="middle">circle</text>\n'
'</svg>\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()
+185
View File
@@ -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 <<EOF
{
"tag_name": $(json_string "$tag"),
"target_commitish": $(json_string "$(git rev-parse HEAD)"),
"name": $(json_string "$release_name"),
"body": $(json_string "$body"),
"draft": false,
"prerelease": true
}
EOF
)"
release_id="$(
curl --fail --silent --show-error \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
--data "$payload" \
"${api_base}/repos/$(urlencode "$owner")/$(urlencode "$repo")/releases" \
| extract_json_id
)"
upload_asset() {
local path name
path="$1"
name="$(basename "$path")"
curl --fail --silent --show-error \
-X POST \
-H "Authorization: token ${GITEA_TOKEN}" \
-F "attachment=@${path}" \
"${api_base}/repos/$(urlencode "$owner")/$(urlencode "$repo")/releases/${release_id}/assets?name=$(urlencode "$name")" \
>/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}"