mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Keep Android screens from crashing when opening subjects, stats, settings, or maps.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
aa1d8c4426
commit
db0422a952
5 changed files with 136 additions and 61 deletions
|
|
@ -16,10 +16,10 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -51,6 +51,7 @@ import org.maturita.maturita.ui.StatsIcon
|
||||||
import org.maturita.maturita.ui.StatsScreen
|
import org.maturita.maturita.ui.StatsScreen
|
||||||
import org.maturita.maturita.ui.SubjectsScreen
|
import org.maturita.maturita.ui.SubjectsScreen
|
||||||
import org.maturita.maturita.ui.UnitMapScreen
|
import org.maturita.maturita.ui.UnitMapScreen
|
||||||
|
import org.maturita.maturita.data.ColorMode
|
||||||
import org.maturita.maturita.ui.VocabExercise
|
import org.maturita.maturita.ui.VocabExercise
|
||||||
import org.maturita.maturita.ui.WelcomeScreen
|
import org.maturita.maturita.ui.WelcomeScreen
|
||||||
|
|
||||||
|
|
@ -64,9 +65,43 @@ class MainActivity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MaturitaApp(vm: AppViewModel) {
|
fun MaturitaApp(vm: AppViewModel) {
|
||||||
|
val p = vm.palette
|
||||||
|
val scheme = if (vm.mode == ColorMode.Dark) {
|
||||||
|
darkColorScheme(
|
||||||
|
primary = p.accent,
|
||||||
|
onPrimary = p.onAccent,
|
||||||
|
secondary = p.accent2,
|
||||||
|
background = p.base,
|
||||||
|
onBackground = p.text,
|
||||||
|
surface = p.mantle,
|
||||||
|
onSurface = p.text,
|
||||||
|
surfaceVariant = p.surface1,
|
||||||
|
onSurfaceVariant = p.subtext,
|
||||||
|
error = p.error,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
lightColorScheme(
|
||||||
|
primary = p.accent,
|
||||||
|
onPrimary = p.onAccent,
|
||||||
|
secondary = p.accent2,
|
||||||
|
background = p.base,
|
||||||
|
onBackground = p.text,
|
||||||
|
surface = p.mantle,
|
||||||
|
onSurface = p.text,
|
||||||
|
surfaceVariant = p.surface1,
|
||||||
|
onSurfaceVariant = p.subtext,
|
||||||
|
error = p.error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
MaterialTheme(colorScheme = scheme) {
|
||||||
|
MaturitaAppBody(vm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MaturitaAppBody(vm: AppViewModel) {
|
||||||
val p = vm.palette
|
val p = vm.palette
|
||||||
LaunchedEffect(Unit) { vm.checkUpdate(false) }
|
LaunchedEffect(Unit) { vm.checkUpdate(false) }
|
||||||
BackHandler(vm.canGoBack || vm.searchOpen || vm.settingsOpen) {
|
BackHandler(vm.canGoBack || vm.searchOpen || vm.settingsOpen) {
|
||||||
|
|
@ -184,11 +219,10 @@ fun MaturitaApp(vm: AppViewModel) {
|
||||||
}
|
}
|
||||||
if (vm.searchOpen) SearchOverlay(vm)
|
if (vm.searchOpen) SearchOverlay(vm)
|
||||||
if (vm.settingsOpen) {
|
if (vm.settingsOpen) {
|
||||||
ModalBottomSheet(
|
Box(
|
||||||
onDismissRequest = { vm.settingsOpen = false },
|
Modifier
|
||||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
.fillMaxSize()
|
||||||
containerColor = p.base,
|
.background(p.base),
|
||||||
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp),
|
|
||||||
) { SettingsSheet(vm) }
|
) { SettingsSheet(vm) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
|
@ -656,7 +657,9 @@ private fun ComboLine(
|
||||||
revealed: Boolean,
|
revealed: Boolean,
|
||||||
inline: Boolean = false,
|
inline: Boolean = false,
|
||||||
) {
|
) {
|
||||||
if (idx in expected.indices) expected[idx] = answer
|
SideEffect {
|
||||||
|
if (idx in expected.indices && expected[idx] != answer) expected[idx] = answer
|
||||||
|
}
|
||||||
val p = vm.palette
|
val p = vm.palette
|
||||||
var open by remember { mutableStateOf(false) }
|
var open by remember { mutableStateOf(false) }
|
||||||
val box = @Composable {
|
val box = @Composable {
|
||||||
|
|
@ -963,6 +966,7 @@ private fun PrefixMenu(value: String, p: Palette, onPick: (String) -> Unit) {
|
||||||
fun LitQuizScreen(vm: AppViewModel, id: String) {
|
fun LitQuizScreen(vm: AppViewModel, id: String) {
|
||||||
val book = vm.content.book(id) ?: return
|
val book = vm.content.book(id) ?: return
|
||||||
val qs = book.arr("quiz")
|
val qs = book.arr("quiz")
|
||||||
|
if (qs.isEmpty()) return
|
||||||
var idx by remember { mutableIntStateOf(0) }
|
var idx by remember { mutableIntStateOf(0) }
|
||||||
var score by remember { mutableIntStateOf(0) }
|
var score by remember { mutableIntStateOf(0) }
|
||||||
var answered by remember { mutableStateOf(false) }
|
var answered by remember { mutableStateOf(false) }
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ fun PathMap(
|
||||||
nodes: List<MapNode>,
|
nodes: List<MapNode>,
|
||||||
palette: Palette,
|
palette: Palette,
|
||||||
litUntil: Float,
|
litUntil: Float,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
nodeSize: Float = 72f,
|
nodeSize: Float = 72f,
|
||||||
mx: Float = 90f,
|
mx: Float = 90f,
|
||||||
my: Float = 110f,
|
my: Float = 110f,
|
||||||
|
|
@ -139,8 +140,8 @@ fun PathMap(
|
||||||
if (nodes.isEmpty()) return
|
if (nodes.isEmpty()) return
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
fun Float.toDp(): androidx.compose.ui.unit.Dp = with(density) { this@toDp.toDp() }
|
fun Float.toDp(): androidx.compose.ui.unit.Dp = with(density) { this@toDp.toDp() }
|
||||||
BoxWithConstraints(Modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier.fillMaxSize()) {
|
||||||
val avail = constraints.maxWidth.toFloat()
|
val avail = constraints.maxWidth.toFloat().coerceAtLeast(1f)
|
||||||
val geo = serpentine(nodes.size, avail, mx, my, spac, gap, wave, lift)
|
val geo = serpentine(nodes.size, avail, mx, my, spac, gap, wave, lift)
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,9 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
|
@ -22,7 +21,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -31,7 +29,6 @@ import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
@ -108,28 +105,26 @@ private fun FeatureCard(vm: AppViewModel, t: String, b: String, l: String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SubjectsScreen(vm: AppViewModel) {
|
fun SubjectsScreen(vm: AppViewModel) {
|
||||||
val p = vm.palette
|
val p = vm.palette
|
||||||
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp)) {
|
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp)) {
|
||||||
PageTop(vm, { vm.back() }, vm.tr("subjects_title"), vm.tr("subjects_sub"))
|
PageTop(vm, { vm.back() }, vm.tr("subjects_title"), vm.tr("subjects_sub"))
|
||||||
androidx.compose.foundation.layout.BoxWithConstraints(Modifier.fillMaxSize()) {
|
Column(
|
||||||
val cell = (maxWidth - 12.dp) / 2
|
|
||||||
FlowRow(
|
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(bottom = 24.dp),
|
.padding(bottom = 24.dp),
|
||||||
maxItemsInEachRow = 2,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
vm.content.subjects.forEach { s ->
|
vm.content.subjects.chunked(2).forEach { row ->
|
||||||
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
row.forEach { s ->
|
||||||
val open = s.bool("open")
|
val open = s.bool("open")
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.width(cell)
|
.weight(1f)
|
||||||
.height(132.dp)
|
.height(132.dp)
|
||||||
.clip(RoundedCornerShape(20.dp))
|
.clip(RoundedCornerShape(20.dp))
|
||||||
.background(p.mantle)
|
.background(p.mantle)
|
||||||
|
|
@ -152,6 +147,8 @@ fun SubjectsScreen(vm: AppViewModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (row.size == 1) Spacer(Modifier.weight(1f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +163,16 @@ fun SettingsSheet(vm: AppViewModel) {
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(20.dp),
|
.padding(20.dp),
|
||||||
) {
|
) {
|
||||||
Text(vm.tr("settings_title"), color = p.text, fontSize = 22.sp, fontWeight = FontWeight.Bold)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
vm.tr("settings_title"),
|
||||||
|
color = p.text,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
PillButton(vm.tr("back"), p) { vm.settingsOpen = false }
|
||||||
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text(vm.tr("mode"), color = p.subtext, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
Text(vm.tr("mode"), color = p.subtext, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
|
|
@ -233,14 +239,14 @@ fun StatsScreen(vm: AppViewModel) {
|
||||||
val pct = if (sum.totalEx == 0) 0 else (100 * sum.doneEx / sum.totalEx)
|
val pct = if (sum.totalEx == 0) 0 else (100 * sum.doneEx / sum.totalEx)
|
||||||
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp)) {
|
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp)) {
|
||||||
PageTop(vm, { vm.back() }, vm.tr("stats_title"), vm.tr("stats_sub"))
|
PageTop(vm, { vm.back() }, vm.tr("stats_title"), vm.tr("stats_sub"))
|
||||||
Column(Modifier.verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) {
|
Column(Modifier.weight(1f).verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) {
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
Metric(p, vm.tr("stats_ex_label"), vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx), Modifier.weight(1f))
|
Metric(p, vm.tr("stats_ex_label"), vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx), Modifier.weight(1f))
|
||||||
Metric(p, vm.tr("stats_pct_label"), vm.fmt("stats_pct_fmt", pct), Modifier.weight(1f))
|
Metric(p, vm.tr("stats_pct_label"), vm.fmt("stats_pct_fmt", pct), Modifier.weight(1f))
|
||||||
Metric(p, vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits), Modifier.weight(1f))
|
Metric(p, vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits), Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
LinearProgressIndicator(pct / 100f, Modifier.fillMaxWidth().height(8.dp).clip(CircleShape), p.accent, p.surface1, StrokeCap.Round)
|
ProgressBar(pct / 100f, p, 8.dp)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(vm.tr("stats_section"), color = p.subtext, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
Text(vm.tr("stats_section"), color = p.subtext, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
@ -258,17 +264,31 @@ fun StatsScreen(vm: AppViewModel) {
|
||||||
)
|
)
|
||||||
if (open && part.totalEx > 0) {
|
if (open && part.totalEx > 0) {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
LinearProgressIndicator(
|
ProgressBar(part.doneEx / part.totalEx.toFloat(), p, 6.dp)
|
||||||
part.doneEx / part.totalEx.toFloat(),
|
}
|
||||||
Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
}
|
||||||
p.accent, p.surface1, StrokeCap.Round,
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProgressBar(fraction: Float, p: org.maturita.maturita.data.Palette, barHeight: androidx.compose.ui.unit.Dp) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(barHeight)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(p.surface1),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(fraction.coerceIn(0f, 1f))
|
||||||
|
.background(p.accent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -342,7 +362,7 @@ fun SearchOverlay(vm: AppViewModel) {
|
||||||
if (hits.isEmpty()) {
|
if (hits.isEmpty()) {
|
||||||
Text(if (q.isEmpty()) vm.tr("search_hint") else vm.tr("search_empty"), color = p.subtext)
|
Text(if (q.isEmpty()) vm.tr("search_hint") else vm.tr("search_empty"), color = p.subtext)
|
||||||
} else {
|
} else {
|
||||||
Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(Modifier.weight(1f).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
hits.forEach { hit ->
|
hits.forEach { hit ->
|
||||||
CardBox(p, Modifier.fillMaxWidth().clickable {
|
CardBox(p, Modifier.fillMaxWidth().clickable {
|
||||||
if (!hit.locked) {
|
if (!hit.locked) {
|
||||||
|
|
@ -409,6 +429,7 @@ fun RoadmapScreen(vm: AppViewModel) {
|
||||||
Column(Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
Column(Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
||||||
PageTop(vm, { vm.back() }, vm.tr("roadmap_title"), vm.tr("roadmap_sub"))
|
PageTop(vm, { vm.back() }, vm.tr("roadmap_title"), vm.tr("roadmap_sub"))
|
||||||
PathMap(
|
PathMap(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
nodes = vm.content.german.mapIndexed { i, u ->
|
nodes = vm.content.german.mapIndexed { i, u ->
|
||||||
val unlocked = u.bool("unlocked")
|
val unlocked = u.bool("unlocked")
|
||||||
val names = u.strs("names")
|
val names = u.strs("names")
|
||||||
|
|
@ -438,6 +459,7 @@ fun UnitMapScreen(vm: AppViewModel, unitId: Int) {
|
||||||
PageTop(vm, { vm.back() }, u.str("title"), vm.tr(u.str("sub")))
|
PageTop(vm, { vm.back() }, u.str("title"), vm.tr(u.str("sub")))
|
||||||
val next = names.indices.firstOrNull { !vm.progress.germanDone(unitId, it + 1) }
|
val next = names.indices.firstOrNull { !vm.progress.germanDone(unitId, it + 1) }
|
||||||
PathMap(
|
PathMap(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
nodes = names.mapIndexed { i, name ->
|
nodes = names.mapIndexed { i, name ->
|
||||||
val n = i + 1
|
val n = i + 1
|
||||||
MapNode(
|
MapNode(
|
||||||
|
|
@ -503,7 +525,7 @@ fun LessonMapScreen(
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
Column(Modifier.fillMaxSize().padding(horizontal = 12.dp)) {
|
||||||
PageTop(vm, { vm.back() }, title, sub)
|
PageTop(vm, { vm.back() }, title, sub)
|
||||||
PathMap(nodes, vm.palette, litUntil, nodeSize = 64f, spac = 150f, mx = 70f)
|
PathMap(nodes, vm.palette, litUntil, modifier = Modifier.weight(1f), nodeSize = 64f, spac = 150f, mx = 70f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ class Updater(private val app: Application) {
|
||||||
val dir = File(app.cacheDir, "updates").apply { mkdirs() }
|
val dir = File(app.cacheDir, "updates").apply { mkdirs() }
|
||||||
val apk = File(dir, "maturita.apk")
|
val apk = File(dir, "maturita.apk")
|
||||||
download(url, apk)
|
download(url, apk)
|
||||||
|
if (!isApk(apk)) {
|
||||||
|
apk.delete()
|
||||||
|
emit(cb, UpdateState("error", messageKey = "update_err_download", messageArg = null))
|
||||||
|
return@execute
|
||||||
|
}
|
||||||
val uri = FileProvider.getUriForFile(app, "${app.packageName}.files", apk)
|
val uri = FileProvider.getUriForFile(app, "${app.packageName}.files", apk)
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||||
|
|
@ -79,6 +84,15 @@ class Updater(private val app: Application) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isApk(file: File): Boolean {
|
||||||
|
if (file.length() < 1_000_000L) return false
|
||||||
|
file.inputStream().use { input ->
|
||||||
|
val header = ByteArray(2)
|
||||||
|
if (input.read(header) != 2) return false
|
||||||
|
return header[0] == 'P'.code.toByte() && header[1] == 'K'.code.toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun get(url: String): String {
|
private fun get(url: String): String {
|
||||||
val c = (URL(url).openConnection() as HttpURLConnection).apply {
|
val c = (URL(url).openConnection() as HttpURLConnection).apply {
|
||||||
connectTimeout = 15000
|
connectTimeout = 15000
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue