mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
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:
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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Executable
+33
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user