mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Build all apps from Markdown content; Compose Desktop replaces GTK on Linux and Windows
- content/ + scripts/build-content.py produce content.json for every app - stable lesson IDs with one-time progress migration (Swift, Kotlin) - shared answer-check tests (tests/answers.json) for Kotlin and Swift - android/desktop: Compose app with updater, speech and GTK progress import - CI packages AppImage and Windows zip with ProGuard, runs the tests Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b324496334
commit
c3bc32113b
964 changed files with 57285 additions and 1156 deletions
|
|
@ -66,6 +66,9 @@ android {
|
|||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
// UI, data and navigation shared with the desktop app (../desktop).
|
||||
sourceSets.getByName("main").kotlin.directories.add("../shared/src")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
|
@ -88,13 +91,14 @@ dependencies {
|
|||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
}
|
||||
|
||||
val extractContent = tasks.register<Exec>("extractContent") {
|
||||
val buildContent = tasks.register<Exec>("buildContent") {
|
||||
workingDir = rootProject.projectDir.parentFile
|
||||
commandLine("python3", "scripts/extract-android-content.py")
|
||||
inputs.dir(rootProject.projectDir.parentFile.resolve("src"))
|
||||
commandLine("python3", "scripts/build-content.py")
|
||||
inputs.dir(rootProject.projectDir.parentFile.resolve("content"))
|
||||
inputs.file(rootProject.projectDir.parentFile.resolve("scripts/build-content.py"))
|
||||
outputs.file(file("src/main/assets/content.json"))
|
||||
}
|
||||
|
||||
tasks.named("preBuild").configure {
|
||||
dependsOn(extractContent)
|
||||
dependsOn(buildContent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package org.maturita.maturita
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import org.maturita.maturita.platform.Platform
|
||||
import org.maturita.maturita.platform.Prefs
|
||||
import org.maturita.maturita.platform.Speaker
|
||||
import org.maturita.maturita.update.Updater
|
||||
import java.util.Locale
|
||||
|
||||
class AndroidPlatform(private val app: Application) : Platform {
|
||||
override val commit: String = BuildConfig.COMMIT
|
||||
override val versionName: String = BuildConfig.VERSION_NAME
|
||||
override val welcomeKey = "welcome_body_android"
|
||||
override val prefs: Prefs = AndroidPrefs(app.getSharedPreferences("maturita", Context.MODE_PRIVATE))
|
||||
override val updater = Updater(app)
|
||||
|
||||
override fun speaker(): Speaker = AndroidSpeaker(app)
|
||||
}
|
||||
|
||||
private class AndroidPrefs(private val sp: SharedPreferences) : Prefs {
|
||||
override fun getBoolean(key: String, fallback: Boolean) = sp.getBoolean(key, fallback)
|
||||
override fun getInt(key: String, fallback: Int) = sp.getInt(key, fallback)
|
||||
override fun getString(key: String, fallback: String?): String? = sp.getString(key, fallback)
|
||||
override fun edit(): Prefs.Editor = Editor(sp.edit())
|
||||
|
||||
private class Editor(private val e: SharedPreferences.Editor) : Prefs.Editor {
|
||||
override fun putBoolean(key: String, value: Boolean) = apply { e.putBoolean(key, value) }
|
||||
override fun putInt(key: String, value: Int) = apply { e.putInt(key, value) }
|
||||
override fun putString(key: String, value: String) = apply { e.putString(key, value) }
|
||||
override fun apply() = e.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private class AndroidSpeaker(context: Context) : Speaker {
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private var ready = false
|
||||
private val tts: TextToSpeech = TextToSpeech(context) { status ->
|
||||
ready = status == TextToSpeech.SUCCESS
|
||||
}
|
||||
|
||||
override fun speak(
|
||||
text: String,
|
||||
locale: Locale,
|
||||
rate: Float,
|
||||
volume: Float,
|
||||
onRange: (Int) -> Unit,
|
||||
onEnd: (Boolean) -> Unit,
|
||||
) {
|
||||
if (!ready) {
|
||||
main.post { onEnd(false) }
|
||||
return
|
||||
}
|
||||
val id = "en-${System.nanoTime()}"
|
||||
tts.language = locale
|
||||
tts.setSpeechRate(rate)
|
||||
tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) {}
|
||||
override fun onRangeStart(utteranceId: String?, rangeStart: Int, rangeEnd: Int, frame: Int) {
|
||||
if (utteranceId == id) main.post { onRange(rangeStart) }
|
||||
}
|
||||
override fun onDone(utteranceId: String?) {
|
||||
if (utteranceId == id) main.post { onEnd(true) }
|
||||
}
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onError(utteranceId: String?) {
|
||||
if (utteranceId == id) main.post { onEnd(false) }
|
||||
}
|
||||
})
|
||||
val params = Bundle().apply { putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, volume) }
|
||||
tts.speak(text, TextToSpeech.QUEUE_FLUSH, params, id)
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
tts.stop()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
tts.stop()
|
||||
tts.shutdown()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,7 @@
|
|||
package org.maturita.maturita
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import java.util.Locale
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import android.util.TypedValue
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
|
|
@ -21,60 +13,19 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.maturita.maturita.data.ColorMode
|
||||
import org.maturita.maturita.data.Palette
|
||||
import org.maturita.maturita.ui.BookListScreen
|
||||
import org.maturita.maturita.ui.BookScreen
|
||||
import org.maturita.maturita.ui.CzechMapScreen
|
||||
import org.maturita.maturita.ui.GermanExercise
|
||||
import org.maturita.maturita.ui.HomeScreen
|
||||
import org.maturita.maturita.ui.HwQuizScreen
|
||||
import org.maturita.maturita.ui.HwYearsScreen
|
||||
import org.maturita.maturita.ui.LessonListScreen
|
||||
import org.maturita.maturita.ui.FyzQuizScreen
|
||||
import org.maturita.maturita.ui.MathPracticeScreen
|
||||
import org.maturita.maturita.ui.MatYearsScreen
|
||||
import org.maturita.maturita.ui.FyzYearsScreen
|
||||
import org.maturita.maturita.ui.LitYearsScreen
|
||||
import org.maturita.maturita.ui.LessonRow
|
||||
import org.maturita.maturita.ui.LitQuizScreen
|
||||
import org.maturita.maturita.ui.SciMapScreen
|
||||
import org.maturita.maturita.ui.SciQuizScreen
|
||||
import org.maturita.maturita.ui.MluvExercise
|
||||
import org.maturita.maturita.ui.NetQuizScreen
|
||||
import org.maturita.maturita.ui.NetYearsScreen
|
||||
import org.maturita.maturita.ui.On2QuizScreen
|
||||
import org.maturita.maturita.ui.On3QuizScreen
|
||||
import org.maturita.maturita.ui.On4QuizScreen
|
||||
import org.maturita.maturita.ui.OnQuizScreen
|
||||
import org.maturita.maturita.ui.OnYearsScreen
|
||||
import org.maturita.maturita.ui.EnYearsScreen
|
||||
import org.maturita.maturita.ui.ChoiceLessonScreen
|
||||
import org.maturita.maturita.ui.DeHomeScreen
|
||||
import org.maturita.maturita.ui.ItYearScreen
|
||||
import org.maturita.maturita.ui.EngExScreen
|
||||
import org.maturita.maturita.ui.PlotScreen
|
||||
import org.maturita.maturita.ui.RoadmapScreen
|
||||
import org.maturita.maturita.ui.SearchScreen
|
||||
import org.maturita.maturita.ui.SettingsScreen
|
||||
import org.maturita.maturita.ui.SlidesScreen
|
||||
import org.maturita.maturita.ui.StatsScreen
|
||||
import org.maturita.maturita.ui.UnitMapScreen
|
||||
import org.maturita.maturita.ui.VocabExercise
|
||||
import org.maturita.maturita.ui.appTypography
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import org.maturita.maturita.data.Content
|
||||
|
||||
/** Keeps the shared app state alive across rotation and other config changes. */
|
||||
class AppHolder(app: Application) : AndroidViewModel(app) {
|
||||
val vm = AppViewModel(
|
||||
Content.parse(runCatching { app.assets.open("content.json").bufferedReader().use { it.readText() } }.getOrNull()),
|
||||
AndroidPlatform(app),
|
||||
)
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val vm: AppViewModel by viewModels()
|
||||
private val holder: AppHolder by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
@ -96,7 +47,11 @@ class MainActivity : ComponentActivity() {
|
|||
return
|
||||
}
|
||||
enableEdgeToEdge()
|
||||
setContent { MaturitaApp(vm) }
|
||||
val vm = holder.vm
|
||||
setContent {
|
||||
BackHandler(vm.canGoBack || vm.searchOpen) { vm.handleBack() }
|
||||
MaturitaApp(vm)
|
||||
}
|
||||
}
|
||||
|
||||
private fun crashView(dump: String, retry: () -> Unit): LinearLayout {
|
||||
|
|
@ -135,512 +90,3 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Material 3 colour roles from the shared theme palette. */
|
||||
private fun schemeFor(p: Palette, dark: Boolean) = if (dark) {
|
||||
darkColorScheme(
|
||||
primary = p.tint,
|
||||
onPrimary = p.onTint,
|
||||
primaryContainer = p.accent,
|
||||
onPrimaryContainer = p.onAccent,
|
||||
secondary = p.accent2,
|
||||
onSecondary = p.onAccent,
|
||||
secondaryContainer = p.surface1,
|
||||
onSecondaryContainer = p.text,
|
||||
tertiary = p.warning,
|
||||
onTertiary = p.crust,
|
||||
background = p.base,
|
||||
onBackground = p.text,
|
||||
surface = p.base,
|
||||
onSurface = p.text,
|
||||
surfaceVariant = p.surface0,
|
||||
onSurfaceVariant = p.subtext,
|
||||
surfaceContainerLowest = p.crust,
|
||||
surfaceContainerLow = p.mantle,
|
||||
surfaceContainer = p.mantle,
|
||||
surfaceContainerHigh = p.surface0,
|
||||
surfaceContainerHighest = p.surface1,
|
||||
outline = p.overlay,
|
||||
outlineVariant = p.surface1,
|
||||
error = p.error,
|
||||
onError = Color.White,
|
||||
errorContainer = p.error.copy(alpha = 0.22f),
|
||||
onErrorContainer = p.text,
|
||||
)
|
||||
} else {
|
||||
lightColorScheme(
|
||||
primary = p.tint,
|
||||
onPrimary = p.onTint,
|
||||
primaryContainer = p.accent,
|
||||
onPrimaryContainer = p.onAccent,
|
||||
secondary = p.accent2,
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = p.surface1,
|
||||
onSecondaryContainer = p.text,
|
||||
tertiary = p.warning,
|
||||
onTertiary = Color.White,
|
||||
background = p.base,
|
||||
onBackground = p.text,
|
||||
surface = p.base,
|
||||
onSurface = p.text,
|
||||
surfaceVariant = p.surface1,
|
||||
onSurfaceVariant = p.subtext,
|
||||
surfaceContainerLowest = p.mantle,
|
||||
surfaceContainerLow = p.mantle,
|
||||
surfaceContainer = p.mantle,
|
||||
surfaceContainerHigh = p.surface1,
|
||||
surfaceContainerHighest = p.surface2,
|
||||
outline = p.overlay,
|
||||
outlineVariant = p.surface2,
|
||||
error = p.error,
|
||||
onError = Color.White,
|
||||
errorContainer = p.error.copy(alpha = 0.14f),
|
||||
onErrorContainer = p.text,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaturitaApp(vm: AppViewModel) {
|
||||
MaterialTheme(colorScheme = schemeFor(vm.palette, vm.mode == ColorMode.Dark), typography = appTypography()) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
MaturitaAppBody(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MaturitaAppBody(vm: AppViewModel) {
|
||||
LaunchedEffect(Unit) { vm.checkUpdate(false) }
|
||||
BackHandler(vm.canGoBack || vm.searchOpen) {
|
||||
if (vm.searchOpen) vm.searchOpen = false else vm.back()
|
||||
}
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
AnimatedContent(
|
||||
targetState = vm.route,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
transitionSpec = {
|
||||
val tabs = targetState.isRoot && initialState.isRoot
|
||||
if (tabs) {
|
||||
fadeIn(tween(180)) togetherWith fadeOut(tween(140))
|
||||
} else if (vm.navigatingForward) {
|
||||
(fadeIn(tween(220)) + slideInHorizontally(tween(220)) { it / 5 })
|
||||
.togetherWith(fadeOut(tween(150)) + slideOutHorizontally(tween(150)) { -it / 8 })
|
||||
} else {
|
||||
(fadeIn(tween(220)) + slideInHorizontally(tween(220)) { -it / 8 })
|
||||
.togetherWith(fadeOut(tween(150)) + slideOutHorizontally(tween(150)) { it / 5 })
|
||||
}
|
||||
},
|
||||
label = "screen",
|
||||
) { route ->
|
||||
when (val r = route) {
|
||||
Route.Welcome, Route.Subjects -> HomeScreen(vm)
|
||||
Route.Stats -> StatsScreen(vm)
|
||||
Route.Settings -> SettingsScreen(vm)
|
||||
Route.DeHome -> DeHomeScreen(vm)
|
||||
is Route.DeMap -> LessonListScreen(
|
||||
vm, vm.tr("de_year${r.year}"), vm.tr("de_y${r.year}_sub"),
|
||||
vm.content.deYear(r.year).map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.deDone(r.year, id), Route.DeLesson(r.year, id))
|
||||
},
|
||||
)
|
||||
is Route.DeLesson -> {
|
||||
val l = vm.content.deLesson(r.year, r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.DeEx(r.year, r.id))
|
||||
}
|
||||
}
|
||||
is Route.DeEx -> EngExScreen(
|
||||
vm, r.year, r.id, Locale.GERMANY, vm.content.deLesson(r.year, r.id),
|
||||
) { vm.markDe(r.year, r.id) }
|
||||
Route.Roadmap -> RoadmapScreen(vm)
|
||||
is Route.UnitMap -> UnitMapScreen(vm, r.unitId)
|
||||
is Route.GermanEx -> GermanExercise(vm, r.unitId, r.ex)
|
||||
is Route.Vocab -> VocabExercise(vm, r.unitId)
|
||||
Route.NetYears -> NetYearsScreen(vm)
|
||||
Route.NetMap -> LessonListScreen(
|
||||
vm, vm.tr("net_year1"), vm.tr("net_sub"),
|
||||
vm.content.netLessons.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.netDone(id), Route.NetLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.NetLesson -> {
|
||||
val l = vm.content.netLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.NetEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.NetEx -> NetQuizScreen(vm, r.id)
|
||||
Route.Net2Map -> ItYearScreen(vm, "net2", "net_year2", "net2_sub") { Route.Net2Lesson(it) }
|
||||
is Route.Net2Lesson -> {
|
||||
val l = vm.content.itLesson("net2", r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Net2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Net2Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net2", r.id)) { vm.markCourse("net2", r.id) }
|
||||
Route.Net3Map -> ItYearScreen(vm, "net3", "net_year3", "net3_sub") { Route.Net3Lesson(it) }
|
||||
is Route.Net3Lesson -> {
|
||||
val l = vm.content.itLesson("net3", r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Net3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Net3Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net3", r.id)) { vm.markCourse("net3", r.id) }
|
||||
Route.Net4Map -> ItYearScreen(vm, "net4", "net_year4", "net4_sub") { Route.Net4Lesson(it) }
|
||||
is Route.Net4Lesson -> {
|
||||
val l = vm.content.itLesson("net4", r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Net4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Net4Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("net4", r.id)) { vm.markCourse("net4", r.id) }
|
||||
Route.HwYears -> HwYearsScreen(vm)
|
||||
Route.HwMap -> LessonListScreen(
|
||||
vm, vm.tr("hw_year1"), vm.tr("hw_sub"),
|
||||
vm.content.hw.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.hwDone(id), Route.HwLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.HwLesson -> {
|
||||
val l = vm.content.hwLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.HwEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.HwEx -> HwQuizScreen(vm, r.id)
|
||||
Route.Hw2Map -> ItYearScreen(vm, "hw2", "hw_year2", "hw2_sub") { Route.Hw2Lesson(it) }
|
||||
is Route.Hw2Lesson -> {
|
||||
val l = vm.content.itLesson("hw2", r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Hw2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Hw2Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw2", r.id)) { vm.markCourse("hw2", r.id) }
|
||||
Route.Hw3Map -> ItYearScreen(vm, "hw3", "hw_year3", "hw3_sub") { Route.Hw3Lesson(it) }
|
||||
is Route.Hw3Lesson -> {
|
||||
val l = vm.content.itLesson("hw3", r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Hw3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Hw3Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw3", r.id)) { vm.markCourse("hw3", r.id) }
|
||||
Route.Hw4Map -> ItYearScreen(vm, "hw4", "hw_year4", "hw4_sub") { Route.Hw4Lesson(it) }
|
||||
is Route.Hw4Lesson -> {
|
||||
val l = vm.content.itLesson("hw4", r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Hw4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Hw4Ex -> ChoiceLessonScreen(vm, vm.content.itLesson("hw4", r.id)) { vm.markCourse("hw4", r.id) }
|
||||
Route.EnYears -> EnYearsScreen(vm)
|
||||
is Route.EnMap -> LessonListScreen(
|
||||
vm, vm.tr("en_year${r.year}"), vm.tr("en_y${r.year}_sub"),
|
||||
vm.content.enYear(r.year).map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.enDone(r.year, id), Route.EnLesson(r.year, id))
|
||||
},
|
||||
)
|
||||
is Route.EnLesson -> {
|
||||
val l = vm.content.enLesson(r.year, r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.EnEx(r.year, r.id))
|
||||
}
|
||||
}
|
||||
is Route.EnEx -> EngExScreen(vm, r.year, r.id)
|
||||
Route.OnYears -> OnYearsScreen(vm)
|
||||
Route.OnMap -> LessonListScreen(
|
||||
vm, vm.tr("on_year1"), vm.tr("on_sub"),
|
||||
vm.content.on.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.onDone(id), Route.OnLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.OnLesson -> {
|
||||
val l = vm.content.onLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.OnEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.OnEx -> OnQuizScreen(vm, r.id)
|
||||
Route.On2Map -> LessonListScreen(
|
||||
vm, vm.tr("on_year2"), vm.tr("on2_sub"),
|
||||
vm.content.on2.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.on2Done(id), Route.On2Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.On2Lesson -> {
|
||||
val l = vm.content.on2Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.On2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.On2Ex -> On2QuizScreen(vm, r.id)
|
||||
Route.On3Map -> LessonListScreen(
|
||||
vm, vm.tr("on_year3"), vm.tr("on3_sub"),
|
||||
vm.content.on3.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.on3Done(id), Route.On3Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.On3Lesson -> {
|
||||
val l = vm.content.on3Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.On3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.On3Ex -> On3QuizScreen(vm, r.id)
|
||||
Route.On4Map -> LessonListScreen(
|
||||
vm, vm.tr("on_year4"), vm.tr("on4_sub"),
|
||||
vm.content.on4.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.on4Done(id), Route.On4Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.On4Lesson -> {
|
||||
val l = vm.content.on4Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.On4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.On4Ex -> On4QuizScreen(vm, r.id)
|
||||
Route.LitYears -> LitYearsScreen(vm)
|
||||
Route.LitMap -> LessonListScreen(
|
||||
vm, vm.tr("lit_year1"), vm.tr("lit_sub"),
|
||||
vm.content.lit.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.litDone(id), Route.LitLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.LitLesson -> {
|
||||
val l = vm.content.litLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.LitEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.LitEx -> LitQuizScreen(vm, 1, r.id)
|
||||
Route.Lit2Map -> LessonListScreen(
|
||||
vm, vm.tr("lit_year2"), vm.tr("lit2_sub"),
|
||||
vm.content.lit2.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.lit2Done(id), Route.Lit2Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Lit2Lesson -> {
|
||||
val l = vm.content.lit2Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Lit2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Lit2Ex -> LitQuizScreen(vm, 2, r.id)
|
||||
Route.Lit3Map -> LessonListScreen(
|
||||
vm, vm.tr("lit_year3"), vm.tr("lit3_sub"),
|
||||
vm.content.lit3.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.lit3Done(id), Route.Lit3Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Lit3Lesson -> {
|
||||
val l = vm.content.lit3Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Lit3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Lit3Ex -> LitQuizScreen(vm, 3, r.id)
|
||||
Route.Lit4Map -> LessonListScreen(
|
||||
vm, vm.tr("lit_year4"), vm.tr("lit4_sub"),
|
||||
vm.content.lit4.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.lit4Done(id), Route.Lit4Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Lit4Lesson -> {
|
||||
val l = vm.content.lit4Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Lit4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Lit4Ex -> LitQuizScreen(vm, 4, r.id)
|
||||
Route.MatYears -> MatYearsScreen(vm)
|
||||
Route.Mat0Map -> LessonListScreen(
|
||||
vm, vm.tr("mat0_year"), vm.tr("mat0_sub"),
|
||||
vm.content.mat0.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat0Done(id), Route.Mat0Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat0Lesson -> {
|
||||
val l = vm.content.mat0Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat0Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat0Ex -> MathPracticeScreen(vm, 0, r.id)
|
||||
Route.MatMap -> LessonListScreen(
|
||||
vm, vm.tr("mat_year1"), vm.tr("mat_sub"),
|
||||
vm.content.mat.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.matDone(id), Route.MatLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.MatLesson -> {
|
||||
val l = vm.content.matLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.MatEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.MatEx -> MathPracticeScreen(vm, 1, r.id)
|
||||
Route.Mat2Map -> LessonListScreen(
|
||||
vm, vm.tr("mat_year2"), vm.tr("mat2_sub"),
|
||||
vm.content.mat2.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat2Done(id), Route.Mat2Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat2Lesson -> {
|
||||
val l = vm.content.mat2Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat2Ex -> MathPracticeScreen(vm, 2, r.id)
|
||||
Route.Mat3Map -> LessonListScreen(
|
||||
vm, vm.tr("mat_year3"), vm.tr("mat3_sub"),
|
||||
vm.content.mat3.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat3Done(id), Route.Mat3Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat3Lesson -> {
|
||||
val l = vm.content.mat3Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat3Ex -> MathPracticeScreen(vm, 3, r.id)
|
||||
Route.Mat4Map -> LessonListScreen(
|
||||
vm, vm.tr("mat_year4"), vm.tr("mat4_sub"),
|
||||
vm.content.mat4.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.mat4Done(id), Route.Mat4Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Mat4Lesson -> {
|
||||
val l = vm.content.mat4Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Mat4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Mat4Ex -> MathPracticeScreen(vm, 4, r.id)
|
||||
Route.FyzYears -> FyzYearsScreen(vm)
|
||||
Route.FyzMap -> LessonListScreen(
|
||||
vm, vm.tr("fyz_year1"), vm.tr("fyz_sub"),
|
||||
vm.content.fyz.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.fyzDone(id), Route.FyzLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.FyzLesson -> {
|
||||
val l = vm.content.fyzLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.FyzEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.FyzEx -> FyzQuizScreen(vm, 1, r.id)
|
||||
Route.Fyz2Map -> LessonListScreen(
|
||||
vm, vm.tr("fyz_year2"), vm.tr("fyz2_sub"),
|
||||
vm.content.fyz2.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.fyz2Done(id), Route.Fyz2Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Fyz2Lesson -> {
|
||||
val l = vm.content.fyz2Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Fyz2Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Fyz2Ex -> FyzQuizScreen(vm, 2, r.id)
|
||||
Route.Fyz3Map -> LessonListScreen(
|
||||
vm, vm.tr("fyz_year3"), vm.tr("fyz3_sub"),
|
||||
vm.content.fyz3.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.fyz3Done(id), Route.Fyz3Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Fyz3Lesson -> {
|
||||
val l = vm.content.fyz3Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Fyz3Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Fyz3Ex -> FyzQuizScreen(vm, 3, r.id)
|
||||
Route.Fyz4Map -> LessonListScreen(
|
||||
vm, vm.tr("fyz_year4"), vm.tr("fyz4_sub"),
|
||||
vm.content.fyz4.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.fyz4Done(id), Route.Fyz4Lesson(id))
|
||||
},
|
||||
)
|
||||
is Route.Fyz4Lesson -> {
|
||||
val l = vm.content.fyz4Lesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.Fyz4Ex(r.id))
|
||||
}
|
||||
}
|
||||
is Route.Fyz4Ex -> FyzQuizScreen(vm, 4, r.id)
|
||||
Route.SciMap -> SciMapScreen(vm)
|
||||
Route.ChemMap -> LessonListScreen(
|
||||
vm, vm.tr("Chemie"), vm.tr("chem_sub"),
|
||||
vm.content.chem.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.chemDone(id), Route.ChemLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.ChemLesson -> {
|
||||
val l = vm.content.chemLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.ChemEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.ChemEx -> SciQuizScreen(vm, "chem", r.id)
|
||||
Route.BioMap -> LessonListScreen(
|
||||
vm, vm.tr("Biologie"), vm.tr("bio_sub"),
|
||||
vm.content.bio.map { l ->
|
||||
val id = l.int("id")
|
||||
LessonRow(vm.tr(l.str("titleKey")), vm.progress.bioDone(id), Route.BioLesson(id))
|
||||
},
|
||||
)
|
||||
is Route.BioLesson -> {
|
||||
val l = vm.content.bioLesson(r.id)
|
||||
if (l != null) SlidesScreen(vm, vm.tr(l.str("titleKey")), vm.tr(l.str("subKey")), l.arr("slides")) {
|
||||
vm.replace(Route.BioEx(r.id))
|
||||
}
|
||||
}
|
||||
is Route.BioEx -> SciQuizScreen(vm, "bio", r.id)
|
||||
Route.CzechMap -> CzechMapScreen(vm)
|
||||
Route.Mluvnice -> LessonListScreen(
|
||||
vm, vm.tr("Mluvnice"), vm.tr("mluv_sub"),
|
||||
vm.content.mluvnice.map { m ->
|
||||
val n = m.int("id")
|
||||
LessonRow(m.str("name"), vm.progress.mluvDone(n), Route.MluvEx(n))
|
||||
},
|
||||
)
|
||||
is Route.MluvEx -> MluvExercise(vm, r.n)
|
||||
Route.ReadingList -> BookListScreen(vm)
|
||||
is Route.Book -> BookScreen(vm, r.id)
|
||||
is Route.BookQuiz -> LitQuizScreen(vm, r.id)
|
||||
is Route.BookPlot -> PlotScreen(vm, r.id)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = vm.searchOpen,
|
||||
enter = fadeIn(tween(160)),
|
||||
exit = fadeOut(tween(120)),
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) { SearchScreen(vm) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,26 +9,20 @@ import android.os.Looper
|
|||
import android.provider.Settings
|
||||
import androidx.core.content.FileProvider
|
||||
import org.maturita.maturita.BuildConfig
|
||||
import org.maturita.maturita.platform.AppUpdater
|
||||
import org.maturita.maturita.platform.UpdateState
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
data class UpdateState(
|
||||
val status: String = "idle",
|
||||
val remote: String? = null,
|
||||
val messageKey: String = "update_current",
|
||||
val messageArg: String? = BuildConfig.COMMIT.take(7),
|
||||
val canInstall: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Self-update from the `builds` branch. CI rewrites that branch on every
|
||||
* push to main with a commit titled "Build from <sha>"; the APK there is
|
||||
* signed with the repository's sideload key, so Android accepts it as an
|
||||
* update over any earlier build signed the same way.
|
||||
*/
|
||||
class Updater(private val app: Application) {
|
||||
class Updater(private val app: Application) : AppUpdater {
|
||||
private val io = Executors.newSingleThreadExecutor()
|
||||
private val main = Handler(Looper.getMainLooper())
|
||||
private val repo = BuildConfig.UPDATE_REPO
|
||||
|
|
@ -38,7 +32,7 @@ class Updater(private val app: Application) {
|
|||
main.post { cb(state) }
|
||||
}
|
||||
|
||||
fun check(interactive: Boolean, cb: (UpdateState) -> Unit) {
|
||||
override fun check(interactive: Boolean, cb: (UpdateState) -> Unit) {
|
||||
if (BuildConfig.COMMIT == "dev") {
|
||||
emit(cb, UpdateState(status = "dev", messageKey = "update_err_devbuild", messageArg = null))
|
||||
return
|
||||
|
|
@ -63,7 +57,7 @@ class Updater(private val app: Application) {
|
|||
}
|
||||
}
|
||||
|
||||
fun install(cb: (UpdateState) -> Unit) {
|
||||
override fun install(cb: (UpdateState) -> Unit) {
|
||||
// Android 8+ needs a per-app opt-in before we may hand an APK to the
|
||||
// package installer. Send the user there and keep the Update button
|
||||
// so the second tap goes straight to the download.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue