Bootstrap Pilgrims of Katan: rebrand, Gitea CI and OTA
Build and Release APK / Build APK and publish release (push) Canceled after 0s

Import of ProLibertateGames (upstream main, GPL-3.0) rebranded for
Wallacenet and repointed at git.wallacenet.ca.

- Identity: applicationId ca.wallacenet.pilgrimsofkatan (+ .dev for the dev
  channel), launcher names "Pilgrims of Katan" / "Pilgrims of Katan DEV",
  neutral hexagon branding replacing the Wallace tartan. Uncial Antiqua keeps
  its OFL licence, and the GPL-3.0 LICENSE plus upstream attribution stay.
- OTA: Updater.kt now reads this repo's own Gitea releases
  (/api/v1/repos/devadmin/PilgrimsOfKatan/releases...) and BuildConfig.
  OTA_ENABLED is on; Gitea answers the same JSON shape the code already parsed.
- Build numbers: versionCode = CI run number + plgVersionRunOffset (1000),
  with the tag and versionName derived from the same figure. The offset is
  permanent, because Gitea Actions restarts run numbers at 1 and a reset would
  otherwise make Android reject the next build as a downgrade.
- CI: ci.yml publishes a dev prerelease from dev, release.yml a production
  release from main, both through scripts/publish-release.sh (Gitea REST API,
  runnable by hand until an Actions runner is registered).
- Docs: docs/CI.md records the channel, numbering and runner decisions;
  docs/README.md seeds the per-game RULES-* conventions; CLAUDE.md carries the
  agent ground rules.

Verified: ./gradlew assembleDebug and testDebugUnitTest green on JDK 17 /
compileSdk 34, both variants stamped, installing side by side under the new
identity.

Co-Authored-By: Paperclip <[email protected]>
This commit is contained in:
adrianoftyriel
2026-10-13 22:03:27 +00:00
co-authored by Paperclip
parent d27c2db894
commit fad4778ad8
32 changed files with 760 additions and 773 deletions
+4 -2
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- OTA updates from GitHub Releases, plus LAN play over TCP sockets. -->
<!-- LAN play over TCP sockets, plus hand-a-down permission for in-app
installs — the OTA updater installs releases published on this repo's
Gitea; see docs/CI.md. -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Network Service Discovery needs multicast to see hosts on the LAN. -->
@@ -23,7 +25,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ProLibertateGames">
android:theme="@style/Theme.PilgrimsOfKatan">
<!-- No fixed orientation: the UI adapts to portrait, landscape and
tablet widths. configChanges keeps Compose state across rotation
@@ -12,7 +12,7 @@ import org.prolibertate.games.score.ScorekeeperRepository
import org.prolibertate.games.settings.SettingsRepository
import org.prolibertate.games.ui.AppEnv
import org.prolibertate.games.ui.AppRoot
import org.prolibertate.games.ui.theme.ProLibertateTheme
import org.prolibertate.games.ui.theme.PilgrimsTheme
import org.prolibertate.games.update.Updater
import java.util.UUID
@@ -43,7 +43,7 @@ class MainActivity : ComponentActivity() {
)
setContent {
ProLibertateTheme {
PilgrimsTheme {
AppRoot(env)
}
}
@@ -308,6 +308,6 @@ class LanTransport(private val context: Context) : Transport {
}
const val CONNECT_TIMEOUT_MS = 8_000
const val WIFI_LOCK_TAG = "ProLibertateGames:lan"
const val WIFI_LOCK_TAG = "PilgrimsOfKatan:lan"
}
}
@@ -33,7 +33,7 @@ const val PURPOSE_SCOREKEEPER = "scorekeeper"
/** Service type and name advertised over mDNS. */
const val SERVICE_TYPE = "_plgames._tcp"
const val SERVICE_NAME = "ProLibertateGames"
const val SERVICE_NAME = "PilgrimsOfKatan"
val protocolJson: Json = Json {
ignoreUnknownKeys = true
@@ -49,7 +49,7 @@ fun LaunchUpdateCheck(env: AppEnv, visible: Boolean) {
}
// Held back behind the splash, so an update prompt cannot appear over the
// tartan before the app itself has.
// splash before the app itself has.
val pending = release
if (pending == null || dismissed || !visible) return
@@ -4,7 +4,6 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -20,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.text.TextStyle
@@ -30,8 +30,9 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import org.prolibertate.games.R
import org.prolibertate.games.ui.theme.TartanBackground
import org.prolibertate.games.ui.theme.threadWidthFor
import org.prolibertate.games.ui.theme.BrandAccent
import org.prolibertate.games.ui.theme.BrandDeep
import org.prolibertate.games.ui.theme.BrandField
/** How long the splash holds at full opacity before fading. */
private const val SPLASH_HOLD_MILLIS = 2_000L
@@ -45,7 +46,11 @@ private const val SPLASH_FADE_MILLIS = 600
val CelticFontFamily = FontFamily(Font(R.font.uncial_antiqua))
/**
* The launch screen: Wallace tartan under the name of the house.
* The launch screen: the brand field under the name of the game.
*
* The gradient runs from the same hue as the window's `splash_background`
* held behind it, so the first frame and the splash are one brand in two
* shades rather than a colour change.
*
* Drawn as an overlay rather than a separate destination, so the menu is
* already composed and ready behind it and the fade reveals a live screen
@@ -70,30 +75,17 @@ fun SplashScreen(
onFinished()
}
BoxWithConstraints(modifier = modifier.fillMaxSize().alpha(alpha)) {
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.alpha(alpha)
.background(Brush.verticalGradient(listOf(BrandField, BrandDeep))),
) {
// Read the constraints here and hold them in locals: inside Column the
// implicit receiver becomes ColumnScope, and maxWidth/maxHeight are no
// longer in scope.
val shortestSide = minOf(maxWidth, maxHeight).value
// Weave scale is derived from the sett, not a fixed fraction, so a
// couple of full repeats are always on screen whatever the threadcount
// says and whatever the display is.
val threadWidth = threadWidthFor(across = shortestSide.dp, repeats = 2f)
val titleSize = (shortestSide * 0.13f).sp
TartanBackground(
modifier = Modifier.fillMaxSize(),
threadWidth = threadWidth,
// On the bias, matching the icon.
rotationDegrees = 45f,
)
// A scrim keeps the lettering legible over the busiest part of the sett.
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.45f))
)
Column(
modifier = Modifier
@@ -103,7 +95,7 @@ fun SplashScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Pro Libertate",
text = "Pilgrims",
style = TextStyle(
fontFamily = CelticFontFamily,
fontSize = titleSize,
@@ -117,11 +109,11 @@ fun SplashScreen(
),
)
Text(
text = "Games",
text = "of Katan",
style = TextStyle(
fontFamily = CelticFontFamily,
fontSize = titleSize,
color = Color(0xFFF2C200),
color = BrandAccent,
textAlign = TextAlign.Center,
shadow = Shadow(
color = Color.Black,
@@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
import org.prolibertate.games.ui.common.CARD_ASPECT
import org.prolibertate.games.ui.theme.FeltGreenDark
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.WallaceGold
import org.prolibertate.games.ui.theme.AccentGold
/**
* The furniture every patience table needs.
@@ -48,7 +48,7 @@ fun TableSlot(
modifier = modifier
.width(width)
.border(
border = BorderStroke(if (highlighted) 3.dp else 0.dp, WallaceGold),
border = BorderStroke(if (highlighted) 3.dp else 0.dp, AccentGold),
shape = RoundedCornerShape(4.dp),
)
.clickable { onClick() },
@@ -48,7 +48,7 @@ import org.prolibertate.games.ui.common.CardBackView
import org.prolibertate.games.ui.common.PlayingCardView
import org.prolibertate.games.ui.theme.FeltGreenDark
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.WallaceGold
import org.prolibertate.games.ui.theme.AccentGold
private const val TITLE = "Klondike"
@@ -311,7 +311,7 @@ private fun Slot(
modifier = modifier
.width(CARD_WIDTH)
.border(
border = BorderStroke(if (highlighted) 3.dp else 0.dp, WallaceGold),
border = BorderStroke(if (highlighted) 3.dp else 0.dp, AccentGold),
shape = RoundedCornerShape(4.dp),
)
.clickable { onClick() },
@@ -41,8 +41,8 @@ import org.prolibertate.games.net.MatchController
import org.prolibertate.games.ui.ScreenScaffold
import org.prolibertate.games.ui.theme.FeltGreenDark
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.WallaceGold
import org.prolibertate.games.ui.theme.WallaceRed
import org.prolibertate.games.ui.theme.AccentGold
import org.prolibertate.games.ui.theme.AccentRed
private const val TITLE = "Peg Solitaire"
@@ -214,15 +214,15 @@ private fun HoleView(
Box(modifier = modifier.size(size), contentAlignment = Alignment.Center) {
val diameter = size * 0.72f
val ring = when {
selected -> WallaceGold
isDestination -> WallaceGold
selected -> AccentGold
isDestination -> AccentGold
else -> Parchment.copy(alpha = 0.35f)
}
Box(
modifier = Modifier
.size(diameter)
.background(
color = if (filled) WallaceRed else FeltGreenDark,
color = if (filled) AccentRed else FeltGreenDark,
shape = CircleShape,
)
.border(
@@ -42,8 +42,8 @@ import org.prolibertate.games.game.yahtzee.scoreOf
import org.prolibertate.games.net.MatchController
import org.prolibertate.games.ui.ScreenScaffold
import org.prolibertate.games.ui.theme.Parchment
import org.prolibertate.games.ui.theme.WallaceGold
import org.prolibertate.games.ui.theme.WallaceRed
import org.prolibertate.games.ui.theme.AccentGold
import org.prolibertate.games.ui.theme.AccentRed
private const val TITLE = "Yahtzee"
@@ -183,7 +183,7 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit
.background(if (face == null) Parchment.copy(alpha = 0.3f) else Parchment, RoundedCornerShape(8.dp))
.border(
width = if (held) 3.dp else 1.dp,
color = if (held) WallaceGold else WallaceRed.copy(alpha = 0.4f),
color = if (held) AccentGold else AccentRed.copy(alpha = 0.4f),
shape = RoundedCornerShape(8.dp),
)
.clickable(enabled = enabled) { onClick() },
@@ -193,7 +193,7 @@ private fun Die(face: Int?, held: Boolean, enabled: Boolean, onClick: () -> Unit
text = face?.toString() ?: "·",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = WallaceRed,
color = AccentRed,
)
}
}
@@ -263,7 +263,7 @@ private fun CardRow(
},
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodySmall,
color = if (written == null && open) WallaceRed else MaterialTheme.colorScheme.onSurface,
color = if (written == null && open) AccentRed else MaterialTheme.colorScheme.onSurface,
)
}
}
@@ -1,187 +0,0 @@
package org.prolibertate.games.ui.theme
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.prolibertate.games.BuildConfig
/** One band of a tartan sett, measured in threads. */
data class TartanStripe(val color: Color, val threads: Int)
val WallaceRed = Color(0xFFE01B24)
val WallaceBlack = Color(0xFF0C0C0C)
val WallaceGold = Color(0xFFF2C518)
/**
* The green the hunting sett puts where the clan sett puts red.
*
* Must match `tartan_field` for a dev build in app/build.gradle.kts, which is
* what the launcher icon is woven from — otherwise the icon and the splash it
* opens on would be visibly different cloth.
*/
val WallaceHuntingGreen = Color(0xFF1E7A3C)
/**
* Expands a half-sett into a full repeat by reflecting it.
*
* Tartans are specified as a half-sett between two pivots and woven out to
* both sides, so `A B C` becomes `A B C B` — the pivot stripes are not
* doubled. Writing the data this way keeps it in the same form a threadcount
* is published in, instead of a flat list that has to be kept symmetric by
* hand.
*/
fun List<TartanStripe>.pivoted(): List<TartanStripe> =
if (size < 3) this else this + drop(1).dropLast(1).reversed()
/**
* The Wallace sett, from its published threadcount: **K/4 R32 K32 Y/4**.
*
* The slashes mark the pivots, so this is the half-sett and it reflects out to
* `K4 R32 K32 Y4 K32 R32` — 136 threads, roughly half black, half red, with a
* yellow line worth 3%. Two consequences that are easy to get backwards and
* that define the pattern:
*
* - the yellow overstripe runs down the **centre of a wide black band**, not
* between red and black;
* - the narrow black guard sits **between two red blocks**, splitting the red
* into pairs.
*
* The threadcount itself lives in [wallaceSett], because the hunting colourway
* below is the same cloth in a different colour and must not drift from it.
*/
val WallaceSett: List<TartanStripe> = wallaceSett(WallaceRed)
/**
* Wallace Hunting: the same threadcount woven in green.
*
* Hunting setts are the clan cloth in muted, outdoor colours, and for Wallace
* that is the one substitution — green for the red — with the black ground and
* the yellow overstripe untouched. Deriving it from the same builder rather
* than writing the numbers out twice is what keeps that true: a change to the
* threadcount cannot land in one colourway and miss the other.
*/
val WallaceHuntingSett: List<TartanStripe> = wallaceSett(WallaceHuntingGreen)
/** The Wallace threadcount woven with [field] in place of the red. */
private fun wallaceSett(field: Color): List<TartanStripe> = listOf(
TartanStripe(WallaceBlack, 4),
TartanStripe(field, 32),
TartanStripe(WallaceBlack, 32),
TartanStripe(WallaceGold, 4),
).pivoted()
/**
* The cloth this build wears: the clan sett on production, the hunting sett on
* dev.
*
* A dev build installs as its own app beside the production one, so it has to
* be recognisable at a glance in the app drawer and on the splash — the green
* says which of the two copies just opened without anything having to be read.
*/
val AppSett: List<TartanStripe> =
if (BuildConfig.DEV_BUILD) WallaceHuntingSett else WallaceSett
/** Threads in one full repeat of a sett. */
val List<TartanStripe>.threadsPerRepeat: Int get() = sumOf { it.threads }
/**
* The thread width that fits [repeats] repeats of [sett] across [across].
*
* Scale has to be derived from the sett rather than fixed, because changing a
* threadcount changes how many threads a repeat contains — a fixed width and a
* larger sett silently zooms in until the cloth is a couple of enormous blocks
* and stops reading as tartan at all.
*/
fun threadWidthFor(
sett: List<TartanStripe> = AppSett,
across: Dp,
repeats: Float = 2f,
): Dp = across / (sett.threadsPerRepeat * repeats)
/**
* Draws a tartan by laying the sett down vertically as warp, then across as
* weft at half opacity. The overlap is what produces the blended squares and
* the sense of a woven cloth rather than a printed grid — red warp under black
* weft comes out a darker red, exactly as it does on the loom.
*
* [rotationDegrees] turns the cloth on the bias. The stripes are drawn well
* past the edges of the canvas so the corners stay covered once it is turned.
*/
fun DrawScope.drawTartan(
sett: List<TartanStripe> = AppSett,
threadSize: Float,
rotationDegrees: Float = 0f,
) {
if (sett.isEmpty() || threadSize <= 0f) return
val repeat = sett.sumOf { it.threads } * threadSize
if (repeat <= 0f) return
// Turning a square by 45 degrees needs about 0.21 of its size spare on
// every edge; three quarters is generous enough for any angle.
val pad = if (rotationDegrees == 0f) 0f else size.maxDimension * 0.75f
val left = -pad
val top = -pad
val right = size.width + pad
val bottom = size.height + pad
rotate(degrees = rotationDegrees) {
// Warp: full-height bands.
var x = left - (left.mod(repeat))
while (x < right) {
var offset = x
for (stripe in sett) {
val width = stripe.threads * threadSize
drawRect(
color = stripe.color,
topLeft = Offset(offset, top),
size = Size(width, bottom - top),
)
offset += width
}
x += repeat
}
// Weft: full-width bands, half opacity so the warp shows through.
var y = top - (top.mod(repeat))
while (y < bottom) {
var offset = y
for (stripe in sett) {
val height = stripe.threads * threadSize
drawRect(
color = stripe.color.copy(alpha = 0.5f),
topLeft = Offset(left, offset),
size = Size(right - left, height),
)
offset += height
}
y += repeat
}
}
}
/**
* A tartan fill. [threadWidth] sets the weave scale, so the same cloth can be
* a fine weave on a phone and a coarse one on a tablet without redrawing it.
*/
@Composable
fun TartanBackground(
modifier: Modifier = Modifier,
sett: List<TartanStripe> = AppSett,
threadWidth: Dp = 2.dp,
rotationDegrees: Float = 0f,
) {
Canvas(modifier = modifier) {
drawTartan(
sett = sett,
threadSize = threadWidth.toPx(),
rotationDegrees = rotationDegrees,
)
}
}
@@ -6,6 +6,7 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import org.prolibertate.games.BuildConfig
/** Card-table green, which the board and table surfaces are drawn on. */
val FeltGreen = Color(0xFF1B5E20)
@@ -14,6 +15,31 @@ val Parchment = Color(0xFFF4EFE4)
val CardRed = Color(0xFFB3261E)
val CardBlack = Color(0xFF1C1B1F)
/**
* The brand palette for Pilgrims of Katan — original and neutral, shared by
* every game in the catalogue. Slate blue on production, deep green on dev, so
* the two installs are told apart at a glance; both must stay in step with the
* resValues in app/build.gradle.kts or the icon and the splash it opens on
* would be different brands.
*
* The upstream shipped a clan tartan here. This project carries no clan or
* third-party trade dress (WALA-118).
*/
val BrandField: Color = if (BuildConfig.DEV_BUILD) Color(0xFF2A6B54) else Color(0xFF2B4A78)
val BrandDeep: Color = if (BuildConfig.DEV_BUILD) Color(0xFF153B2F) else Color(0xFF182A44)
/** The parchment accent: the emblem on the icon, the second line of the splash. */
val BrandAccent: Color = Color(0xFFEFD9A4)
/**
* Accents used *inside* a game table — a highlight, a held die, a danger mark.
* Deliberately channel-free: what a game shows must not change with which
* install it is running in. Values unchanged from the upstream, so the games
* themselves look exactly as they did.
*/
val AccentRed = Color(0xFFE01B24)
val AccentGold = Color(0xFFF2C518)
val TeamColours: List<Color> = listOf(
Color(0xFF1565C0), // blue
Color(0xFFC62828), // red
@@ -39,7 +65,7 @@ private val LightColours = lightColorScheme(
)
@Composable
fun ProLibertateTheme(
fun PilgrimsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit,
) {
@@ -5,13 +5,13 @@ package org.prolibertate.games.update
*
* This is a property of the build, never a preference. The two channels are
* published by different workflows and have entirely separate version
* sequences — a dev build's versionCode is its CI run number, a production
* build's is its release run number, and the two are unrelated. There is no
* sequences — a dev build's versionCode is its CI build number, a production
* build's is its release build number, and the two are unrelated. There is no
* meaning to be had from comparing across them, so nothing does.
*
* They also carry different applicationIds — a dev build installs as
* `…games.dev` — so both can sit on one device, each updating itself down its
* own line and leaving the other alone.
* `…pilgrimsofkatan.dev` — so both can sit on one device, each updating
* itself down its own line and leaving the other alone.
*/
enum class UpdateChannel(val label: String, val blurb: String) {
PRODUCTION("Production", "Stable builds released from main"),
@@ -21,7 +21,7 @@ enum class UpdateChannel(val label: String, val blurb: String) {
/** Marks a tag or a version name as belonging to the dev channel. */
const val DEV_SUFFIX = "-dev"
/** One published build, as read off a GitHub release. */
/** One published build, as read off a repository release. */
data class Release(
val tag: String,
val versionCode: Int,
@@ -49,14 +49,14 @@ fun channelOfVersionName(versionName: String): UpdateChannel =
* The channel a release belongs to.
*
* Two independent marks, and either is enough: the `-dev` suffix the workflow
* puts on the tag, and GitHub's own prerelease flag. A release would have to
* lose both to be mistaken for the other channel.
* puts on the tag, and the release's own prerelease flag. A release would have
* to lose both to be mistaken for the other channel.
*/
fun channelOfTag(tag: String, prerelease: Boolean): UpdateChannel =
if (tag.endsWith(DEV_SUFFIX) || prerelease) UpdateChannel.DEV else UpdateChannel.PRODUCTION
/**
* The run number out of a `v<series>.<n>` tag, which is what an APK's
* The build number out of a `v<series>.<n>` tag, which is what an APK's
* versionCode is.
*
* Only the trailing number is read. The series is a name for people, and
@@ -70,10 +70,10 @@ fun versionCodeOfTag(tag: String): Int? =
* nothing at all.
*
* Only an actual update is worth a word. Opening the app is not asking a
* question, so a check that finds nothing — or that cannot reach GitHub at all
* — stays quiet; a phone on a train would otherwise be told off for it every
* single launch. The button in Settings *is* asking a question, and reports
* every answer including those two.
* question, so a check that finds nothing — or that cannot reach the release
* server at all — stays quiet; a phone on a train would otherwise be told off
* for it every single launch. The button in Settings *is* asking a question, and
* reports every answer including those two.
*/
fun launchOffer(verdict: UpdateVerdict): Release? =
(verdict as? UpdateVerdict.Install)?.release
@@ -10,17 +10,34 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import org.prolibertate.games.BuildConfig
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
/**
* Over-the-air updates from the repository's GitHub Releases.
* Over-the-air updates from the repository's releases.
*
* Release tags are v<series>.<run-number> and the APK's versionCode is that same run
* number, so comparing the two is enough to tell whether a newer build exists.
* The repository's releases must be publicly downloadable — no token is
* embedded in the app, and GitHub blocks anonymous access to private assets.
* The channel is the Gitea instance this app ships from
* (`git.wallacenet.ca`), not GitHub. Gitea's release API answers with the
* same shape the updater reads — `tag_name`, `prerelease`, `draft`, and
* assets carrying `browser_download_url` — so this is the same code the
* upstream ran, pointed at `/api/v1/repos/...` routes instead of
* `api.github.com`, with `limit` where GitHub says `per_page`.
*
* Release tags are v<series>.<n>, where **n is the stamped build number the
* APK's versionCode was built from** (the CI run number plus the fixed
* `plgVersionRunOffset` — see docs/CI.md), so comparing the two is enough to
* tell whether a newer build exists.
*
* The repository and its releases are public: no token is embedded in the
* app, and every download is anonymous by design. If the repo were ever made
* private, every check would simply start failing rather than leaking
* anything.
*
* `BuildConfig.OTA_ENABLED` is a kill switch, not a channel decision — it is
* true in this build; setting it false makes every check report that and
* fetch nothing.
*
* There is nothing to choose here. The channel comes from the running APK, so
* a dev install looks only at dev builds and a production install only at
@@ -35,6 +52,11 @@ class Updater(private val activity: Activity) {
* this phone is on, and the phone already knows.
*/
suspend fun check(): UpdateVerdict = withContext(Dispatchers.IO) {
if (!BuildConfig.OTA_ENABLED) {
return@withContext UpdateVerdict.Refused(
"In-app updates are not enabled for this build yet."
)
}
try {
val channel = installedChannel()
verdictFor(
@@ -48,23 +70,28 @@ class Updater(private val activity: Activity) {
}
/**
* Production reads GitHub's "latest release", which by definition skips
* prereleases. Dev walks the release list for the newest prerelease, which
* is what the CI pipeline publishes from the dev branch.
* Production reads the "latest release", which by definition skips
* prereleases — Gitea implements it as the most recent non-prerelease,
* non-draft release, so the rule holds on this instance too.
*
* Dev walks the release list for the newest prerelease, which is what the
* CI pipeline publishes from the dev branch. The list is taken as the
* highest versionCode among the dev releases rather than the first one
* returned, so the answer does not depend on how the server chose to
* order the page.
*/
private fun fetchLatestRelease(channel: UpdateChannel): Release? = when (channel) {
UpdateChannel.PRODUCTION ->
getJsonObject("https://api.github.com/repos/$OWNER/$REPO/releases/latest")
getJsonObject("$API/releases/latest")
?.let { toRelease(it) }
UpdateChannel.DEV ->
getJsonArray("https://api.github.com/repos/$OWNER/$REPO/releases?per_page=30")
getJsonArray("$API/releases?limit=30")
?.let { releases ->
// GitHub returns newest first.
(0 until releases.length())
.map { releases.getJSONObject(it) }
.firstOrNull { it.optBoolean("prerelease") && !it.optBoolean("draft") }
?.let { toRelease(it) }
.mapNotNull { toRelease(releases.getJSONObject(it)) }
.filter { it.channel == UpdateChannel.DEV }
.maxByOrNull { it.versionCode }
}
}
@@ -98,8 +125,8 @@ class Updater(private val activity: Activity) {
private fun openConnection(url: String): HttpURLConnection =
(URL(url).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
setRequestProperty("Accept", "application/vnd.github+json")
setRequestProperty("User-Agent", "ProLibertateGames-Updater")
setRequestProperty("Accept", "application/json")
setRequestProperty("User-Agent", "PilgrimsOfKatan-Updater")
connectTimeout = 15_000
readTimeout = 15_000
}
@@ -175,7 +202,12 @@ class Updater(private val activity: Activity) {
activity.packageManager.getPackageInfo(activity.packageName, 0).versionName ?: "unknown"
private companion object {
const val OWNER = "adrianoftyriel"
const val REPO = "ProLibertateGames"
/** The Gitea instance that publishes this app's releases. */
const val SERVER = "https://git.wallacenet.ca"
const val OWNER = "devadmin"
const val REPO = "PilgrimsOfKatan"
/** Release routes, Gitea's `/api/v1` spelling of GitHub's API. */
const val API = "$SERVER/api/v1/repos/$OWNER/$REPO"
}
}
@@ -1,70 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A single square of the Wallace tartan, set on the bias.
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
centre of a wide black band, and the narrow black guard splits the red into
pairs.
The stripes reference @color/tartan_field rather than a literal, because the
same threadcount is woven in two colourways: Wallace red on a production
build, Wallace Hunting green on a dev one, so the two installs are told apart
in the app drawer by the icon alone. The build generates that colour; see
resValue in app/build.gradle.kts.
Warp is opaque, weft laid over it at half opacity, which is what gives the
blended squares rather than a flat grid. Paths run well past the viewport
because the group is rotated 45 degrees; the overspill keeps the corners
covered. One full repeat fills the adaptive icon's safe zone.
The flat field behind the launcher emblem: brand_field, slate blue on
production and deep green on dev, generated by the build (see resValue in
app/build.gradle.kts) so the two installs are told apart in the app drawer
by the icon alone.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:rotation="45"
android:pivotX="54.0"
android:pivotY="54.0">
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
</group>
<path
android:fillColor="@color/brand_field"
android:pathData="M0,0h108v108h-108z" />
</vector>
@@ -1,15 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Deliberately empty. The icon is the cloth itself, so the adaptive icon's
foreground layer carries nothing — anything drawn here would sit on top of the
tartan and break the "single square of tartan" idea.
The brand emblem: a hexagonal ring around a solid hexagonal core, in
parchment on the brand field behind it (see ic_launcher_background).
The hexagon is a game tile, not any game's mark — deliberately generic
branding for Pilgrims of Katan, carrying no third-party logo or trade dress.
Everything sits inside the adaptive icon's safe zone (a centred 66dp circle
on this 108dp canvas) so no launcher mask can clip it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Ring and core in one even-odd path: outer hexagon, inner hexagon,
core hexagon — odd coverage fills, so the ring and the core are drawn
and the gap between them stays open to the field showing through. -->
<path
android:fillColor="#00000000"
android:pathData="M0,0h108v108h-108z" />
android:fillColor="@color/brand_accent"
android:fillType="evenOdd"
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
</vector>
+12 -60
View File
@@ -1,70 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A single square of the Wallace tartan, set on the bias.
The legacy (pre-API 26) launcher icon: the same brand emblem as the adaptive
icon's foreground, over the same brand field, in the one layer those
launchers get.
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
centre of a wide black band, and the narrow black guard splits the red into
pairs.
The stripes reference @color/tartan_field rather than a literal, because the
same threadcount is woven in two colourways: Wallace red on a production
build, Wallace Hunting green on a dev one, so the two installs are told apart
in the app drawer by the icon alone. The build generates that colour; see
resValue in app/build.gradle.kts.
Warp is opaque, weft laid over it at half opacity, which is what gives the
blended squares rather than a flat grid. Paths run well past the viewport
because the group is rotated 45 degrees; the overspill keeps the corners
covered. One full repeat fills the adaptive icon's safe zone.
Field first, emblem second: two paths, because the emblem's ring relies on
even-odd parity and a filled background inside the same path would flip it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:rotation="45"
android:pivotX="54.0"
android:pivotY="54.0">
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
</group>
<path
android:fillColor="@color/brand_field"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="@color/brand_accent"
android:fillType="evenOdd"
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
</vector>
+12 -60
View File
@@ -1,70 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A single square of the Wallace tartan, set on the bias.
The legacy (pre-API 26) launcher icon: the same brand emblem as the adaptive
icon's foreground, over the same brand field, in the one layer those
launchers get.
Generated from the published threadcount K/4 R32 K32 Y/4 — the same numbers
as WallaceSett in Tartan.kt. The slashes are pivots, so the half-sett
reflects out to K4 R32 K32 Y4 K32 R32: the yellow overstripe runs down the
centre of a wide black band, and the narrow black guard splits the red into
pairs.
The stripes reference @color/tartan_field rather than a literal, because the
same threadcount is woven in two colourways: Wallace red on a production
build, Wallace Hunting green on a dev one, so the two installs are told apart
in the app drawer by the icon alone. The build generates that colour; see
resValue in app/build.gradle.kts.
Warp is opaque, weft laid over it at half opacity, which is what gives the
blended squares rather than a flat grid. Paths run well past the viewport
because the group is rotated 45 degrees; the overspill keeps the corners
covered. One full repeat fills the adaptive icon's safe zone.
Field first, emblem second: two paths, because the emblem's ring relies on
even-odd parity and a filled background inside the same path would flip it.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:rotation="45"
android:pivotX="54.0"
android:pivotY="54.0">
<path android:fillColor="@color/tartan_ground" android:pathData="M-90.00,-90.00h288.00v288.00h-288.00z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-88.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-69.88,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M-36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M-16.94,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M2.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M36.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M55.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M74.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M108.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_field" android:pathData="M127.06,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_field" android:pathData="M146.12,-90.00h16.94v288.00h-16.94z" />
<path android:fillColor="@color/tartan_over" android:pathData="M180.00,-90.00h2.12v288.00h-2.12z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-105.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-88.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-69.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-52.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,-36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,-33.88h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,-16.94h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,0.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,2.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,19.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,36.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,38.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,55.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,72.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,74.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,91.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,108.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,110.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,127.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,144.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_field" android:fillAlpha="0.5" android:pathData="M-90.00,146.12h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,163.06h288.00v16.94h-288.00z" />
<path android:fillColor="@color/tartan_over" android:fillAlpha="0.5" android:pathData="M-90.00,180.00h288.00v2.12h-288.00z" />
<path android:fillColor="@color/tartan_ground" android:fillAlpha="0.5" android:pathData="M-90.00,182.12h288.00v16.94h-288.00z" />
</group>
<path
android:fillColor="@color/brand_field"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="@color/brand_accent"
android:fillType="evenOdd"
android:pathData="M84,54 L69,79.98 L39,79.98 L24,54 L39,28.02 L69,28.02 Z M72,54 L63,69.59 L45,69.59 L36,54 L45,38.41 L63,38.41 Z M63,54 L58.5,61.79 L49.5,61.79 L45,54 L49.5,46.21 L58.5,46.21 Z" />
</vector>
+8 -8
View File
@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The colours the sett shares between channels. The one that differs —
tartan_field, red on production and hunting green on dev — is generated by the
build, along with splash_background and ic_launcher_background, which are
shades of it. See resValue in app/build.gradle.kts.
The colours the brand shares between channels. The two that differ —
brand_field (slate blue on production, deep green on dev) and its darker
shade splash_background — are generated by the build. See resValue in
app/build.gradle.kts.
-->
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- The ground the sett is woven on, and the overstripe down its centre.
Same numbers as WallaceBlack and WallaceGold in Tartan.kt. -->
<color name="tartan_ground">#0C0C0C</color>
<color name="tartan_over">#F2C518</color>
<!-- The parchment accent drawn on the brand field: the emblem on the
launcher icon and the second line of the splash. Same number as the
accent in Theme.kt. -->
<color name="brand_accent">#EFD9A4</color>
</resources>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
app_name is generated by the build rather than declared here, because it
differs by channel: a dev build installs as "Pro Libertate Games DEV"
differs by channel: a dev build installs as "Pilgrims of Katan DEV"
alongside the production copy. See resValue in app/build.gradle.kts.
-->
<resources>
+1 -1
View File
@@ -2,7 +2,7 @@
<resources>
<!-- Compose supplies the real theme; this only needs to be a Material 3
base with no action bar so the app draws its own top bars. -->
<style name="Theme.ProLibertateGames" parent="android:Theme.Material.NoActionBar">
<style name="Theme.PilgrimsOfKatan" parent="android:Theme.Material.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@color/splash_background</item>