feat: error log info screen

This commit is contained in:
rushiiMachine 2025-04-06 21:26:31 -07:00
parent 95ff30684d
commit 3a9e3562ca
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
14 changed files with 468 additions and 88 deletions

View file

@ -36,4 +36,4 @@ jobs:
git add * git add *
git merge l10n git merge l10n
git push git push
git push origin --delete l10n git push origin --delete l10n || true

View file

@ -150,8 +150,9 @@ dependencies {
debugImplementation(libs.compose.ui.tooling) debugImplementation(libs.compose.ui.tooling)
debugImplementation(libs.compose.runtime.tracing) debugImplementation(libs.compose.runtime.tracing)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.immutable) implementation(libs.kotlinx.immutable)
implementation(libs.kotlinx.serialization.json)
implementation(libs.apksig) implementation(libs.apksig)
implementation(libs.axml) implementation(libs.axml)

View file

@ -10,6 +10,7 @@ import com.aliucord.manager.network.services.*
import com.aliucord.manager.ui.screens.about.AboutModel import com.aliucord.manager.ui.screens.about.AboutModel
import com.aliucord.manager.ui.screens.home.HomeModel import com.aliucord.manager.ui.screens.home.HomeModel
import com.aliucord.manager.ui.screens.iconopts.IconOptionsModel import com.aliucord.manager.ui.screens.iconopts.IconOptionsModel
import com.aliucord.manager.ui.screens.log.LogScreenModel
import com.aliucord.manager.ui.screens.patching.PatchingScreenModel import com.aliucord.manager.ui.screens.patching.PatchingScreenModel
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
import com.aliucord.manager.ui.screens.plugins.PluginsModel import com.aliucord.manager.ui.screens.plugins.PluginsModel
@ -54,6 +55,7 @@ class ManagerApplication : Application() {
factoryOf(::SettingsModel) factoryOf(::SettingsModel)
factoryOf(::PatchOptionsModel) factoryOf(::PatchOptionsModel)
factoryOf(::IconOptionsModel) factoryOf(::IconOptionsModel)
factoryOf(::LogScreenModel)
viewModelOf(::UpdaterViewModel) viewModelOf(::UpdaterViewModel)
}) })
@ -63,6 +65,7 @@ class ManagerApplication : Application() {
singleOf(::PathManager) singleOf(::PathManager)
singleOf(::InstallerManager) singleOf(::InstallerManager)
singleOf(::OverlayManager) singleOf(::OverlayManager)
singleOf(::InstallLogManager)
singleOf(::AndroidDownloadManager) singleOf(::AndroidDownloadManager)
singleOf(::KtorDownloadManager) singleOf(::KtorDownloadManager)

View file

@ -0,0 +1,106 @@
package com.aliucord.manager.manager
import android.app.Application
import android.os.Build
import android.util.Log
import androidx.compose.runtime.Immutable
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.ui.screens.patchopts.PatchOptions
import com.aliucord.manager.util.IS_PROBABLY_EMULATOR
import com.aliucord.manager.util.isPlayProtectEnabled
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
/**
* Central manager for storing all attempted installations and
* their associated logs/crashes (not including manager crashes themselves).
*/
class InstallLogManager(
private val application: Application,
private val json: Json,
) {
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
/**
* Loads the install log from disk, if it exists.
*/
fun fetchInstallData(id: String): InstallLogData? {
val path = logsDir.resolve("$id.json")
if (!path.exists()) return null
return try {
@OptIn(ExperimentalSerializationApi::class)
json.decodeFromStream(path.inputStream())
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to open install log $id", t)
null
}
}
/**
* Writes an install log entry to disk.
*/
suspend fun storeInstallData(
id: String,
installDate: Instant,
options: PatchOptions,
error: Throwable?,
) {
val path = logsDir.resolve("$id.json")
val data = InstallLogData(
id = id,
installDate = installDate,
installOptions = options,
environmentInfo = getEnvironmentInfo(),
installationLog = "", // TODO: install log
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
)
path.writeText(json.encodeToString(data))
}
/**
* Creates a list of details about the current installation environment.
*/
suspend fun getEnvironmentInfo(): String {
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else ""
val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unavailable"
val playProtect = when (application.isPlayProtectEnabled()) {
null -> "Unavailable"
true -> "Enabled"
false -> "Disabled"
}
return """
Aliucord Manager v${BuildConfig.VERSION_NAME}
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
Android API: ${Build.VERSION.SDK_INT}
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
Emulator: $IS_PROBABLY_EMULATOR (guess)
Play Protect: $playProtect
SOC: $soc
""".trimIndent()
}
}
@Immutable
@Serializable
data class InstallLogData(
val id: String,
val installDate: Instant,
val installOptions: PatchOptions,
val environmentInfo: String,
val installationLog: String,
val errorStacktrace: String?,
) {
val isError: Boolean
get() = errorStacktrace != null
}

View file

@ -0,0 +1,132 @@
package com.aliucord.manager.ui.screens.log
import android.os.Parcelable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.aliucord.manager.R
import com.aliucord.manager.manager.InstallLogData
import com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.screens.log.components.SelectableTextArea
import com.aliucord.manager.ui.screens.patchopts.components.options.PatchOption
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.koin.core.parameter.parametersOf
@Parcelize
class LogScreen(private val installId: String) : Screen, Parcelable {
@IgnoredOnParcel
override val key: ScreenKey
get() = "LogScreen-$installId"
@Composable
override fun Content() {
val model = getScreenModel<LogScreenModel> { parametersOf(installId) }
val navigator = LocalNavigator.currentOrThrow
if (model.shouldCloseScreen) {
navigator.back(currentActivity = null)
}
model.data?.let {
LogScreenContent(
data = it,
onSaveLog = model::saveLog,
)
}
}
}
@Composable
fun LogScreenContent(
data: InstallLogData,
onSaveLog: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.log_title)) },
navigationIcon = { BackButton() },
)
},
) { paddingValues ->
LazyColumn(
verticalArrangement = Arrangement.spacedBy(28.dp),
modifier = Modifier
.padding(paddingValues)
.padding(vertical = 10.dp, horizontal = 22.dp)
) {
item("INSTALL_INFO") {
PatchOption(
name = stringResource(R.string.log_section_install_info),
description = null,
) {
SelectableTextArea(
text = """
Installation ID: ${data.id}
Installation Date: ${data.installDate}
""".trimIndent(),
modifier = Modifier.fillMaxWidth(),
)
}
}
item("ENVIRONMENT_INFO") {
PatchOption(
name = stringResource(R.string.log_section_env_info),
description = stringResource(R.string.log_section_env_info_desc),
) {
SelectableTextArea(
text = data.environmentInfo,
modifier = Modifier.fillMaxWidth(),
)
}
}
if (data.errorStacktrace != null) {
item("ERROR_STACKTRACE") {
PatchOption(
name = stringResource(R.string.log_section_error),
description = null,
) {
SelectableTextArea(
text = data.errorStacktrace,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
// item("LOG") {
// PatchOption(
// name = stringResource(R.string.log_section_log),
// description = null,
// ) {
// SelectableTextArea(
// text = data.installationLog,
// modifier = Modifier.fillMaxWidth(),
// )
// }
// }
item("SAVE_BUTTON") {
MainActionButton(
text = stringResource(R.string.log_action_export),
icon = painterResource(R.drawable.ic_download),
onClick = onSaveLog,
modifier = Modifier.padding(top = 10.dp),
)
}
}
}
}

View file

@ -0,0 +1,76 @@
package com.aliucord.manager.ui.screens.log
import android.annotation.SuppressLint
import android.app.Application
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.R
import com.aliucord.manager.manager.InstallLogData
import com.aliucord.manager.manager.InstallLogManager
import com.aliucord.manager.util.*
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class LogScreenModel(
private val installId: String,
private val logs: InstallLogManager,
private val application: Application,
) : ScreenModel {
var shouldCloseScreen by mutableStateOf(false)
private set
var data by mutableStateOf<InstallLogData?>(null)
private set
init {
loadLogData()
}
/**
* Formats the log data into a file and writes it to the downloads folder.
*/
fun saveLog() = screenModelScope.launchBlock {
val data = data ?: return@launchBlock
@SuppressLint("SimpleDateFormat")
val formattedDate = SimpleDateFormat("yyyy-MM-dd hh-mm-s a", Locale.ENGLISH)
.format(Date(data.installDate.toEpochMilliseconds()))
val content = buildString {
appendLine("////////////////// Installation Info //////////////////")
append("Install ID: ")
appendLine(data.id)
append("Install time: ")
appendLine(formattedDate)
append("Result: ")
appendLine(if (data.isError) "Failure" else "Success")
append("\n\n")
appendLine("////////////////// Environment Info //////////////////")
appendLine(data.environmentInfo)
append("\n\n")
appendLine("////////////////// Error Stacktrace //////////////////")
appendLine(data.errorStacktrace ?: "None")
append("\n\n")
appendLine("////////////////// Installation Log //////////////////")
appendLine(data.installationLog)
}
application.saveFile("Aliucord Manager $formattedDate.log", content)
}
private fun loadLogData() = screenModelScope.launchBlock {
val result = logs.fetchInstallData(installId)
if (result != null) {
data = result
} else {
shouldCloseScreen = true
application.showToast(R.string.network_load_fail)
}
}
}

View file

@ -0,0 +1,40 @@
package com.aliucord.manager.ui.screens.log.components
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import com.aliucord.manager.ui.util.horizontalScrollbar
import com.aliucord.manager.ui.util.thenIf
@Composable
fun SelectableTextArea(
text: String,
modifier: Modifier = Modifier.Companion,
) {
val scrollState = rememberScrollState()
val scrollable by remember { derivedStateOf { scrollState.maxValue > 0 } }
SelectionContainer {
Text(
text = text,
style = MaterialTheme.typography.labelSmall,
fontFamily = FontFamily.Companion.Monospace,
softWrap = false,
modifier = modifier
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
.padding(start = 18.dp, end = 18.dp, top = 14.dp, bottom = 14.dp)
.horizontalScroll(scrollState)
.horizontalScrollbar(scrollState)
.thenIf(scrollable) { padding(bottom = 10.dp) }
)
}
}

View file

@ -220,12 +220,13 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
enter = fadeIn() + slideInVertically(), enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically { it * -2 }, exit = fadeOut() + slideOutVertically { it * -2 },
) { ) {
Column { Column(
verticalArrangement = Arrangement.spacedBy(VERTICAL_PADDING / 2),
) {
HorizontalDivider( HorizontalDivider(
thickness = 1.dp, thickness = 1.dp,
modifier = Modifier modifier = Modifier
.padding(vertical = 4.dp) .padding(bottom = VERTICAL_PADDING / 2)
.padding(bottom = VERTICAL_PADDING)
) )
when (filteredState) { when (filteredState) {
@ -244,7 +245,13 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
MainActionButton( MainActionButton(
text = stringResource(R.string.action_retry_install), text = stringResource(R.string.action_retry_install),
icon = painterResource(R.drawable.ic_refresh), icon = painterResource(R.drawable.ic_refresh),
onClick = model::restart, onClick = model::launchApp,
)
MainActionButton(
text = stringResource(R.string.action_open_error_log),
icon = painterResource(R.drawable.ic_launch),
onClick = { model.openLog(navigator) },
) )
} }
} }
@ -260,9 +267,6 @@ class PatchingScreen(private val data: PatchOptions) : Screen, Parcelable {
cacheCleared = true cacheCleared = true
model.clearCache() model.clearCache()
}, },
modifier = Modifier
.padding(top = 14.dp)
.fillMaxWidth()
) )
} }
} }
@ -303,21 +307,3 @@ private fun BannerSection(
} }
} }
} }
// TODO: error log
// item(key = "ERROR_LOG") {
// SelectionContainer {
// Text(
// text = failureLog,
// style = MaterialTheme.typography.labelSmall,
// fontFamily = FontFamily.Monospace,
// softWrap = false,
// modifier = Modifier
// .padding(top = VERTICAL_PADDING)
// .clip(RoundedCornerShape(10.dp))
// .background(MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp))
// .padding(24.dp)
// .horizontalScroll(rememberScrollState())
// )
// }
// }

View file

@ -2,39 +2,42 @@ package com.aliucord.manager.ui.screens.patching
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.* import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.manager.PathManager import com.aliucord.manager.manager.*
import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.patcher.KotlinPatchRunner import com.aliucord.manager.patcher.KotlinPatchRunner
import com.aliucord.manager.patcher.StepRunner import com.aliucord.manager.patcher.StepRunner
import com.aliucord.manager.patcher.steps.StepGroup import com.aliucord.manager.patcher.steps.StepGroup
import com.aliucord.manager.patcher.steps.base.Step import com.aliucord.manager.patcher.steps.base.Step
import com.aliucord.manager.patcher.steps.base.StepState import com.aliucord.manager.patcher.steps.base.StepState
import com.aliucord.manager.patcher.steps.install.InstallStep import com.aliucord.manager.patcher.steps.install.InstallStep
import com.aliucord.manager.ui.screens.log.LogScreen
import com.aliucord.manager.ui.screens.patchopts.PatchOptions import com.aliucord.manager.ui.screens.patchopts.PatchOptions
import com.aliucord.manager.ui.util.toUnsafeImmutable import com.aliucord.manager.ui.util.toUnsafeImmutable
import com.aliucord.manager.util.* import com.aliucord.manager.util.showToast
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.text.SimpleDateFormat import kotlinx.datetime.Clock
import java.util.Date import kotlinx.datetime.Instant
import java.util.UUID
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class PatchingScreenModel( class PatchingScreenModel(
private val application: Application,
private val paths: PathManager,
private val options: PatchOptions, private val options: PatchOptions,
private val paths: PathManager,
private val prefs: PreferencesManager, private val prefs: PreferencesManager,
private val application: Application,
private val installLogs: InstallLogManager,
) : StateScreenModel<PatchingScreenState>(PatchingScreenState.Working) { ) : StateScreenModel<PatchingScreenState>(PatchingScreenState.Working) {
private var startTime: Date? = null private var installId: String? = null
private var startTime: Instant? = null
private var runnerJob: Job? = null private var runnerJob: Job? = null
private var stepRunner: StepRunner? = null private var stepRunner: StepRunner? = null
@ -73,26 +76,6 @@ class PatchingScreenModel(
} }
} }
fun copyDebugToClipboard() {
val content = (state.value as? PatchingScreenState.Failed)?.failureLog
?: return
application.copyToClipboard(content)
application.showToast(R.string.action_copied)
}
fun saveFailureLog() {
val startTime = startTime ?: return
val failureLog = (state.value as? PatchingScreenState.Failed)?.failureLog
?: return
@SuppressLint("SimpleDateFormat")
val formattedDate = SimpleDateFormat("yyyy-MM-dd hh-mm-s a").format(startTime)
val fileName = "Aliucord Manager $formattedDate.log"
application.saveFile(fileName, failureLog)
}
fun clearCache() { fun clearCache() {
screenModelScope.launch { paths.clearCache() } screenModelScope.launch { paths.clearCache() }
application.showToast(R.string.action_cleared_cache) application.showToast(R.string.action_cleared_cache)
@ -102,10 +85,14 @@ class PatchingScreenModel(
runnerJob?.cancel("Manual cancellation") runnerJob?.cancel("Manual cancellation")
steps = null steps = null
startTime = Date() @SuppressLint("MemberExtensionConflict")
installId = UUID.randomUUID().toString()
startTime = Clock.System.now()
mutableState.value = PatchingScreenState.Working mutableState.value = PatchingScreenState.Working
runnerJob = screenModelScope.launch { runnerJob = screenModelScope.launch {
Log.i(BuildConfig.TAG, "Starting installation with environment:\n" + installLogs.getEnvironmentInfo())
try { try {
startPatchRunner() startPatchRunner()
} catch (_: CancellationException) { } catch (_: CancellationException) {
@ -113,11 +100,22 @@ class PatchingScreenModel(
mutableState.value = PatchingScreenState.CloseScreen mutableState.value = PatchingScreenState.CloseScreen
} catch (error: Throwable) { } catch (error: Throwable) {
Log.e(BuildConfig.TAG, "Failed to orchestrate patch runner", error) Log.e(BuildConfig.TAG, "Failed to orchestrate patch runner", error)
mutableState.value = PatchingScreenState.Failed(failureLog = getFailureInfo(error)) mutableState.value = PatchingScreenState.Failed(installId = installId!!)
installLogs.storeInstallData(
id = installId!!,
installDate = startTime!!,
options = options,
error = error,
)
} }
} }
} }
fun openLog(navigator: Navigator) {
val id = installId ?: return
navigator.push(LogScreen(installId = id))
}
private suspend fun startPatchRunner() { private suspend fun startPatchRunner() {
val runner = KotlinPatchRunner(options) val runner = KotlinPatchRunner(options)
.also { stepRunner = it } .also { stepRunner = it }
@ -141,41 +139,28 @@ class PatchingScreenModel(
// At this point, the installation has successfully completed // At this point, the installation has successfully completed
else { else {
mutableState.value = PatchingScreenState.Success mutableState.value = PatchingScreenState.Success
installLogs.storeInstallData(
id = installId!!,
installDate = startTime!!,
options = options,
error = null,
)
} }
} }
else -> { else -> {
Log.e(BuildConfig.TAG, "Failed to perform installation process", error) Log.e(BuildConfig.TAG, "Failed to perform installation process", error)
mutableState.value = PatchingScreenState.Failed(failureLog = getFailureInfo(error)) mutableState.value = PatchingScreenState.Failed(installId = installId!!)
installLogs.storeInstallData(
id = installId!!,
installDate = startTime!!,
options = options,
error = error,
)
} }
} }
} }
private suspend fun getFailureInfo(stacktrace: Throwable): String {
val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else ""
val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unavailable"
val playProtect = when (application.isPlayProtectEnabled()) {
null -> "Unavailable"
true -> "Enabled"
false -> "Disabled"
}
val header = """
Aliucord Manager v${BuildConfig.VERSION_NAME}
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges
Android API: ${Build.VERSION.SDK_INT}
Emulator: $IS_PROBABLY_EMULATOR (guess)
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
Play Protect: $playProtect
SOC: $soc
""".trimIndent()
return header + "\n\n" + Log.getStackTraceString(stacktrace).trimEnd()
}
private companion object { private companion object {
/** /**
* Random fun facts to show on the installation screen. * Random fun facts to show on the installation screen.

View file

@ -5,7 +5,7 @@ import com.aliucord.manager.ui.screens.patching.PatchingScreenState.CloseScreen
sealed interface PatchingScreenState { sealed interface PatchingScreenState {
data object Working : PatchingScreenState data object Working : PatchingScreenState
data object Success : PatchingScreenState data object Success : PatchingScreenState
data class Failed(val failureLog: String) : PatchingScreenState data class Failed(val installId: String) : PatchingScreenState
data object CloseScreen : PatchingScreenState data object CloseScreen : PatchingScreenState
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
// TODO: rename this to a generic titled component
@Composable @Composable
fun PatchOption( fun PatchOption(
name: String, name: String,
@ -17,7 +18,7 @@ fun PatchOption(
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
modifier = modifier, modifier = modifier,
) { ) {

View file

@ -0,0 +1,39 @@
package com.aliucord.manager.ui.util
import androidx.compose.foundation.ScrollState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlin.math.max
// Based on https://gist.github.com/XFY9326/2067efcc3c5899557cc6a334d76a92c8
fun Modifier.horizontalScrollbar(
scrollState: ScrollState,
scrollBarHeight: Dp = 4.dp,
minScrollBarWidth: Dp = 5.dp,
scrollBarColor: Color = Color.White.copy(alpha = .4f),
cornerRadius: Dp = 2.dp,
): Modifier = composed {
drawWithContent {
drawContent()
if (scrollState.maxValue <= 0) return@drawWithContent
val visibleWidth: Float = this.size.width - scrollState.maxValue
val scrollBarWidth: Float = max(visibleWidth * (visibleWidth / this.size.width), minScrollBarWidth.toPx())
val scrollPercent: Float = scrollState.value.toFloat() / scrollState.maxValue
val scrollBarOffsetX: Float = scrollState.value + (visibleWidth - scrollBarWidth) * scrollPercent
drawRoundRect(
color = scrollBarColor,
topLeft = Offset(scrollBarOffsetX, this.size.height - scrollBarHeight.toPx()),
size = Size(scrollBarWidth, scrollBarHeight.toPx()),
cornerRadius = CornerRadius(cornerRadius.toPx())
)
}
}

View file

@ -10,7 +10,8 @@
<string name="action_cancel">Cancel</string> <string name="action_cancel">Cancel</string>
<string name="action_retry">Retry</string> <string name="action_retry">Retry</string>
<string name="action_retry_install">Retry Installation</string> <string name="action_retry_install">Retry installation</string>
<string name="action_open_error_log">Open error log</string>
<string name="action_apply">Apply</string> <string name="action_apply">Apply</string>
<string name="action_confirm">Confirm</string> <string name="action_confirm">Confirm</string>
<string name="action_dismiss">Dismiss</string> <string name="action_dismiss">Dismiss</string>
@ -184,6 +185,14 @@
<string name="iconopts_image_preview_desc">Uncropped preview of the selected image</string> <string name="iconopts_image_preview_desc">Uncropped preview of the selected image</string>
<string name="iconopts_image_preview_none">No image selected</string> <string name="iconopts_image_preview_none">No image selected</string>
<string name="log_title">Log</string>
<string name="log_section_install_info">Installation Info</string>
<string name="log_section_env_info">Environment Info</string>
<string name="log_section_env_info_desc">Information about your device that may be crucial for debugging.</string>
<string name="log_section_error">Error Stacktrace</string>
<string name="log_section_log">Installation Log</string>
<string name="log_action_export">Export log</string>
<string name="notif_group_install_title">Active installation</string> <string name="notif_group_install_title">Active installation</string>
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string> <string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>
<string name="notif_install_ready_title">Aliucord installation ready!</string> <string name="notif_install_ready_title">Aliucord installation ready!</string>

View file

@ -17,6 +17,7 @@ compose-pipette = "1.0.0-beta01"
#diff = "4.12" #diff = "4.12"
koin = "4.0.2" koin = "4.0.2"
kotlin = "2.1.10" kotlin = "2.1.10"
kotlinx-datetime = "0.6.2"
kotlinx-immutable = "0.3.8" kotlinx-immutable = "0.3.8"
kotlinx-serialization = "1.8.0" kotlinx-serialization = "1.8.0"
ktor = "3.0.3" ktor = "3.0.3"
@ -63,8 +64,9 @@ koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } koin-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
# KotlinX # KotlinX
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
kotlinx-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-immutable" } kotlinx-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-immutable" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
# Ktor # Ktor
ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }