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
|
||||
var showChangelog by mutableStateOf(shouldShowChangelog())
|
||||
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)
|
||||
val route: Route get() = stack.last()
|
||||
|
|
@ -50,11 +53,13 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
|||
searchOpen = false
|
||||
if (stack.last() == r) return
|
||||
if (r.isRoot) {
|
||||
navigatingForward = false
|
||||
val target = if (r == Route.Welcome) Route.Subjects else r
|
||||
stack.clear()
|
||||
stack.add(target)
|
||||
return
|
||||
}
|
||||
navigatingForward = true
|
||||
if (root != Route.Subjects) {
|
||||
stack.clear()
|
||||
stack.addAll(r.stack())
|
||||
|
|
@ -72,17 +77,20 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
|
|||
fun openPage(page: String) {
|
||||
val r = routeFromPage(page) ?: return
|
||||
searchOpen = false
|
||||
navigatingForward = true
|
||||
stack.clear()
|
||||
stack.addAll(r.stack())
|
||||
}
|
||||
|
||||
/** Replace the current screen (lesson slides -> its exercise). */
|
||||
fun replace(r: Route) {
|
||||
navigatingForward = true
|
||||
if (stack.size > 1) stack.removeAt(stack.lastIndex)
|
||||
stack.add(r)
|
||||
}
|
||||
|
||||
fun back() {
|
||||
navigatingForward = false
|
||||
when {
|
||||
stack.size > 1 -> stack.removeAt(stack.lastIndex)
|
||||
root != Route.Subjects -> go(Route.Subjects)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ package org.maturita.maturita
|
|||
|
||||
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
|
||||
|
|
@ -206,7 +214,24 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
|||
if (vm.searchOpen) vm.searchOpen = false else vm.back()
|
||||
}
|
||||
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.Stats -> StatsScreen(vm)
|
||||
Route.Settings -> SettingsScreen(vm)
|
||||
|
|
@ -609,6 +634,13 @@ private fun MaturitaAppBody(vm: AppViewModel) {
|
|||
is Route.BookQuiz -> LitQuizScreen(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
|
||||
|
||||
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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -36,6 +44,7 @@ import androidx.compose.material3.FilterChip
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -53,9 +62,11 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
|
|
@ -234,8 +245,12 @@ fun ActionBar(
|
|||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
if (feedback.isNotEmpty()) {
|
||||
FeedbackLine(feedback, kind)
|
||||
AnimatedVisibility(
|
||||
visible = feedback.isNotEmpty(),
|
||||
enter = fadeIn(tween(180)) + expandVertically(),
|
||||
exit = fadeOut(tween(120)) + shrinkVertically(),
|
||||
) {
|
||||
FeedbackLine(feedback, kind, animate = false)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
|
|
@ -309,8 +324,27 @@ fun PillButton(label: String, modifier: Modifier = Modifier, onClick: () -> Unit
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun FeedbackLine(text: String, kind: String) {
|
||||
if (text.isEmpty()) return
|
||||
fun SmoothProgress(
|
||||
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) {
|
||||
"ok" -> MaterialTheme.colorScheme.primary
|
||||
"warn" -> MaterialTheme.colorScheme.tertiary
|
||||
|
|
@ -321,6 +355,22 @@ fun FeedbackLine(text: String, kind: String) {
|
|||
"warn" -> Icons.Filled.Warning
|
||||
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) {
|
||||
Icon(icon, null, tint = color, modifier = Modifier.size(20.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
|
@ -330,13 +380,18 @@ fun FeedbackLine(text: String, kind: String) {
|
|||
|
||||
@Composable
|
||||
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,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -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. */
|
||||
@Composable
|
||||
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
|
||||
false -> MaterialTheme.colorScheme.errorContainer
|
||||
null -> if (selected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
val container by animateColorAsState(target, tween(180), label = "option")
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ import androidx.compose.material3.Card
|
|||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
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,
|
||||
) { inner ->
|
||||
Column(Modifier.fillMaxSize().padding(inner).verticalScroll(rememberScrollState()).padding(16.dp)) {
|
||||
LinearProgressIndicator(
|
||||
progress = { if (total == 0) 0f else done / total.toFloat() },
|
||||
SmoothProgress(
|
||||
progress = if (total == 0) 0f else done / total.toFloat(),
|
||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
|
@ -1354,7 +1353,7 @@ fun LitQuizScreen(vm: AppViewModel, id: String) {
|
|||
}
|
||||
} else {
|
||||
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))
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
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.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -116,8 +115,8 @@ fun HomeScreen(vm: AppViewModel) {
|
|||
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { pct },
|
||||
SmoothProgress(
|
||||
progress = pct,
|
||||
modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
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))
|
||||
}
|
||||
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 {
|
||||
if (open && part.totalEx > 0) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { part.doneEx / part.totalEx.toFloat() },
|
||||
SmoothProgress(
|
||||
progress = part.doneEx / part.totalEx.toFloat(),
|
||||
modifier = Modifier.fillMaxWidth().height(4.dp).clip(CircleShape),
|
||||
)
|
||||
}
|
||||
|
|
@ -770,7 +769,6 @@ fun HwYearsScreen(vm: AppViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Composable
|
||||
fun DeHomeScreen(vm: AppViewModel) {
|
||||
DetailScaffold(vm, vm.tr("Deutsch"), vm.tr("de_home_sub")) { inner ->
|
||||
|
|
@ -808,6 +806,7 @@ fun EnYearsScreen(vm: AppViewModel) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnYearsScreen(vm: AppViewModel) {
|
||||
DetailScaffold(vm, vm.tr("Občanská nauka"), vm.tr("on_years_sub")) { inner ->
|
||||
LazyColumn(contentPadding = PaddingValues(top = inner.calculateTopPadding(), bottom = inner.calculateBottomPadding() + 16.dp)) {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ windowcontrols button:not(:hover) image {
|
|||
}
|
||||
button {
|
||||
background-image: none;
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
}
|
||||
tooltip {
|
||||
background-color: @bg_crust;
|
||||
|
|
|
|||
|
|
@ -110,12 +110,12 @@ final class AppModel: ObservableObject {
|
|||
}
|
||||
|
||||
func setTheme(_ id: ThemeId) {
|
||||
themeId = id
|
||||
withAnimation(Motion.settle) { themeId = id }
|
||||
progress.themeId = id
|
||||
}
|
||||
|
||||
func applyMode(_ m: ColorMode) {
|
||||
mode = m
|
||||
withAnimation(Motion.settle) { mode = m }
|
||||
progress.mode = m
|
||||
}
|
||||
|
||||
|
|
@ -285,7 +285,7 @@ final class AppModel: ObservableObject {
|
|||
|
||||
func dismissChangelog() {
|
||||
progress.seenCommit = AppConfig.commit
|
||||
showChangelog = false
|
||||
withAnimation(Motion.settle) { showChangelog = false }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,47 @@
|
|||
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)
|
||||
enum MacChrome {
|
||||
static let pageInset: CGFloat = 28
|
||||
|
|
@ -265,8 +307,10 @@ struct FeedbackLine: View {
|
|||
let text: String
|
||||
let kind: String
|
||||
let palette: Palette
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if !text.isEmpty {
|
||||
Label {
|
||||
Text(text)
|
||||
|
|
@ -279,8 +323,12 @@ struct FeedbackLine: View {
|
|||
.font(.subheadline.weight(.semibold))
|
||||
#endif
|
||||
.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 {
|
||||
|
|
@ -288,7 +336,10 @@ struct Meaning: View {
|
|||
let palette: Palette
|
||||
let visible: Bool
|
||||
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if visible && !text.isEmpty {
|
||||
Text(text)
|
||||
#if os(macOS)
|
||||
|
|
@ -298,8 +349,11 @@ struct Meaning: View {
|
|||
#endif
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
.animation(reduceMotion ? nil : Motion.settle, value: visible)
|
||||
}
|
||||
}
|
||||
|
||||
struct WordField: View {
|
||||
|
|
@ -307,6 +361,7 @@ struct WordField: View {
|
|||
var placeholder: String
|
||||
var mark: Bool?
|
||||
var palette: Palette?
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
|
|
@ -332,6 +387,7 @@ struct WordField: View {
|
|||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.stroke(border, lineWidth: mark == nil ? 0.5 : 1.5)
|
||||
)
|
||||
.animation(reduceMotion ? nil : Motion.settle, value: mark)
|
||||
}
|
||||
|
||||
private var border: Color {
|
||||
|
|
@ -426,6 +482,7 @@ struct OptionChip: View {
|
|||
let selected: Bool
|
||||
let mark: Bool?
|
||||
var action: () -> Void
|
||||
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
|
|
@ -453,6 +510,8 @@ struct OptionChip: View {
|
|||
.controlSize(.large)
|
||||
#endif
|
||||
.padding(.bottom, 4)
|
||||
.animation(reduceMotion ? nil : Motion.settle, value: mark)
|
||||
.animation(reduceMotion ? nil : Motion.press, value: selected)
|
||||
}
|
||||
|
||||
private var symbol: String {
|
||||
|
|
|
|||
|
|
@ -35,10 +35,11 @@ struct ExerciseScaffold<Content: View>: View {
|
|||
if !feedback.isEmpty {
|
||||
FeedbackLine(text: feedback, kind: kind, palette: p)
|
||||
.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)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
|
@ -1224,9 +1224,10 @@ private struct VlsmEx: View {
|
|||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.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() {
|
||||
|
|
@ -1359,7 +1360,7 @@ struct LitQuizScreen: View {
|
|||
}) { EmptyView() }
|
||||
}
|
||||
}
|
||||
.animation(.default, value: answered)
|
||||
.animation(Motion.settle, value: answered)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ struct PathMap: View {
|
|||
#else
|
||||
.frame(minHeight: 420)
|
||||
#endif
|
||||
.softAppear()
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -321,7 +322,7 @@ struct PathMap: View {
|
|||
|
||||
if let dest = node.destination, !node.locked {
|
||||
NavigationLink(value: dest) { content }
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
} else {
|
||||
content.opacity(node.locked ? 0.55 : 1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ struct PracticeHome: View {
|
|||
|
||||
var body: some View {
|
||||
#if os(macOS)
|
||||
macHome
|
||||
macHome.softAppear()
|
||||
#else
|
||||
iosHome
|
||||
iosHome.softAppear()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +46,7 @@ struct PracticeHome: View {
|
|||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
ProgressView(value: pct)
|
||||
.animation(Motion.settle, value: pct)
|
||||
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
|
@ -91,7 +92,7 @@ struct PracticeHome: View {
|
|||
NavigationLink(value: dest) {
|
||||
subjectTileBody(s, open: true, progress: progress, part: part)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
} else {
|
||||
subjectTileBody(s, open: false, progress: 0, part: part)
|
||||
}
|
||||
|
|
@ -108,6 +109,7 @@ struct PracticeHome: View {
|
|||
Spacer(minLength: 0)
|
||||
if open && part.totalEx > 0 {
|
||||
ProgressView(value: progress)
|
||||
.animation(Motion.settle, value: progress)
|
||||
Text(vm.fmt("stats_ex_fmt", part.doneEx, part.totalEx))
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
|
@ -146,6 +148,7 @@ struct PracticeHome: View {
|
|||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
ProgressView(value: pct)
|
||||
.animation(Motion.settle, value: pct)
|
||||
Text(vm.fmt("stats_ex_fmt", sum.doneEx, sum.totalEx))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
|
|
@ -415,7 +418,7 @@ struct SettingsScreen: View {
|
|||
.frame(maxWidth: .infinity, minHeight: 64, alignment: .leading)
|
||||
.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))
|
||||
}
|
||||
ProgressView(value: pct)
|
||||
.animation(Motion.settle, value: pct)
|
||||
}
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
|
|
@ -505,6 +509,7 @@ struct StatsScreen: View {
|
|||
}
|
||||
if open && part.totalEx > 0 {
|
||||
ProgressView(value: Double(part.doneEx) / Double(part.totalEx))
|
||||
.animation(Motion.settle, value: part.doneEx)
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
|
|
@ -773,7 +778,7 @@ struct SearchScreen: View {
|
|||
.frame(maxWidth: .infinity, minHeight: 84, alignment: .leading)
|
||||
.softSurface(cornerRadius: 16)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
.disabled(hit.locked)
|
||||
}
|
||||
}
|
||||
|
|
@ -848,7 +853,7 @@ struct SearchScreen: View {
|
|||
Image(systemName: "xmark.circle.fill")
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
.accessibilityLabel("Clear")
|
||||
}
|
||||
}
|
||||
|
|
@ -1205,7 +1210,7 @@ struct NetYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
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"))
|
||||
.navigationSubtitle(vm.tr("net_years_sub"))
|
||||
|
|
@ -1246,7 +1251,7 @@ struct HwYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
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í"))
|
||||
.navigationSubtitle(vm.tr("hw_years_sub"))
|
||||
|
|
@ -1288,7 +1293,7 @@ struct DeHomeScreen: View {
|
|||
NavigationLink(value: rows[i].3) {
|
||||
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"))
|
||||
.navigationSubtitle(vm.tr("de_home_sub"))
|
||||
|
|
@ -1324,7 +1329,7 @@ struct EnYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
.navigationTitle(vm.tr("English"))
|
||||
.navigationSubtitle(vm.tr("en_years_sub"))
|
||||
|
|
@ -1358,7 +1363,7 @@ struct OnYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
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"))
|
||||
.navigationSubtitle(vm.tr("on_years_sub"))
|
||||
|
|
@ -1472,9 +1477,14 @@ struct SlidesScreen: View {
|
|||
#else
|
||||
if slides.indices.contains(idx) {
|
||||
slidePage(slides[idx])
|
||||
.id(idx)
|
||||
.transition(.opacity)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.animation(Motion.settle, value: idx)
|
||||
#endif
|
||||
.detailScreen(title, subtitle: subtitle)
|
||||
.glassBottomBar {
|
||||
VStack(spacing: 10) {
|
||||
|
|
@ -1485,7 +1495,7 @@ struct SlidesScreen: View {
|
|||
.frame(width: i == idx ? 18 : 6, height: 6)
|
||||
}
|
||||
}
|
||||
.animation(.default, value: idx)
|
||||
.animation(Motion.settle, value: idx)
|
||||
HStack(spacing: 10) {
|
||||
if idx > 0 {
|
||||
Button {
|
||||
|
|
@ -1668,7 +1678,7 @@ struct LitYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
.navigationTitle(vm.tr("Literatura"))
|
||||
.navigationSubtitle(vm.tr("lit_years_sub"))
|
||||
|
|
@ -1710,7 +1720,7 @@ struct MatYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
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"))
|
||||
.navigationSubtitle(vm.tr("mat_years_sub"))
|
||||
|
|
@ -1754,7 +1764,7 @@ struct FyzYearsScreen: View {
|
|||
NavigationLink(value: years[i].2) {
|
||||
MacLinkCard(title: vm.tr(years[i].0), subtitle: vm.tr(years[i].1), badge: "\(i + 1).")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
.navigationTitle(vm.tr("Fyzika"))
|
||||
.navigationSubtitle(vm.tr("fyz_years_sub"))
|
||||
|
|
@ -1790,12 +1800,12 @@ struct SciMapScreen: View {
|
|||
NavigationLink(value: Route.chemMap) {
|
||||
MacLinkCard(title: vm.tr("Chemie"), subtitle: vm.tr("chem_sub"), systemImage: "flask.fill")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
} else {
|
||||
NavigationLink(value: Route.bioMap) {
|
||||
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"))
|
||||
|
|
@ -1835,17 +1845,17 @@ struct CzechMapScreen: View {
|
|||
NavigationLink(value: Route.mluvnice) {
|
||||
MacLinkCard(title: vm.tr("Mluvnice"), systemImage: "textformat")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
case 2:
|
||||
NavigationLink(value: Route.readingList) {
|
||||
MacLinkCard(title: vm.tr("Maturitní četba"), systemImage: "books.vertical")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
default:
|
||||
NavigationLink(value: Route.litYears) {
|
||||
MacLinkCard(title: vm.tr("Literatura"), systemImage: "book.closed")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.tr("Český jazyk a literatura"))
|
||||
|
|
@ -1932,7 +1942,7 @@ struct BookListScreen: View {
|
|||
.frame(maxWidth: .infinity, minHeight: 168, alignment: .topLeading)
|
||||
.softSurface(cornerRadius: 18)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
}
|
||||
.padding(MacChrome.pageInset)
|
||||
|
|
@ -2070,7 +2080,7 @@ struct BookScreen: View {
|
|||
doneLabel: vm.tr("book_done")
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
if hasPlot {
|
||||
NavigationLink(value: Route.bookPlot(id)) {
|
||||
|
|
@ -2082,7 +2092,7 @@ struct BookScreen: View {
|
|||
doneLabel: vm.tr("book_done")
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
}
|
||||
#else
|
||||
|
|
@ -2096,7 +2106,7 @@ struct BookScreen: View {
|
|||
doneLabel: vm.tr("book_done")
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
if hasPlot {
|
||||
NavigationLink(value: Route.bookPlot(id)) {
|
||||
|
|
@ -2108,7 +2118,7 @@ struct BookScreen: View {
|
|||
doneLabel: vm.tr("book_done")
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.buttonStyle(PressScaleStyle())
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,8 +58,10 @@ struct MacRoot: View {
|
|||
if showsUpdateAccessory {
|
||||
UpdateBanner(vm: vm)
|
||||
.padding(16)
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.animation(Motion.settle, value: showsUpdateAccessory)
|
||||
.onAppear { vm.checkUpdate(false) }
|
||||
.frame(minWidth: 980, minHeight: 640)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1282,7 +1282,7 @@ static void on_stub_quiz_clicked(GtkButton *button, gpointer user_data) {
|
|||
(void)user_data;
|
||||
cetba_prepare_stub_quiz();
|
||||
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) {
|
||||
|
|
@ -1290,7 +1290,7 @@ static void on_stub_plot_clicked(GtkButton *button, gpointer user_data) {
|
|||
(void)user_data;
|
||||
cetba_prepare_stub_plot();
|
||||
if (main_stack)
|
||||
gtk_stack_set_visible_child_name(main_stack, "cetbastubdej");
|
||||
show_page("cetbastubdej", 1);
|
||||
}
|
||||
|
||||
static GtkWidget *book_rail;
|
||||
|
|
@ -1467,7 +1467,7 @@ static void on_book_node_clicked(GtkButton *button, gpointer user_data) {
|
|||
rebuild_stub_about(idx);
|
||||
}
|
||||
if (main_stack && target)
|
||||
gtk_stack_set_visible_child_name(main_stack, target);
|
||||
show_page(target, 1);
|
||||
}
|
||||
|
||||
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);
|
||||
rebuild_stub_about(idx);
|
||||
if (main_stack)
|
||||
gtk_stack_set_visible_child_name(main_stack, "cetbastub");
|
||||
show_page("cetbastub", 1);
|
||||
}
|
||||
|
||||
void cetba_register_search(void) {
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ static void en_open_unit(GtkButton *button, gpointer data) {
|
|||
if (L->next_btn)
|
||||
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) {
|
||||
|
|
@ -699,7 +699,7 @@ static void en_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
skill_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -460,7 +460,7 @@ static void fyz_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
fyz_slide_apply(L);
|
||||
} 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_rescale_lesson_notes(NetLesson *L, int body, int head, int kick);
|
||||
GtkWidget *make_back_button(const char *target);
|
||||
void show_page(const char *name, int motion);
|
||||
void on_nav_clicked(GtkButton *button, gpointer user_data);
|
||||
int unit_done_count(const UnitCtx *u);
|
||||
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++;
|
||||
hw_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -634,7 +634,7 @@ static void lit_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
lit_slide_apply(L);
|
||||
} 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");
|
||||
int w, h;
|
||||
|
||||
gtk_stack_set_transition_type(main_stack, GTK_STACK_TRANSITION_TYPE_NONE);
|
||||
if (start && *start)
|
||||
gtk_stack_set_visible_child_name(main_stack, start);
|
||||
else
|
||||
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)
|
||||
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)
|
||||
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) {
|
||||
|
|
@ -469,7 +469,7 @@ static void math_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
math_slide_apply(L);
|
||||
} else {
|
||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
||||
show_page(L->ex_page, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,17 +4,40 @@
|
|||
/* 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) {
|
||||
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;
|
||||
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)button;
|
||||
(void)user_data;
|
||||
gtk_stack_set_visible_child_name(main_stack, "subjects");
|
||||
show_page("subjects", 1);
|
||||
}
|
||||
|
||||
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_CALLBACK(gtk_widget_queue_draw), icon);
|
||||
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);
|
||||
return back;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ void net_open_unit(GtkButton *button, gpointer data) {
|
|||
return;
|
||||
L->idx = 0;
|
||||
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) {
|
||||
|
|
@ -220,7 +220,7 @@ void net_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
net_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -660,7 +660,7 @@ static void on_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
on_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -566,7 +566,7 @@ static void on2_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
on2_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -565,7 +565,7 @@ static void on3_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
on3_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -566,7 +566,7 @@ static void on4_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
on4_slide_apply(L);
|
||||
} 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)
|
||||
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) {
|
||||
|
|
@ -460,7 +460,7 @@ static void sci_slide_next(GtkButton *button, gpointer data) {
|
|||
L->idx++;
|
||||
sci_slide_apply(L);
|
||||
} 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++;
|
||||
skill_slide_apply(L);
|
||||
} 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;
|
||||
|
||||
static GtkWidget *search_layer;
|
||||
static GtkWidget *search_reveal;
|
||||
static gboolean search_want_open;
|
||||
static GtkWidget *search_entry;
|
||||
static GtkWidget *search_list;
|
||||
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))
|
||||
return;
|
||||
search_close();
|
||||
gtk_stack_set_visible_child_name(main_stack, target);
|
||||
show_page(target, 1);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
return search_layer && gtk_widget_get_visible(search_layer);
|
||||
return search_want_open;
|
||||
}
|
||||
|
||||
void search_close(void) {
|
||||
if (search_layer)
|
||||
gtk_widget_set_visible(search_layer, FALSE);
|
||||
search_want_open = 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) {
|
||||
if (!search_layer)
|
||||
if (!search_reveal)
|
||||
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) {
|
||||
gtk_editable_set_text(GTK_EDITABLE(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_set_hexpand(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);
|
||||
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_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;
|
||||
if (!target || !target[0])
|
||||
target = "subjects";
|
||||
gtk_stack_set_visible_child_name(main_stack, target);
|
||||
show_page(target, -1);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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) {
|
||||
|
|
|
|||
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);
|
||||
|
||||
if (up.banner) {
|
||||
gtk_widget_set_visible(up.banner, banner);
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(up.banner), banner);
|
||||
if (banner) {
|
||||
gtk_label_set_text(GTK_LABEL(up.banner_label), status);
|
||||
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)data;
|
||||
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. */
|
||||
|
|
@ -1030,7 +1030,6 @@ GtkWidget *build_update_banner(void) {
|
|||
GtkWidget *later = gtk_button_new();
|
||||
|
||||
gtk_widget_add_css_class(bar, "update-banner");
|
||||
gtk_widget_set_visible(bar, FALSE);
|
||||
|
||||
gtk_widget_set_hexpand(label, TRUE);
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(bar), later);
|
||||
|
||||
up.banner = bar;
|
||||
up.banner_label = label;
|
||||
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. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue