feat: art-first library cards with a display menu (#1744)

* feat: art-first library cards with a display menu for size, title and storage

* build: use native arm64 docker on Apple Silicon and add a fast deploy loop

* fix: address review feedback on library cards and local build scripts
This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-28 22:40:53 -04:00
1 parent 110cfc28f8
commit 0516d023fa
12 files changed
+261 -34

No files matched your search

@@ -125,6 +125,24 @@ object SessionPrefs {
prefs(context).edit().putString("storesView", if (view in STORES_VIEWS) view else STORES_VIEWS.first()).apply()
}
fun libraryCardLevel(context: Context): Int = prefs(context).getInt("libraryCardLevel", 2)
fun setLibraryCardLevel(context: Context, level: Int) {
prefs(context).edit().putInt("libraryCardLevel", level).apply()
}
fun libraryShowTitle(context: Context): Boolean = prefs(context).getBoolean("libraryShowTitle", true)
fun setLibraryShowTitle(context: Context, show: Boolean) {
prefs(context).edit().putBoolean("libraryShowTitle", show).apply()
}
fun libraryShowSize(context: Context): Boolean = prefs(context).getBoolean("libraryShowSize", true)
fun setLibraryShowSize(context: Context, show: Boolean) {
prefs(context).edit().putBoolean("libraryShowSize", show).apply()
}
fun libraryShowUninstalled(context: Context): Boolean = prefs(context).getBoolean("libraryShowUninstalled", false)
fun setLibraryShowUninstalled(context: Context, show: Boolean) {
@@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -35,12 +37,15 @@ import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FilterAlt
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material.icons.filled.Sort
import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -55,11 +60,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.onPreviewKeyEvent
@@ -88,6 +95,8 @@ import com.droiddeck.launcher.stores.StoreSizes
import com.droiddeck.launcher.stores.StoresState
import com.droiddeck.launcher.stores.arrangeStoreTabs
import com.droiddeck.launcher.stores.download.DownloadEntry
import com.droiddeck.launcher.stores.download.DownloadStage
import com.droiddeck.launcher.stores.download.DownloadState
import com.droiddeck.launcher.stores.formatBytes
import com.droiddeck.launcher.stores.itch.ItchCollection
import com.droiddeck.launcher.stores.itch.ItchCollections
@@ -147,6 +156,37 @@ internal fun librarySources(entries: List<LibraryEntry>, order: List<String>): L
.filter { source -> entries.any { it.source == source } }
/** Survives leaving the page, so Stores can open the Library on one store's owned games. */
internal object LibraryDisplay {
val scales = listOf(0.75f, 0.875f, 1f, 1.2f, 1.45f)
var level by mutableIntStateOf(2)
private set
var title by mutableStateOf(true)
private set
var size by mutableStateOf(true)
private set
fun load(context: android.content.Context) {
level = SessionPrefs.libraryCardLevel(context).coerceIn(0, scales.lastIndex)
title = SessionPrefs.libraryShowTitle(context)
size = SessionPrefs.libraryShowSize(context)
}
fun step(context: android.content.Context, by: Int) {
level = (level + by).coerceIn(0, scales.lastIndex)
SessionPrefs.setLibraryCardLevel(context, level)
}
fun toggleTitle(context: android.content.Context) {
title = !title
SessionPrefs.setLibraryShowTitle(context, title)
}
fun toggleSize(context: android.content.Context) {
size = !size
SessionPrefs.setLibraryShowSize(context, size)
}
}
internal object LibraryView {
var source by mutableStateOf(LIBRARY_ALL)
var query by mutableStateOf("")
@@ -171,6 +211,7 @@ internal fun LibraryPage(s: FrontEndState, a: FrontEndActions, selected: String,
val focusManager = androidx.compose.ui.platform.LocalFocusManager.current
val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current
LaunchedEffect(Unit) { StoresState.refresh(ctx) }
remember { LibraryDisplay.load(ctx) }
val stores = s.gameStoresEnabled && !s.shortcutPicker
val signedIn = Store.entries.filter { StoresState.isSignedIn(it) }
LaunchedEffect(stores, signedIn) { if (stores) signedIn.forEach { StoresState.loadLibrary(ctx, it) } }
@@ -360,7 +401,7 @@ private fun LibraryGrid(
) {
val listMode = s.storesView == "list"
val capsules = s.storesView == "capsules"
val tileWidth = if (capsules) CapsuleWidth else CardWidth
val tileWidth = (if (capsules) CapsuleWidth else if (listMode) CardWidth else LibraryCardWidth) * LibraryDisplay.scales[LibraryDisplay.level]
val focus = LocalFrontFocus.current
val scope = rememberCoroutineScope()
val density = androidx.compose.ui.platform.LocalDensity.current
@@ -397,7 +438,7 @@ private fun LibraryGrid(
}
val picker = s.shortcutPicker && e.game != null
val choose = if (picker) ({ a.onSteamGame(e.game!!) }) else null
if (listMode) LibraryRow(e, first, navigation, choose, onOpen) else LibraryTile(e, capsules, first, navigation, choose, onOpen)
if (listMode) LibraryRow(e, first, navigation, choose, onOpen) else LibraryTile(e, capsules, first, navigation, choose, onOpen, showSource = source == LIBRARY_ALL)
}
LazyVerticalGrid(
columns = if (listMode) GridCells.Fixed(1) else GridCells.Adaptive(tileWidth), state = grid,
@@ -464,7 +505,7 @@ private fun LibraryHeader(
ModeHeader(
stringResource(R.string.rail_games), LibraryView.query, { LibraryView.query = it },
placeholder = stringResource(R.string.stores_search_library),
clearSearchFocus = searchCleared,
clearSearchFocus = searchCleared, inlineWhenStacked = true,
lead = {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(start = 2.dp, end = 4.dp)) {
Text(stringResource(R.string.rail_games), fontSize = if (narrow) 20.sp else 24.sp, fontWeight = FontWeight.Bold, color = colors.onBackground, maxLines = 1)
@@ -473,6 +514,7 @@ private fun LibraryHeader(
}
},
) {
DisplayButton()
if (showUninstalled != null || xbox) LibraryFilterButton(showUninstalled, onShowUninstalled, xbox)
if (collections) CollectionsButton()
SortButton(s, a)
@@ -532,7 +574,7 @@ private fun entryArt(e: LibraryEntry, tall: Boolean): Any? {
}
@Composable
private fun LibraryTile(e: LibraryEntry, capsule: Boolean, first: Boolean, navigation: Modifier, choose: (() -> Unit)?, onOpen: (String) -> Unit) {
private fun LibraryTile(e: LibraryEntry, capsule: Boolean, first: Boolean, navigation: Modifier, choose: (() -> Unit)?, onOpen: (String) -> Unit, showSource: Boolean) {
val pal = LocalPalette.current
val src = remember { MutableInteractionSource() }
val hot = rememberHot(src)
@@ -556,9 +598,60 @@ private fun LibraryTile(e: LibraryEntry, capsule: Boolean, first: Boolean, navig
.hoverable(src).clickable(role = Role.Button, interactionSource = src, indication = LocalIndication.current, onClick = open)
.controllerConfirm(onClick = choose ?: open),
) {
CardFace(e.title, wide, tall, download, hot, capsule, dim = !e.installed && download == null,
badge = { SourceChip(e.source, small = true, modifier = Modifier.align(Alignment.TopStart).padding(6.dp)) }) {
LibraryMarker(e, download)
LibraryArt(e, wide, tall, download, hot, capsule, showSource)
}
}
private val LibraryCardWidth = 150.dp
@Composable
private fun LibraryArt(e: LibraryEntry, wide: Any?, tall: Any?, download: DownloadEntry?, hot: Boolean, capsule: Boolean, showSource: Boolean) {
val url = if (capsule) tall ?: wide else wide ?: tall
val art = cardArtRequest(url, tall)
val ratio = if (capsule) 5f / 7f else 16f / 9f
var offRatio by remember(url) { mutableStateOf(false) }
var trim by remember(url) { mutableStateOf(false) }
val blurs = android.os.Build.VERSION.SDK_INT >= 31
Box(
Modifier.fillMaxWidth().aspectRatio(ratio).background(Color(0xFF101318))
.then(if (url == null) Modifier.background(artBrush(hueOf(e.title))) else Modifier),
) {
if (art != null && offRatio) {
AsyncImage(model = art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize().blur(32.dp))
Spacer(Modifier.matchParentSize().background(Color.Black.copy(alpha = if (blurs) 0.18f else 0.6f)))
}
if (art != null) AsyncImage(
model = art, contentDescription = null, contentScale = if (trim) ContentScale.Crop else ContentScale.Fit, modifier = Modifier.matchParentSize(),
onSuccess = { r ->
val d = r.result.drawable
if (d.intrinsicWidth > 0 && d.intrinsicHeight > 0) {
val shape = d.intrinsicWidth.toFloat() / d.intrinsicHeight
trim = !capsule && shape > ratio * 1.25f
offRatio = !trim && kotlin.math.abs(shape / ratio - 1f) > 0.03f
}
},
)
if (showSource) SourceChip(e.source, small = true, modifier = Modifier.align(Alignment.TopStart).padding(6.dp))
val state = when {
download != null -> "${download.percent}%"
e.item != null && StoresState.isUnfinished(e.item) -> stringResource(R.string.stores_resume)
e.installed && libraryUpdateAvailable(e) -> stringResource(R.string.common_update)
else -> null
}
if (state != null) Box(Modifier.align(Alignment.TopEnd).padding(6.dp)) { StateChip(state, ChipTone.BUSY, small = true) }
val showName = LibraryDisplay.title || hot
val showSize = LibraryDisplay.size || hot
if (showName || showSize) Column(
verticalArrangement = Arrangement.spacedBy(1.dp),
modifier = Modifier.align(Alignment.BottomCenter).fillMaxWidth()
.background(Brush.verticalGradient(0f to Color.Transparent, 0.6f to Color.Black.copy(alpha = 0.78f), 1f to Color.Black.copy(alpha = 0.92f)))
.padding(start = 8.dp, end = 8.dp, top = 20.dp, bottom = if (download != null) 14.dp else 6.dp),
) {
if (showName) Text(e.title, fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = if (hot) 2 else 1, overflow = TextOverflow.Ellipsis, lineHeight = 13.sp)
if (showSize) LibrarySize(e)
}
if (download != null) Box(Modifier.align(Alignment.BottomCenter).padding(start = 6.dp, end = 6.dp, bottom = 5.dp)) {
ProgressBarThin(download.fraction, paused = download.state == DownloadState.PAUSED, verify = download.stage == DownloadStage.VERIFY)
}
}
}
@@ -578,19 +671,27 @@ private fun RequestSize(e: LibraryEntry) {
LaunchedEffect(item.key) { StoreSizes.request(ctx, item) }
}
private fun librarySize(e: LibraryEntry): Long? = if (e.installed) e.storeKey?.let { StoreSizes.onDisk[it] }
?: e.game?.gameFiles?.let { FolderSizes.known[it.path] }
else e.item?.let { StoreSizes.size(it) }?.takeIf { it > 0 }
@Composable
internal fun LibraryMarker(e: LibraryEntry, download: DownloadEntry?) {
private fun LibrarySize(e: LibraryEntry) {
val size = librarySize(e)?.takeIf { it > 0 } ?: return
Text(formatBytes(size), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1)
}
@Composable
internal fun LibraryMarker(e: LibraryEntry, download: DownloadEntry?, showDownload: Boolean = true) {
val pal = LocalPalette.current
val size = if (e.installed) e.storeKey?.let { StoreSizes.onDisk[it] }
?: e.game?.gameFiles?.let { FolderSizes.known[it.path] }
else e.item?.let { StoreSizes.size(it) }?.takeIf { it > 0 }
val size = librarySize(e)
when {
download != null -> Text("${download.percent}%", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1)
e.item != null && StoresState.isUnfinished(e.item) -> Text(stringResource(R.string.stores_resume), fontSize = 10.sp, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1)
else -> Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(3.dp)) {
if (e.installed && libraryUpdateAvailable(e)) Text(stringResource(R.string.common_update), fontSize = 10.sp, fontWeight = FontWeight.Bold, color = pal.signal, maxLines = 1)
if (size != null && size > 0) Text(formatBytes(size), fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, maxLines = 1)
if (!e.installed) Icon(Icons.Outlined.Download, stringResource(R.string.stores_section_not_installed), tint = Color.White, modifier = Modifier.size(12.dp))
if (!e.installed && showDownload) Icon(Icons.Outlined.Download, stringResource(R.string.stores_section_not_installed), tint = Color.White, modifier = Modifier.size(12.dp))
}
}
}
@@ -766,6 +867,45 @@ private fun CollectionsButton() {
}
}
@Composable
private fun DisplayButton() {
val pal = LocalPalette.current
val ctx = LocalContext.current
val title = stringResource(R.string.library_display)
var at by remember { mutableStateOf<Rect?>(null) }
Box(Modifier.onGloballyPositioned { at = it.boundsInRoot() }) {
HeaderIconButton(title, Icons.Filled.Visibility, "display") {
val from = at ?: return@HeaderIconButton
Steps.ask(StepAsk(
anchor = from, side = StepSide.Left, accent = pal.signal, pillCorner = 8.dp, items = 4,
handle = { Icon(Icons.Filled.Visibility, null, tint = pal.signal, modifier = Modifier.size(18.dp)) },
) {
StepTitle(title)
StepList {
val last = LibraryDisplay.scales.lastIndex
StepChoice(
stringResource(R.string.library_display_larger), icon = Icons.Filled.Add, enabled = open && LibraryDisplay.level < last,
trailing = "${LibraryDisplay.level + 1}/${last + 1}",
modifier = Modifier.fillMaxWidth().stepItem(0).focusRequester(first),
) { LibraryDisplay.step(ctx, 1) }
StepChoice(
stringResource(R.string.library_display_smaller), icon = Icons.Filled.Remove, enabled = open && LibraryDisplay.level > 0,
modifier = Modifier.fillMaxWidth().stepItem(1),
) { LibraryDisplay.step(ctx, -1) }
StepChoice(
stringResource(R.string.library_display_title), selected = LibraryDisplay.title, enabled = open,
modifier = Modifier.fillMaxWidth().stepItem(2),
) { LibraryDisplay.toggleTitle(ctx) }
StepChoice(
stringResource(R.string.library_display_size), selected = LibraryDisplay.size, enabled = open,
modifier = Modifier.fillMaxWidth().stepItem(3),
) { LibraryDisplay.toggleSize(ctx) }
}
})
}
}
}
@Composable
private fun SortButton(s: FrontEndState, a: FrontEndActions) {
val pal = LocalPalette.current
@@ -1059,7 +1059,7 @@ internal fun SectionLabel(title: String) {
internal fun ModeHeader(
title: String, query: String, onQuery: (String) -> Unit, placeholder: String,
clearSearchFocus: Boolean = false,
lead: (@Composable () -> Unit)? = null, trailing: @Composable () -> Unit,
lead: (@Composable () -> Unit)? = null, inlineWhenStacked: Boolean = false, trailing: @Composable () -> Unit,
) {
val colors = MaterialTheme.colorScheme
val pal = LocalPalette.current
@@ -1101,8 +1101,23 @@ internal fun ModeHeader(
}
BoxWithConstraints(Modifier.fillMaxWidth()) {
val stacked = maxWidth < NarrowPaneWidth
val inline = stacked && inlineWhenStacked && maxWidth >= 440.dp
Column(Modifier.fillMaxWidth()) {
if (stacked) {
if (inline && !(searching || open > 0.01f)) Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.fillMaxWidth().heightIn(min = 52.dp).padding(vertical = 4.dp),
) {
heading()
Box(Modifier.weight(1f), contentAlignment = Alignment.CenterEnd) {
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.horizontalScroll(rememberScrollState()),
) {
searchButton()
trailing()
}
}
} else if (stacked) {
Box(Modifier.padding(vertical = 4.dp)) { heading() }
Row(
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp),
+5
View File
@@ -2308,6 +2308,11 @@
<string name="stores_resume">Resume</string>
<string name="stores_refresh">Refresh</string>
<string name="stores_sort">Sort</string>
<string name="library_display">Display</string>
<string name="library_display_larger">Larger cards</string>
<string name="library_display_smaller">Smaller cards</string>
<string name="library_display_title">Show title</string>
<string name="library_display_size">Show size</string>
<string name="stores_sort_last_played">Recently played</string>
<string name="stores_sort_recently_installed">Recently installed</string>
<string name="stores_sort_title_asc">Title A–Z</string>
+4 -3
View File
@@ -24,7 +24,8 @@ java_dir="${JAVA_HOME:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents
[[ -x "$java_dir/bin/java" ]] || java_dir="${HOME}/.local/share/mise/installs/java/temurin-17"
[[ "$java_dir" == */homebrew/* ]] && echo "using homebrew java path: $java_dir"
[[ "$java_dir" == */mise/* ]] && echo "using mise java path: $java_dir"
image_name=${DROIDDECK_BUILD_IMAGE:-droiddeck-local-cross:24.04-v3}
. "${repo_root}/tools/docker_platform.sh"
image_name=${DROIDDECK_BUILD_IMAGE}
build_variant=${DROIDDECK_BUILD_VARIANT:-release}
case "$build_variant" in
debug) gradle_task=assembleDebug ;;
@@ -113,7 +114,7 @@ image_id=$(docker image inspect --format '{{.Id}}' "${image_name}")
rm -rf -- "${linuxfs_dir}"
mkdir -p "${linuxfs_dir}"
docker run --rm --platform linux/amd64 \
docker run --rm --platform "${docker_platform}" \
--user "$(id -u):$(id -g)" \
-v "${repo_root}:/src" -w /src "${image_name}" bash -lc '
set -euo pipefail
@@ -283,7 +284,7 @@ install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp"
for library in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
cp -L "${mango_pkgs}/usr/lib/${library}" "${mango_dir}/${library}"
done
docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" -v "${repo_root}:/src" -w /src "${image_name}" \
docker run --rm --platform "${docker_platform}" --user "$(id -u):$(id -g)" -v "${repo_root}:/src" -w /src "${image_name}" \
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
-o app/src/main/assets/linuxfs/usr/local/lib/mangoapp/libtracefs.so.1 tools/mangoapp/libtracefs-shim.c
mkdir -p "${linuxfs_dir}/usr/local/bin"
+8 -3
View File
@@ -1,14 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
repo=$(cd "$(dirname "$0")/../.." && pwd)
image=${DROIDDECK_BUILD_IMAGE:-droiddeck-local-cross:24.04-v3}
docker run --rm --platform linux/amd64 -v "$repo:/src" \
-v "${HOME}/.cache/droiddeck-comet-rust:/rust" -w /src \
. "$repo/tools/docker_platform.sh"
image=${DROIDDECK_BUILD_IMAGE}
platform=${DROIDDECK_DOCKER_PLATFORM}
rust_cache="${HOME}/.cache/droiddeck-comet-rust${platform#linux/amd64}"
rust_cache=${rust_cache/\/arm64/-arm64}
docker run --rm --platform "$platform" -v "$repo:/src" \
-v "${rust_cache}:/rust" -w /src \
-e CARGO_HOME=/rust/cargo -e RUSTUP_HOME=/rust/rustup "$image" bash -lc '
set -euo pipefail
trap "chown -R '"$(id -u):$(id -g)"' app/build/comet app/src/main/assets/linuxfs/usr/local/lib/droiddeck-comet /rust 2>/dev/null || true" EXIT
apt-get update -qq
apt-get install -y -qq curl git unzip ca-certificates pkg-config >/dev/null
git config --global --add safe.directory "*"
export PATH=/rust/cargo/bin:$PATH
if ! command -v cargo >/dev/null; then
curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.90.0
+11
View File
@@ -0,0 +1,11 @@
# Sourced by the build scripts: picks the Docker platform and the matching cross-build image.
docker_platform=${DROIDDECK_DOCKER_PLATFORM:-}
if [[ -z "${docker_platform}" ]]; then
case "$(uname -m)" in arm64|aarch64) docker_platform=linux/arm64 ;; *) docker_platform=linux/amd64 ;; esac
fi
case "${docker_platform}" in linux/arm64|linux/arm64/*) docker_platform=linux/arm64 ;; esac
if [[ -z "${DROIDDECK_BUILD_IMAGE:-}" ]]; then
DROIDDECK_BUILD_IMAGE=droiddeck-local-cross:24.04-v3
[[ "${docker_platform}" == linux/arm64 ]] && DROIDDECK_BUILD_IMAGE+=-arm64
fi
export DROIDDECK_DOCKER_PLATFORM=${docker_platform} DROIDDECK_BUILD_IMAGE
+2 -1
View File
@@ -1,4 +1,5 @@
"""Check the engine before modifying build assets; never rebuild for a daemon error."""
import os
from pathlib import Path
import subprocess
import sys
@@ -20,5 +21,5 @@ if __name__ == '__main__':
if 'No such image' not in image.stderr and 'image not known' not in image.stderr: # Docker, Podman
sys.exit('Cannot inspect the build image: ' + image.stderr.strip())
repo = Path(__file__).resolve().parents[1]
subprocess.run(['docker', 'build', '--platform', 'linux/amd64', '-t', sys.argv[1],
subprocess.run(['docker', 'build', '--platform', os.environ.get('DROIDDECK_DOCKER_PLATFORM', 'linux/amd64'), '-t', sys.argv[1],
'-f', str(repo / 'tools/local-cross.Dockerfile'), str(repo)], check=True)
+6 -10
View File
@@ -3,16 +3,12 @@ FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y \
binutils \
binutils-aarch64-linux-gnu \
gcc \
g++ \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
python3 \
zstd \
unzip \
&& apt-get install -y binutils gcc g++ python3 zstd unzip \
&& if [ "$(uname -m)" = aarch64 ]; then \
for tool in gcc g++ strip readelf objdump ar; do ln -s "$(command -v $tool)" /usr/local/bin/aarch64-linux-gnu-$tool; done; \
else \
apt-get install -y binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu g++-aarch64-linux-gnu; \
fi \
&& mkdir -p /src
WORKDIR /src
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Kotlin/resource-only loop: reuses the assets build_local.sh already prepared, then signs and installs.
set -euo pipefail
repo_root=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "${repo_root}"
sdk_dir=${ANDROID_HOME:-${ANDROID_SDK_ROOT:-"${HOME}/Library/Android/sdk"}}
export ANDROID_HOME=${sdk_dir} ANDROID_SDK_ROOT=${sdk_dir}
export JAVA_HOME=${JAVA_HOME:-/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home}
out=${1:-"${TMPDIR:-/tmp}/droiddeck-quick-signed.apk"}
signing_env=${DROIDDECK_SIGNING_ENV:-}
if [[ -z "${signing_env}" ]]; then
for candidate in "${repo_root}/.signing.env" \
"$(dirname "$(git -C "${repo_root}" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || echo .)")/.signing.env"; do
[[ -f "${candidate}" ]] && { signing_env=${candidate}; break; }
done
fi
[[ -f "${signing_env}" ]] || { echo "No signing env found; set DROIDDECK_SIGNING_ENV or add .signing.env." >&2; exit 1; }
build_tools=
while IFS= read -r dir; do
[[ -x "${dir}/aapt" && -x "${dir}/zipalign" && -x "${dir}/apksigner" ]] && { build_tools=${dir}; break; }
done < <(ls -d "${sdk_dir}"/build-tools/* 2>/dev/null | sort -rV)
[[ -n "${build_tools}" ]] || { echo "No build-tools with aapt, zipalign and apksigner under ${sdk_dir}." >&2; exit 1; }
./gradlew -q :app:assembleRelease
(
cd "$(dirname "${signing_env}")"
set -a; . "${signing_env}"; set +a
BUILD_TOOLS=${build_tools} "${repo_root}/tools/release/sign-apk.sh" \
"${repo_root}/app/build/outputs/apk/release/app-release.apk" standard "${out}" | tail -1
)
"${repo_root}/tools/deploy_local.sh" "${out}" | tail -2
+3 -2
View File
@@ -5,7 +5,8 @@ cd "$repo_root"
if [[ $(uname -s) == Linux ]]; then
exec python3 -m unittest discover -s tools/tests "$@"
fi
image=${DROIDDECK_BUILD_IMAGE:-droiddeck-local-cross:24.04-v3}
. "${repo_root}/tools/docker_platform.sh"
image=${DROIDDECK_BUILD_IMAGE}
python3 tools/docker_preflight.py "$image"
exec docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" \
exec docker run --rm --platform "${DROIDDECK_DOCKER_PLATFORM}" --user "$(id -u):$(id -g)" \
-v "$repo_root:/src:ro" -w /src "$image" python3 -m unittest discover -s tools/tests "$@"
+2 -1
View File
@@ -2,6 +2,7 @@
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
repo=$(cd "$here/../.." && pwd)
. "$repo/tools/docker_platform.sh"
native=0
if [ "${1:-}" = --native ]; then native=1; shift; fi
@@ -87,7 +88,7 @@ compile() {
if [ "$native" = 1 ]; then
(cd "$repo" && "${cmd[@]}")
else
docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" -v "$repo:/src" -w /src \
docker run --rm --platform "${DROIDDECK_DOCKER_PLATFORM:-linux/amd64}" --user "$(id -u):$(id -g)" -v "$repo:/src" -w /src \
"${DROIDDECK_BUILD_IMAGE:-droiddeck-local-cross:24.04-v3}" "${cmd[@]}"
fi
}