mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Add short transitions for navigation, answers and progress on every platform.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
27a845c1d6
commit
59d40c886a
32 changed files with 359 additions and 121 deletions
|
|
@ -34,6 +34,9 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
private set
|
private set
|
||||||
var showChangelog by mutableStateOf(shouldShowChangelog())
|
var showChangelog by mutableStateOf(shouldShowChangelog())
|
||||||
private set
|
private set
|
||||||
|
/** True when the latest navigation moved deeper; false for back and tab switches. */
|
||||||
|
var navigatingForward by mutableStateOf(true)
|
||||||
|
private set
|
||||||
|
|
||||||
private val stack = mutableStateListOf<Route>(Route.Subjects)
|
private val stack = mutableStateListOf<Route>(Route.Subjects)
|
||||||
val route: Route get() = stack.last()
|
val route: Route get() = stack.last()
|
||||||
|
|
@ -50,11 +53,13 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
searchOpen = false
|
searchOpen = false
|
||||||
if (stack.last() == r) return
|
if (stack.last() == r) return
|
||||||
if (r.isRoot) {
|
if (r.isRoot) {
|
||||||
|
navigatingForward = false
|
||||||
val target = if (r == Route.Welcome) Route.Subjects else r
|
val target = if (r == Route.Welcome) Route.Subjects else r
|
||||||
stack.clear()
|
stack.clear()
|
||||||
stack.add(target)
|
stack.add(target)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
navigatingForward = true
|
||||||
if (root != Route.Subjects) {
|
if (root != Route.Subjects) {
|
||||||
stack.clear()
|
stack.clear()
|
||||||
stack.addAll(r.stack())
|
stack.addAll(r.stack())
|
||||||
|
|
@ -72,17 +77,20 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
||||||
fun openPage(page: String) {
|
fun openPage(page: String) {
|
||||||
val r = routeFromPage(page) ?: return
|
val r = routeFromPage(page) ?: return
|
||||||
searchOpen = false
|
searchOpen = false
|
||||||
|
navigatingForward = true
|
||||||
stack.clear()
|
stack.clear()
|
||||||
stack.addAll(r.stack())
|
stack.addAll(r.stack())
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Replace the current screen (lesson slides -> its exercise). */
|
/** Replace the current screen (lesson slides -> its exercise). */
|
||||||
fun replace(r: Route) {
|
fun replace(r: Route) {
|
||||||
|
navigatingForward = true
|
||||||
if (stack.size > 1) stack.removeAt(stack.lastIndex)
|
if (stack.size > 1) stack.removeAt(stack.lastIndex)
|
||||||
stack.add(r)
|
stack.add(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun back() {
|
fun back() {
|
||||||
|
navigatingForward = false
|
||||||
when {
|
when {
|
||||||
stack.size > 1 -> stack.removeAt(stack.lastIndex)
|
stack.size > 1 -> stack.removeAt(stack.lastIndex)
|
||||||
root != Route.Subjects -> go(Route.Subjects)
|
root != Route.Subjects -> go(Route.Subjects)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@ package org.maturita.maturita
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import java.util.Locale
|
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.util.TypedValue
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
|
|
@ -206,7 +214,24 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
||||||
if (vm.searchOpen) vm.searchOpen = false else vm.back()
|
if (vm.searchOpen) vm.searchOpen = false else vm.back()
|
||||||
}
|
}
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
when (val r = vm.route) {
|
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.Welcome, Route.Subjects -> HomeScreen(vm)
|
||||||
Route.Stats -> StatsScreen(vm)
|
Route.Stats -> StatsScreen(vm)
|
||||||
Route.Settings -> SettingsScreen(vm)
|
Route.Settings -> SettingsScreen(vm)
|
||||||
|
|
@ -609,6 +634,13 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
||||||
is Route.BookQuiz -> LitQuizScreen(vm, r.id)
|
is Route.BookQuiz -> LitQuizScreen(vm, r.id)
|
||||||
is Route.BookPlot -> PlotScreen(vm, r.id)
|
is Route.BookPlot -> PlotScreen(vm, r.id)
|
||||||
}
|
}
|
||||||
if (vm.searchOpen) SearchScreen(vm)
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = vm.searchOpen,
|
||||||
|
enter = fadeIn(tween(160)),
|
||||||
|
exit = fadeOut(tween(120)),
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize()) { SearchScreen(vm) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
package org.maturita.maturita.ui
|
package org.maturita.maturita.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -36,6 +44,7 @@ import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -53,9 +62,11 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
|
@ -234,8 +245,12 @@ fun ActionBar(
|
||||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
) {
|
) {
|
||||||
if (feedback.isNotEmpty()) {
|
AnimatedVisibility(
|
||||||
FeedbackLine(feedback, kind)
|
visible = feedback.isNotEmpty(),
|
||||||
|
enter = fadeIn(tween(180)) + expandVertically(),
|
||||||
|
exit = fadeOut(tween(120)) + shrinkVertically(),
|
||||||
|
) {
|
||||||
|
FeedbackLine(feedback, kind, animate = false)
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
|
|
@ -309,8 +324,27 @@ fun PillButton(label: String, modifier: Modifier = Modifier, onClick: () -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FeedbackLine(text: String, kind: String) {
|
fun SmoothProgress(
|
||||||
if (text.isEmpty()) return
|
progress: Float,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
) {
|
||||||
|
val animated by animateFloatAsState(
|
||||||
|
targetValue = progress.coerceIn(0f, 1f),
|
||||||
|
animationSpec = tween(durationMillis = 380),
|
||||||
|
label = "progress",
|
||||||
|
)
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { animated },
|
||||||
|
modifier = modifier,
|
||||||
|
color = color,
|
||||||
|
trackColor = trackColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeedbackLine(text: String, kind: String, animate: Boolean = true) {
|
||||||
val color = when (kind) {
|
val color = when (kind) {
|
||||||
"ok" -> MaterialTheme.colorScheme.primary
|
"ok" -> MaterialTheme.colorScheme.primary
|
||||||
"warn" -> MaterialTheme.colorScheme.tertiary
|
"warn" -> MaterialTheme.colorScheme.tertiary
|
||||||
|
|
@ -321,6 +355,22 @@ fun FeedbackLine(text: String, kind: String) {
|
||||||
"warn" -> Icons.Filled.Warning
|
"warn" -> Icons.Filled.Warning
|
||||||
else -> Icons.Filled.Cancel
|
else -> Icons.Filled.Cancel
|
||||||
}
|
}
|
||||||
|
if (!animate) {
|
||||||
|
if (text.isEmpty()) return
|
||||||
|
FeedbackRow(text, color, icon)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = text.isNotEmpty(),
|
||||||
|
enter = fadeIn(tween(180)) + expandVertically(),
|
||||||
|
exit = fadeOut(tween(120)) + shrinkVertically(),
|
||||||
|
) {
|
||||||
|
FeedbackRow(text, color, icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FeedbackRow(text: String, color: Color, icon: ImageVector) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Icon(icon, null, tint = color, modifier = Modifier.size(20.dp))
|
Icon(icon, null, tint = color, modifier = Modifier.size(20.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
@ -330,7 +380,11 @@ fun FeedbackLine(text: String, kind: String) {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Meaning(text: String, visible: Boolean) {
|
fun Meaning(text: String, visible: Boolean) {
|
||||||
if (!visible || text.isEmpty()) return
|
AnimatedVisibility(
|
||||||
|
visible = visible && text.isNotEmpty(),
|
||||||
|
enter = fadeIn(tween(180)),
|
||||||
|
exit = fadeOut(tween(120)),
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text,
|
text,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -338,6 +392,7 @@ fun Meaning(text: String, visible: Boolean) {
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Hint(text: String) {
|
fun Hint(text: String) {
|
||||||
|
|
@ -397,11 +452,12 @@ fun SegChip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||||
/** Multiple-choice answer with radio state and, after checking, a correct/wrong mark. */
|
/** Multiple-choice answer with radio state and, after checking, a correct/wrong mark. */
|
||||||
@Composable
|
@Composable
|
||||||
fun OptionRow(text: String, selected: Boolean, mark: Boolean?, enabled: Boolean = true, onClick: () -> Unit) {
|
fun OptionRow(text: String, selected: Boolean, mark: Boolean?, enabled: Boolean = true, onClick: () -> Unit) {
|
||||||
val container = when (mark) {
|
val target = when (mark) {
|
||||||
true -> MaterialTheme.colorScheme.primaryContainer
|
true -> MaterialTheme.colorScheme.primaryContainer
|
||||||
false -> MaterialTheme.colorScheme.errorContainer
|
false -> MaterialTheme.colorScheme.errorContainer
|
||||||
null -> if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh
|
null -> if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
}
|
}
|
||||||
|
val container by animateColorAsState(target, tween(180), label = "option")
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
|
@ -479,8 +478,8 @@ private fun VocabEx(vm: AppViewModel, title: String, sub: String?, spec: J, onDo
|
||||||
bottomBar = if (cur != null) ({ ActionBar(vm.tr("check"), fb.first, fb.second) { check() } }) else null,
|
bottomBar = if (cur != null) ({ ActionBar(vm.tr("check"), fb.first, fb.second) { check() } }) else null,
|
||||||
) { inner ->
|
) { inner ->
|
||||||
Column(Modifier.fillMaxSize().padding(inner).verticalScroll(rememberScrollState()).padding(16.dp)) {
|
Column(Modifier.fillMaxSize().padding(inner).verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||||
LinearProgressIndicator(
|
SmoothProgress(
|
||||||
progress = { if (total == 0) 0f else done / total.toFloat() },
|
progress = if (total == 0) 0f else done / total.toFloat(),
|
||||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
|
|
@ -1354,7 +1353,7 @@ fun LitQuizScreen(vm: AppViewModel, id: String) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val q = qs[idx]
|
val q = qs[idx]
|
||||||
LinearProgressIndicator(progress = { idx / qs.size.toFloat() }, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
SmoothProgress(progress = idx / qs.size.toFloat(), modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||||
Text(vm.fmt("lit_question_fmt", idx + 1, qs.size), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(vm.fmt("lit_question_fmt", idx + 1, qs.size), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -116,8 +115,8 @@ fun HomeScreen(vm: AppViewModel) {
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
LinearProgressIndicator(
|
SmoothProgress(
|
||||||
progress = { pct },
|
progress = pct,
|
||||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.18f),
|
trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.18f),
|
||||||
|
|
@ -246,7 +245,7 @@ fun StatsScreen(vm: AppViewModel) {
|
||||||
Metric(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits), Modifier.weight(1f))
|
Metric(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits), Modifier.weight(1f))
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
LinearProgressIndicator(progress = { pct }, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
SmoothProgress(progress = pct, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,8 +261,8 @@ fun StatsScreen(vm: AppViewModel) {
|
||||||
Column {
|
Column {
|
||||||
if (open && part.totalEx > 0) {
|
if (open && part.totalEx > 0) {
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
LinearProgressIndicator(
|
SmoothProgress(
|
||||||
progress = { part.doneEx / part.totalEx.toFloat() },
|
progress = part.doneEx / part.totalEx.toFloat(),
|
||||||
modifier = Modifier.fillMaxWidth().height(4.dp).clip(CircleShape),
|
modifier = Modifier.fillMaxWidth().height(4.dp).clip(CircleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -770,7 +769,6 @@ fun HwYearsScreen(vm: AppViewModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DeHomeScreen(vm: AppViewModel) {
|
fun DeHomeScreen(vm: AppViewModel) {
|
||||||
DetailScaffold(vm, vm.tr("Deutsch"), vm.tr("de_home_sub")) { inner ->
|
DetailScaffold(vm, vm.tr("Deutsch"), vm.tr("de_home_sub")) { inner ->
|
||||||
|
|
@ -808,6 +806,7 @@ fun EnYearsScreen(vm: AppViewModel) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
fun OnYearsScreen(vm: AppViewModel) {
|
fun OnYearsScreen(vm: AppViewModel) {
|
||||||
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
|
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
|
||||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ windowcontrols button:not(:hover) image {
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
|
transition: background-color 120ms ease, color 120ms ease;
|
||||||
}
|
}
|
||||||
tooltip {
|
tooltip {
|
||||||
background-color: @bg_crust;
|
background-color: @bg_crust;
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,12 @@ final class AppModel: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTheme(_ id: ThemeId) {
|
func setTheme(_ id: ThemeId) {
|
||||||
themeId = id
|
withAnimation(Motion.settle) { themeId = id }
|
||||||
progress.themeId = id
|
progress.themeId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyMode(_ m: ColorMode) {
|
func applyMode(_ m: ColorMode) {
|
||||||
mode = m
|
withAnimation(Motion.settle) { mode = m }
|
||||||
progress.mode = m
|
progress.mode = m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -285,7 +285,7 @@ final class AppModel: ObservableObject {
|
||||||
|
|
||||||
func dismissChangelog() {
|
func dismissChangelog() {
|
||||||
progress.seenCommit = AppConfig.commit
|
progress.seenCommit = AppConfig.commit
|
||||||
showChangelog = false
|
withAnimation(Motion.settle) { showChangelog = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,47 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
enum Motion {
|
||||||
|
static let press = Animation.easeOut(duration: 0.16)
|
||||||
|
static let settle = Animation.easeOut(duration: 0.28)
|
||||||
|
static let feedback = Animation.spring(response: 0.32, dampingFraction: 0.82)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slight press-in for custom cards and map nodes. System buttons keep their own style.
|
||||||
|
struct PressScaleStyle: ButtonStyle {
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.scaleEffect(reduceMotion || !configuration.isPressed ? 1 : 0.98)
|
||||||
|
.animation(reduceMotion ? nil : Motion.press, value: configuration.isPressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct SoftAppear: ViewModifier {
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
@State private var shown = false
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.opacity(reduceMotion || shown ? 1 : 0)
|
||||||
|
.offset(y: reduceMotion || shown ? 0 : 8)
|
||||||
|
.onAppear {
|
||||||
|
guard !reduceMotion else {
|
||||||
|
shown = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
withAnimation(Motion.settle) { shown = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
/// One short fade-in. Use on a landing surface, not on every pushed screen.
|
||||||
|
func softAppear() -> some View {
|
||||||
|
modifier(SoftAppear())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
enum MacChrome {
|
enum MacChrome {
|
||||||
static let pageInset: CGFloat = 28
|
static let pageInset: CGFloat = 28
|
||||||
|
|
@ -265,8 +307,10 @@ struct FeedbackLine: View {
|
||||||
let text: String
|
let text: String
|
||||||
let kind: String
|
let kind: String
|
||||||
let palette: Palette
|
let palette: Palette
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Group {
|
||||||
if !text.isEmpty {
|
if !text.isEmpty {
|
||||||
Label {
|
Label {
|
||||||
Text(text)
|
Text(text)
|
||||||
|
|
@ -279,8 +323,12 @@ struct FeedbackLine: View {
|
||||||
.font(.subheadline.weight(.semibold))
|
.font(.subheadline.weight(.semibold))
|
||||||
#endif
|
#endif
|
||||||
.foregroundStyle(kind == "ok" ? palette.success : kind == "warn" ? palette.warning : palette.error)
|
.foregroundStyle(kind == "ok" ? palette.success : kind == "warn" ? palette.warning : palette.error)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .leading)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.animation(reduceMotion ? nil : Motion.feedback, value: text)
|
||||||
|
.animation(reduceMotion ? nil : Motion.feedback, value: kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Meaning: View {
|
struct Meaning: View {
|
||||||
|
|
@ -288,7 +336,10 @@ struct Meaning: View {
|
||||||
let palette: Palette
|
let palette: Palette
|
||||||
let visible: Bool
|
let visible: Bool
|
||||||
|
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Group {
|
||||||
if visible && !text.isEmpty {
|
if visible && !text.isEmpty {
|
||||||
Text(text)
|
Text(text)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|
@ -298,8 +349,11 @@ struct Meaning: View {
|
||||||
#endif
|
#endif
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.animation(reduceMotion ? nil : Motion.settle, value: visible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WordField: View {
|
struct WordField: View {
|
||||||
|
|
@ -307,6 +361,7 @@ struct WordField: View {
|
||||||
var placeholder: String
|
var placeholder: String
|
||||||
var mark: Bool?
|
var mark: Bool?
|
||||||
var palette: Palette?
|
var palette: Palette?
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
|
|
@ -332,6 +387,7 @@ struct WordField: View {
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.stroke(border, lineWidth: mark == nil ? 0.5 : 1.5)
|
.stroke(border, lineWidth: mark == nil ? 0.5 : 1.5)
|
||||||
)
|
)
|
||||||
|
.animation(reduceMotion ? nil : Motion.settle, value: mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var border: Color {
|
private var border: Color {
|
||||||
|
|
@ -426,6 +482,7 @@ struct OptionChip: View {
|
||||||
let selected: Bool
|
let selected: Bool
|
||||||
let mark: Bool?
|
let mark: Bool?
|
||||||
var action: () -> Void
|
var action: () -> Void
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
|
|
@ -453,6 +510,8 @@ struct OptionChip: View {
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
#endif
|
#endif
|
||||||
.padding(.bottom, 4)
|
.padding(.bottom, 4)
|
||||||
|
.animation(reduceMotion ? nil : Motion.settle, value: mark)
|
||||||
|
.animation(reduceMotion ? nil : Motion.press, value: selected)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var symbol: String {
|
private var symbol: String {
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,11 @@ struct ExerciseScaffold<Content: View>: View {
|
||||||
if !feedback.isEmpty {
|
if !feedback.isEmpty {
|
||||||
FeedbackLine(text: feedback, kind: kind, palette: p)
|
FeedbackLine(text: feedback, kind: kind, palette: p)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .leading)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.default, value: feedback)
|
.animation(Motion.feedback, value: feedback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ struct WordPicker: View {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
|
|
@ -1224,9 +1224,10 @@ private struct VlsmEx: View {
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.transition(.opacity.combined(with: .scale(scale: 0.96, anchor: .leading)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.default, value: fb.0)
|
.animation(Motion.feedback, value: fb.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func maybeFinish() {
|
private func maybeFinish() {
|
||||||
|
|
@ -1359,7 +1360,7 @@ struct LitQuizScreen: View {
|
||||||
}) { EmptyView() }
|
}) { EmptyView() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.default, value: answered)
|
.animation(Motion.settle, value: answered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ struct PathMap: View {
|
||||||
#else
|
#else
|
||||||
.frame(minHeight: 420)
|
.frame(minHeight: 420)
|
||||||
#endif
|
#endif
|
||||||
|
.softAppear()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|
@ -321,7 +322,7 @@ struct PathMap: View {
|
||||||
|
|
||||||
if let dest = node.destination, !node.locked {
|
if let dest = node.destination, !node.locked {
|
||||||
NavigationLink(value: dest) { content }
|
NavigationLink(value: dest) { content }
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
} else {
|
} else {
|
||||||
content.opacity(node.locked ? 0.55 : 1)
|
content.opacity(node.locked ? 0.55 : 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ struct PracticeHome: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
macHome
|
macHome.softAppear()
|
||||||
#else
|
#else
|
||||||
iosHome
|
iosHome.softAppear()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,6 +46,7 @@ struct PracticeHome: View {
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
ProgressView(value: pct)
|
ProgressView(value: pct)
|
||||||
|
.animation(Motion.settle, value: pct)
|
||||||
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -91,7 +92,7 @@ struct PracticeHome: View {
|
||||||
NavigationLink(value: dest) {
|
NavigationLink(value: dest) {
|
||||||
subjectTileBody(s, open: true, progress: progress, part: part)
|
subjectTileBody(s, open: true, progress: progress, part: part)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
} else {
|
} else {
|
||||||
subjectTileBody(s, open: false, progress: 0, part: part)
|
subjectTileBody(s, open: false, progress: 0, part: part)
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +109,7 @@ struct PracticeHome: View {
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
if open && part.totalEx > 0 {
|
if open && part.totalEx > 0 {
|
||||||
ProgressView(value: progress)
|
ProgressView(value: progress)
|
||||||
|
.animation(Motion.settle, value: progress)
|
||||||
Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
|
Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -146,6 +148,7 @@ struct PracticeHome: View {
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
ProgressView(value: pct)
|
ProgressView(value: pct)
|
||||||
|
.animation(Motion.settle, value: pct)
|
||||||
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
|
|
@ -415,7 +418,7 @@ struct SettingsScreen: View {
|
||||||
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
|
||||||
.softSurface(cornerRadius: 16)
|
.softSurface(cornerRadius: 16)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -476,6 +479,7 @@ struct StatsScreen: View {
|
||||||
stat(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits))
|
stat(vm.tr("stats_units_label"), vm.fmt("stats_units_fmt", sum.doneUnits, sum.openUnits))
|
||||||
}
|
}
|
||||||
ProgressView(value: pct)
|
ProgressView(value: pct)
|
||||||
|
.animation(Motion.settle, value: pct)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||||
|
|
@ -505,6 +509,7 @@ struct StatsScreen: View {
|
||||||
}
|
}
|
||||||
if open && part.totalEx > 0 {
|
if open && part.totalEx > 0 {
|
||||||
ProgressView(value: Double(part.doneEx) / Double(part.totalEx))
|
ProgressView(value: Double(part.doneEx) / Double(part.totalEx))
|
||||||
|
.animation(Motion.settle, value: part.doneEx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} icon: {
|
} icon: {
|
||||||
|
|
@ -773,7 +778,7 @@ struct SearchScreen: View {
|
||||||
.frame(maxWidth: .infinity, minHeight: 84, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 84, alignment: .leading)
|
||||||
.softSurface(cornerRadius: 16)
|
.softSurface(cornerRadius: 16)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
.disabled(hit.locked)
|
.disabled(hit.locked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -848,7 +853,7 @@ struct SearchScreen: View {
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark.circle.fill")
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
.accessibilityLabel("Clear")
|
.accessibilityLabel("Clear")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1205,7 +1210,7 @@ struct NetYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("net_years_title"))
|
.navigationTitle(vm.tr("net_years_title"))
|
||||||
.navigationSubtitle(vm.tr("net_years_sub"))
|
.navigationSubtitle(vm.tr("net_years_sub"))
|
||||||
|
|
@ -1246,7 +1251,7 @@ struct HwYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Technické vybavení"))
|
.navigationTitle(vm.tr("Technické vybavení"))
|
||||||
.navigationSubtitle(vm.tr("hw_years_sub"))
|
.navigationSubtitle(vm.tr("hw_years_sub"))
|
||||||
|
|
@ -1288,7 +1293,7 @@ struct DeHomeScreen: View {
|
||||||
NavigationLink(value: rows[i].3) {
|
NavigationLink(value: rows[i].3) {
|
||||||
MacLinkCard(title: vm.tr(rows[i].1), subtitle: vm.tr(rows[i].2), badge: rows[i].0)
|
MacLinkCard(title: vm.tr(rows[i].1), subtitle: vm.tr(rows[i].2), badge: rows[i].0)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Deutsch"))
|
.navigationTitle(vm.tr("Deutsch"))
|
||||||
.navigationSubtitle(vm.tr("de_home_sub"))
|
.navigationSubtitle(vm.tr("de_home_sub"))
|
||||||
|
|
@ -1324,7 +1329,7 @@ struct EnYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("English"))
|
.navigationTitle(vm.tr("English"))
|
||||||
.navigationSubtitle(vm.tr("en_years_sub"))
|
.navigationSubtitle(vm.tr("en_years_sub"))
|
||||||
|
|
@ -1358,7 +1363,7 @@ struct OnYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Občanská nauka"))
|
.navigationTitle(vm.tr("Občanská nauka"))
|
||||||
.navigationSubtitle(vm.tr("on_years_sub"))
|
.navigationSubtitle(vm.tr("on_years_sub"))
|
||||||
|
|
@ -1472,9 +1477,14 @@ struct SlidesScreen: View {
|
||||||
#else
|
#else
|
||||||
if slides.indices.contains(idx) {
|
if slides.indices.contains(idx) {
|
||||||
slidePage(slides[idx])
|
slidePage(slides[idx])
|
||||||
|
.id(idx)
|
||||||
|
.transition(.opacity)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
.animation(Motion.settle, value: idx)
|
||||||
|
#endif
|
||||||
.detailScreen(title, subtitle: subtitle)
|
.detailScreen(title, subtitle: subtitle)
|
||||||
.glassBottomBar {
|
.glassBottomBar {
|
||||||
VStack(spacing: 10) {
|
VStack(spacing: 10) {
|
||||||
|
|
@ -1485,7 +1495,7 @@ struct SlidesScreen: View {
|
||||||
.frame(width: i == idx ? 18 : 6, height: 6)
|
.frame(width: i == idx ? 18 : 6, height: 6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.default, value: idx)
|
.animation(Motion.settle, value: idx)
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
Button {
|
Button {
|
||||||
|
|
@ -1668,7 +1678,7 @@ struct LitYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Literatura"))
|
.navigationTitle(vm.tr("Literatura"))
|
||||||
.navigationSubtitle(vm.tr("lit_years_sub"))
|
.navigationSubtitle(vm.tr("lit_years_sub"))
|
||||||
|
|
@ -1710,7 +1720,7 @@ struct MatYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: years[i].3)
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: years[i].3)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Matematika"))
|
.navigationTitle(vm.tr("Matematika"))
|
||||||
.navigationSubtitle(vm.tr("mat_years_sub"))
|
.navigationSubtitle(vm.tr("mat_years_sub"))
|
||||||
|
|
@ -1754,7 +1764,7 @@ struct FyzYearsScreen: View {
|
||||||
NavigationLink(value: years[i].2) {
|
NavigationLink(value: years[i].2) {
|
||||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Fyzika"))
|
.navigationTitle(vm.tr("Fyzika"))
|
||||||
.navigationSubtitle(vm.tr("fyz_years_sub"))
|
.navigationSubtitle(vm.tr("fyz_years_sub"))
|
||||||
|
|
@ -1790,12 +1800,12 @@ struct SciMapScreen: View {
|
||||||
NavigationLink(value: Route.chemMap) {
|
NavigationLink(value: Route.chemMap) {
|
||||||
MacLinkCard(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub"), systemImage: "flask.fill")
|
MacLinkCard(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub"), systemImage: "flask.fill")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
} else {
|
} else {
|
||||||
NavigationLink(value: Route.bioMap) {
|
NavigationLink(value: Route.bioMap) {
|
||||||
MacLinkCard(title: vm.tr("Biologie"), subtitle: vm.tr("bio_sub"), systemImage: "leaf.fill")
|
MacLinkCard(title: vm.tr("Biologie"), subtitle: vm.tr("bio_sub"), systemImage: "leaf.fill")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Základy Přírodopisných věd"))
|
.navigationTitle(vm.tr("Základy Přírodopisných věd"))
|
||||||
|
|
@ -1835,17 +1845,17 @@ struct CzechMapScreen: View {
|
||||||
NavigationLink(value: Route.mluvnice) {
|
NavigationLink(value: Route.mluvnice) {
|
||||||
MacLinkCard(title: vm.tr("Mluvnice"), systemImage: "textformat")
|
MacLinkCard(title: vm.tr("Mluvnice"), systemImage: "textformat")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
case 2:
|
case 2:
|
||||||
NavigationLink(value: Route.readingList) {
|
NavigationLink(value: Route.readingList) {
|
||||||
MacLinkCard(title: vm.tr("Maturitní četba"), systemImage: "books.vertical")
|
MacLinkCard(title: vm.tr("Maturitní četba"), systemImage: "books.vertical")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
default:
|
default:
|
||||||
NavigationLink(value: Route.litYears) {
|
NavigationLink(value: Route.litYears) {
|
||||||
MacLinkCard(title: vm.tr("Literatura"), systemImage: "book.closed")
|
MacLinkCard(title: vm.tr("Literatura"), systemImage: "book.closed")
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle(vm.tr("Český jazyk a literatura"))
|
.navigationTitle(vm.tr("Český jazyk a literatura"))
|
||||||
|
|
@ -1932,7 +1942,7 @@ struct BookListScreen: View {
|
||||||
.frame(maxWidth: .infinity, minHeight: 168, alignment: .topLeading)
|
.frame(maxWidth: .infinity, minHeight: 168, alignment: .topLeading)
|
||||||
.softSurface(cornerRadius: 18)
|
.softSurface(cornerRadius: 18)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(MacChrome.pageInset)
|
.padding(MacChrome.pageInset)
|
||||||
|
|
@ -2070,7 +2080,7 @@ struct BookScreen: View {
|
||||||
doneLabel: vm.tr("book_done")
|
doneLabel: vm.tr("book_done")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
if hasPlot {
|
if hasPlot {
|
||||||
NavigationLink(value: Route.bookPlot(id)) {
|
NavigationLink(value: Route.bookPlot(id)) {
|
||||||
|
|
@ -2082,7 +2092,7 @@ struct BookScreen: View {
|
||||||
doneLabel: vm.tr("book_done")
|
doneLabel: vm.tr("book_done")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
|
@ -2096,7 +2106,7 @@ struct BookScreen: View {
|
||||||
doneLabel: vm.tr("book_done")
|
doneLabel: vm.tr("book_done")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
if hasPlot {
|
if hasPlot {
|
||||||
NavigationLink(value: Route.bookPlot(id)) {
|
NavigationLink(value: Route.bookPlot(id)) {
|
||||||
|
|
@ -2108,7 +2118,7 @@ struct BookScreen: View {
|
||||||
doneLabel: vm.tr("book_done")
|
doneLabel: vm.tr("book_done")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(PressScaleStyle())
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,10 @@ struct MacRoot: View {
|
||||||
if showsUpdateAccessory {
|
if showsUpdateAccessory {
|
||||||
UpdateBanner(vm: vm)
|
UpdateBanner(vm: vm)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
|
.transition(.move(edge: .top).combined(with: .opacity))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.animation(Motion.settle, value: showsUpdateAccessory)
|
||||||
.onAppear { vm.checkUpdate(false) }
|
.onAppear { vm.checkUpdate(false) }
|
||||||
.frame(minWidth: 980, minHeight: 640)
|
.frame(minWidth: 980, minHeight: 640)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1282,7 +1282,7 @@ static void on_stub_quiz_clicked(GtkButton *button, gpointer user_data) {
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
cetba_prepare_stub_quiz();
|
cetba_prepare_stub_quiz();
|
||||||
if (main_stack)
|
if (main_stack)
|
||||||
gtk_stack_set_visible_child_name(main_stack, "cetbastubquiz");
|
show_page("cetbastubquiz", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_stub_plot_clicked(GtkButton *button, gpointer user_data) {
|
static void on_stub_plot_clicked(GtkButton *button, gpointer user_data) {
|
||||||
|
|
@ -1290,7 +1290,7 @@ static void on_stub_plot_clicked(GtkButton *button, gpointer user_data) {
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
cetba_prepare_stub_plot();
|
cetba_prepare_stub_plot();
|
||||||
if (main_stack)
|
if (main_stack)
|
||||||
gtk_stack_set_visible_child_name(main_stack, "cetbastubdej");
|
show_page("cetbastubdej", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *book_rail;
|
static GtkWidget *book_rail;
|
||||||
|
|
@ -1467,7 +1467,7 @@ static void on_book_node_clicked(GtkButton *button, gpointer user_data) {
|
||||||
rebuild_stub_about(idx);
|
rebuild_stub_about(idx);
|
||||||
}
|
}
|
||||||
if (main_stack && target)
|
if (main_stack && target)
|
||||||
gtk_stack_set_visible_child_name(main_stack, target);
|
show_page(target, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *book_make_node(int i) {
|
static GtkWidget *book_make_node(int i) {
|
||||||
|
|
@ -1810,7 +1810,7 @@ void cetba_open_stub(int idx) {
|
||||||
gtk_label_set_text(GTK_LABEL(stub_title_lbl), book_defs[idx].title);
|
gtk_label_set_text(GTK_LABEL(stub_title_lbl), book_defs[idx].title);
|
||||||
rebuild_stub_about(idx);
|
rebuild_stub_about(idx);
|
||||||
if (main_stack)
|
if (main_stack)
|
||||||
gtk_stack_set_visible_child_name(main_stack, "cetbastub");
|
show_page("cetbastub", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cetba_register_search(void) {
|
void cetba_register_search(void) {
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ static void en_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void en_add_node(EngMap *m, GtkFixed *fixed, int index) {
|
static void en_add_node(EngMap *m, GtkFixed *fixed, int index) {
|
||||||
|
|
@ -699,7 +699,7 @@ static void en_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
skill_slide_apply(L);
|
skill_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ static void fyz_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *fyz_lesson_button(FyzTrack *T, int index) {
|
static GtkWidget *fyz_lesson_button(FyzTrack *T, int index) {
|
||||||
|
|
@ -460,7 +460,7 @@ static void fyz_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
fyz_slide_apply(L);
|
fyz_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1199,6 +1199,7 @@ void draw_bulb_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||||
void net_lesson_notes_ensure(NetLesson *L);
|
void net_lesson_notes_ensure(NetLesson *L);
|
||||||
void net_rescale_lesson_notes(NetLesson *L, int body, int head, int kick);
|
void net_rescale_lesson_notes(NetLesson *L, int body, int head, int kick);
|
||||||
GtkWidget *make_back_button(const char *target);
|
GtkWidget *make_back_button(const char *target);
|
||||||
|
void show_page(const char *name, int motion);
|
||||||
void on_nav_clicked(GtkButton *button, gpointer user_data);
|
void on_nav_clicked(GtkButton *button, gpointer user_data);
|
||||||
int unit_done_count(const UnitCtx *u);
|
int unit_done_count(const UnitCtx *u);
|
||||||
void progress_for_units(int first, int n, ProgressSum *out);
|
void progress_for_units(int first, int n, ProgressSum *out);
|
||||||
|
|
|
||||||
2
src/hw.c
2
src/hw.c
|
|
@ -661,7 +661,7 @@ void hw_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
hw_slide_apply(L);
|
hw_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -377,7 +377,7 @@ static void lit_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void lit_add_node(LitTrack *T, GtkFixed *fixed, int index) {
|
static void lit_add_node(LitTrack *T, GtkFixed *fixed, int index) {
|
||||||
|
|
@ -634,7 +634,7 @@ static void lit_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
lit_slide_apply(L);
|
lit_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -736,10 +736,13 @@ void activate(GtkApplication *app, gpointer user_data) {
|
||||||
const char *size = g_getenv("MATURITA_SIZE");
|
const char *size = g_getenv("MATURITA_SIZE");
|
||||||
int w, h;
|
int w, h;
|
||||||
|
|
||||||
|
gtk_stack_set_transition_type(main_stack, GTK_STACK_TRANSITION_TYPE_NONE);
|
||||||
if (start && *start)
|
if (start && *start)
|
||||||
gtk_stack_set_visible_child_name(main_stack, start);
|
gtk_stack_set_visible_child_name(main_stack, start);
|
||||||
else
|
else
|
||||||
gtk_stack_set_visible_child_name(main_stack, "welcome");
|
gtk_stack_set_visible_child_name(main_stack, "welcome");
|
||||||
|
gtk_stack_set_transition_type(main_stack, GTK_STACK_TRANSITION_TYPE_CROSSFADE);
|
||||||
|
gtk_stack_set_transition_duration(main_stack, 180);
|
||||||
if (size && sscanf(size, "%dx%d", &w, &h) == 2)
|
if (size && sscanf(size, "%dx%d", &w, &h) == 2)
|
||||||
gtk_window_set_default_size(window, w, h);
|
gtk_window_set_default_size(window, w, h);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,7 @@ static void math_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *math_lesson_button(MathTrack *T, int index) {
|
static GtkWidget *math_lesson_button(MathTrack *T, int index) {
|
||||||
|
|
@ -469,7 +469,7 @@ static void math_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
math_slide_apply(L);
|
math_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,40 @@
|
||||||
/* Navigation */
|
/* Navigation */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
/* motion: -1 slides back, 0 crossfades, 1 slides forward. */
|
||||||
|
void show_page(const char *name, int motion) {
|
||||||
|
GtkStackTransitionType type = GTK_STACK_TRANSITION_TYPE_CROSSFADE;
|
||||||
|
guint ms = 180;
|
||||||
|
|
||||||
|
if (!main_stack || !name || !name[0])
|
||||||
|
return;
|
||||||
|
if (!gtk_stack_get_child_by_name(main_stack, name))
|
||||||
|
return;
|
||||||
|
if (motion > 0) {
|
||||||
|
type = GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT;
|
||||||
|
ms = 200;
|
||||||
|
} else if (motion < 0) {
|
||||||
|
type = GTK_STACK_TRANSITION_TYPE_SLIDE_RIGHT;
|
||||||
|
ms = 200;
|
||||||
|
}
|
||||||
|
gtk_stack_set_transition_type(main_stack, type);
|
||||||
|
gtk_stack_set_transition_duration(main_stack, ms);
|
||||||
|
gtk_stack_set_visible_child_name(main_stack, name);
|
||||||
|
}
|
||||||
|
|
||||||
void on_nav_clicked(GtkButton *button, gpointer user_data) {
|
void on_nav_clicked(GtkButton *button, gpointer user_data) {
|
||||||
const char *target = g_object_get_data(G_OBJECT(button), "target");
|
const char *target = g_object_get_data(G_OBJECT(button), "target");
|
||||||
|
int back = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "nav-back"));
|
||||||
|
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
if (target)
|
if (target)
|
||||||
gtk_stack_set_visible_child_name(main_stack, target);
|
show_page(target, back ? -1 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_continue_clicked(GtkButton *button, gpointer user_data) {
|
void on_continue_clicked(GtkButton *button, gpointer user_data) {
|
||||||
(void)button;
|
(void)button;
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
gtk_stack_set_visible_child_name(main_stack, "subjects");
|
show_page("subjects", 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
GtkWidget *make_back_button(const char *target) {
|
GtkWidget *make_back_button(const char *target) {
|
||||||
|
|
@ -31,6 +54,7 @@ GtkWidget *make_back_button(const char *target) {
|
||||||
g_signal_connect_swapped(back, "state-flags-changed",
|
g_signal_connect_swapped(back, "state-flags-changed",
|
||||||
G_CALLBACK(gtk_widget_queue_draw), icon);
|
G_CALLBACK(gtk_widget_queue_draw), icon);
|
||||||
g_object_set_data_full(G_OBJECT(back), "target", g_strdup(target), g_free);
|
g_object_set_data_full(G_OBJECT(back), "target", g_strdup(target), g_free);
|
||||||
|
g_object_set_data(G_OBJECT(back), "nav-back", GINT_TO_POINTER(1));
|
||||||
g_signal_connect(back, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
g_signal_connect(back, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||||
return back;
|
return back;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ void net_open_unit(GtkButton *button, gpointer data) {
|
||||||
return;
|
return;
|
||||||
L->idx = 0;
|
L->idx = 0;
|
||||||
net_slide_apply(L);
|
net_slide_apply(L);
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void net_slide_prev(GtkButton *button, gpointer data) {
|
void net_slide_prev(GtkButton *button, gpointer data) {
|
||||||
|
|
@ -220,7 +220,7 @@ void net_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
net_slide_apply(L);
|
net_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,7 @@ static void on_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_add_node(GtkFixed *fixed, int index) {
|
static void on_add_node(GtkFixed *fixed, int index) {
|
||||||
|
|
@ -660,7 +660,7 @@ static void on_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
on_slide_apply(L);
|
on_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ static void on2_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on2_add_node(GtkFixed *fixed, int index) {
|
static void on2_add_node(GtkFixed *fixed, int index) {
|
||||||
|
|
@ -566,7 +566,7 @@ static void on2_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
on2_slide_apply(L);
|
on2_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@ static void on3_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on3_add_node(GtkFixed *fixed, int index) {
|
static void on3_add_node(GtkFixed *fixed, int index) {
|
||||||
|
|
@ -565,7 +565,7 @@ static void on3_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
on3_slide_apply(L);
|
on3_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ static void on4_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on4_add_node(GtkFixed *fixed, int index) {
|
static void on4_add_node(GtkFixed *fixed, int index) {
|
||||||
|
|
@ -566,7 +566,7 @@ static void on4_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
on4_slide_apply(L);
|
on4_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@ static void sci_open_unit(GtkButton *button, gpointer data) {
|
||||||
if (L->next_btn)
|
if (L->next_btn)
|
||||||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||||||
}
|
}
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->unit_page);
|
show_page(L->unit_page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *sci_lesson_button(SciTrack *T, int index) {
|
static GtkWidget *sci_lesson_button(SciTrack *T, int index) {
|
||||||
|
|
@ -460,7 +460,7 @@ static void sci_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
sci_slide_apply(L);
|
sci_slide_apply(L);
|
||||||
} else {
|
} else {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ static void qy_slide_next(GtkButton *button, gpointer data) {
|
||||||
L->idx++;
|
L->idx++;
|
||||||
skill_slide_apply(L);
|
skill_slide_apply(L);
|
||||||
} else if (L->ex_page) {
|
} else if (L->ex_page) {
|
||||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
show_page(L->ex_page, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
49
src/search.c
49
src/search.c
|
|
@ -16,6 +16,8 @@ typedef struct {
|
||||||
} SearchItem;
|
} SearchItem;
|
||||||
|
|
||||||
static GtkWidget *search_layer;
|
static GtkWidget *search_layer;
|
||||||
|
static GtkWidget *search_reveal;
|
||||||
|
static gboolean search_want_open;
|
||||||
static GtkWidget *search_entry;
|
static GtkWidget *search_entry;
|
||||||
static GtkWidget *search_list;
|
static GtkWidget *search_list;
|
||||||
static GtkWidget *search_empty;
|
static GtkWidget *search_empty;
|
||||||
|
|
@ -472,7 +474,7 @@ static void search_go(const char *target) {
|
||||||
if (!gtk_stack_get_child_by_name(main_stack, target))
|
if (!gtk_stack_get_child_by_name(main_stack, target))
|
||||||
return;
|
return;
|
||||||
search_close();
|
search_close();
|
||||||
gtk_stack_set_visible_child_name(main_stack, target);
|
show_page(target, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_row_activated(GtkListBox *box, GtkListBoxRow *row,
|
static void on_row_activated(GtkListBox *box, GtkListBoxRow *row,
|
||||||
|
|
@ -641,19 +643,40 @@ static void on_scrim_clicked(GtkGestureClick *g, gint n_press, gdouble x,
|
||||||
search_close();
|
search_close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void on_search_revealed(GObject *obj, GParamSpec *pspec, gpointer data) {
|
||||||
|
GtkRevealer *reveal = GTK_REVEALER(obj);
|
||||||
|
|
||||||
|
(void)pspec;
|
||||||
|
(void)data;
|
||||||
|
if (!gtk_revealer_get_reveal_child(reveal) &&
|
||||||
|
!gtk_revealer_get_child_revealed(reveal))
|
||||||
|
gtk_widget_set_visible(GTK_WIDGET(reveal), FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
gboolean search_is_open(void) {
|
gboolean search_is_open(void) {
|
||||||
return search_layer && gtk_widget_get_visible(search_layer);
|
return search_want_open;
|
||||||
}
|
}
|
||||||
|
|
||||||
void search_close(void) {
|
void search_close(void) {
|
||||||
if (search_layer)
|
search_want_open = FALSE;
|
||||||
gtk_widget_set_visible(search_layer, FALSE);
|
if (search_reveal)
|
||||||
|
gtk_revealer_set_reveal_child(GTK_REVEALER(search_reveal), FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean reveal_search(gpointer data) {
|
||||||
|
(void)data;
|
||||||
|
if (search_want_open && search_reveal)
|
||||||
|
gtk_revealer_set_reveal_child(GTK_REVEALER(search_reveal), TRUE);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void search_open(void) {
|
void search_open(void) {
|
||||||
if (!search_layer)
|
if (!search_reveal)
|
||||||
return;
|
return;
|
||||||
gtk_widget_set_visible(search_layer, TRUE);
|
search_want_open = TRUE;
|
||||||
|
gtk_widget_set_visible(search_reveal, TRUE);
|
||||||
|
/* Wait a frame so the overlay is mapped and the fade actually runs. */
|
||||||
|
g_idle_add(reveal_search, NULL);
|
||||||
if (search_entry) {
|
if (search_entry) {
|
||||||
gtk_editable_set_text(GTK_EDITABLE(search_entry), "");
|
gtk_editable_set_text(GTK_EDITABLE(search_entry), "");
|
||||||
gtk_widget_grab_focus(search_entry);
|
gtk_widget_grab_focus(search_entry);
|
||||||
|
|
@ -711,7 +734,6 @@ void search_attach(GtkOverlay *overlay) {
|
||||||
gtk_widget_add_css_class(search_layer, "search-layer");
|
gtk_widget_add_css_class(search_layer, "search-layer");
|
||||||
gtk_widget_set_hexpand(search_layer, TRUE);
|
gtk_widget_set_hexpand(search_layer, TRUE);
|
||||||
gtk_widget_set_vexpand(search_layer, TRUE);
|
gtk_widget_set_vexpand(search_layer, TRUE);
|
||||||
gtk_widget_set_visible(search_layer, FALSE);
|
|
||||||
|
|
||||||
scrim = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
scrim = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
gtk_widget_add_css_class(scrim, "search-scrim");
|
gtk_widget_add_css_class(scrim, "search-scrim");
|
||||||
|
|
@ -763,5 +785,16 @@ void search_attach(GtkOverlay *overlay) {
|
||||||
g_signal_connect(search_list, "row-activated",
|
g_signal_connect(search_list, "row-activated",
|
||||||
G_CALLBACK(on_row_activated), NULL);
|
G_CALLBACK(on_row_activated), NULL);
|
||||||
|
|
||||||
gtk_overlay_add_overlay(overlay, search_layer);
|
search_reveal = gtk_revealer_new();
|
||||||
|
gtk_revealer_set_transition_type(GTK_REVEALER(search_reveal),
|
||||||
|
GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
|
||||||
|
gtk_revealer_set_transition_duration(GTK_REVEALER(search_reveal), 160);
|
||||||
|
gtk_revealer_set_reveal_child(GTK_REVEALER(search_reveal), FALSE);
|
||||||
|
gtk_widget_set_hexpand(search_reveal, TRUE);
|
||||||
|
gtk_widget_set_vexpand(search_reveal, TRUE);
|
||||||
|
gtk_widget_set_visible(search_reveal, FALSE);
|
||||||
|
gtk_revealer_set_child(GTK_REVEALER(search_reveal), search_layer);
|
||||||
|
g_signal_connect(search_reveal, "notify::child-revealed",
|
||||||
|
G_CALLBACK(on_search_revealed), NULL);
|
||||||
|
gtk_overlay_add_overlay(overlay, search_reveal);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ void on_stats_back_clicked(GtkButton *button, gpointer user_data) {
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
if (!target || !target[0])
|
if (!target || !target[0])
|
||||||
target = "subjects";
|
target = "subjects";
|
||||||
gtk_stack_set_visible_child_name(main_stack, target);
|
show_page(target, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void on_stats_clicked(GtkButton *button, gpointer user_data) {
|
void on_stats_clicked(GtkButton *button, gpointer user_data) {
|
||||||
|
|
@ -227,7 +227,7 @@ void on_stats_clicked(GtkButton *button, gpointer user_data) {
|
||||||
stats_return_page = g_strdup(cur);
|
stats_return_page = g_strdup(cur);
|
||||||
}
|
}
|
||||||
refresh_stats_ui();
|
refresh_stats_ui();
|
||||||
gtk_stack_set_visible_child_name(main_stack, "stats");
|
show_page("stats", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out) {
|
GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out) {
|
||||||
|
|
|
||||||
18
src/update.c
18
src/update.c
|
|
@ -413,7 +413,7 @@ static void update_refresh_ui(void) {
|
||||||
gtk_widget_set_visible(up.progress, up.state == UPDATE_DOWNLOADING);
|
gtk_widget_set_visible(up.progress, up.state == UPDATE_DOWNLOADING);
|
||||||
|
|
||||||
if (up.banner) {
|
if (up.banner) {
|
||||||
gtk_widget_set_visible(up.banner, banner);
|
gtk_revealer_set_reveal_child(GTK_REVEALER(up.banner), banner);
|
||||||
if (banner) {
|
if (banner) {
|
||||||
gtk_label_set_text(GTK_LABEL(up.banner_label), status);
|
gtk_label_set_text(GTK_LABEL(up.banner_label), status);
|
||||||
gtk_button_set_label(GTK_BUTTON(up.banner_action), tr(action));
|
gtk_button_set_label(GTK_BUTTON(up.banner_action), tr(action));
|
||||||
|
|
@ -1019,7 +1019,7 @@ static void on_update_dismiss(GtkButton *btn, gpointer data) {
|
||||||
(void)btn;
|
(void)btn;
|
||||||
(void)data;
|
(void)data;
|
||||||
if (up.banner)
|
if (up.banner)
|
||||||
gtk_widget_set_visible(up.banner, FALSE);
|
gtk_revealer_set_reveal_child(GTK_REVEALER(up.banner), FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Strip shown above the page stack once an update is waiting. */
|
/* Strip shown above the page stack once an update is waiting. */
|
||||||
|
|
@ -1030,7 +1030,6 @@ GtkWidget *build_update_banner(void) {
|
||||||
GtkWidget *later = gtk_button_new();
|
GtkWidget *later = gtk_button_new();
|
||||||
|
|
||||||
gtk_widget_add_css_class(bar, "update-banner");
|
gtk_widget_add_css_class(bar, "update-banner");
|
||||||
gtk_widget_set_visible(bar, FALSE);
|
|
||||||
|
|
||||||
gtk_widget_set_hexpand(label, TRUE);
|
gtk_widget_set_hexpand(label, TRUE);
|
||||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
|
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
|
||||||
|
|
@ -1048,10 +1047,19 @@ GtkWidget *build_update_banner(void) {
|
||||||
g_signal_connect(later, "clicked", G_CALLBACK(on_update_dismiss), NULL);
|
g_signal_connect(later, "clicked", G_CALLBACK(on_update_dismiss), NULL);
|
||||||
gtk_box_append(GTK_BOX(bar), later);
|
gtk_box_append(GTK_BOX(bar), later);
|
||||||
|
|
||||||
up.banner = bar;
|
|
||||||
up.banner_label = label;
|
up.banner_label = label;
|
||||||
up.banner_action = action;
|
up.banner_action = action;
|
||||||
return bar;
|
{
|
||||||
|
GtkWidget *reveal = gtk_revealer_new();
|
||||||
|
|
||||||
|
gtk_revealer_set_transition_type(GTK_REVEALER(reveal),
|
||||||
|
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
||||||
|
gtk_revealer_set_transition_duration(GTK_REVEALER(reveal), 180);
|
||||||
|
gtk_revealer_set_reveal_child(GTK_REVEALER(reveal), FALSE);
|
||||||
|
gtk_revealer_set_child(GTK_REVEALER(reveal), bar);
|
||||||
|
up.banner = reveal;
|
||||||
|
return reveal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "Updates" section of the settings popover. */
|
/* "Updates" section of the settings popover. */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue