From 59d40c886a77b9a6d93cf4baf98a7a199a4580c0 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Thu, 8 Oct 2026 21:13:32 +0200 Subject: [PATCH] Add short transitions for navigation, answers and progress on every platform. Co-authored-by: Cursor --- .../org/maturita/maturita/AppViewModel.kt | 8 ++ .../org/maturita/maturita/MainActivity.kt | 36 ++++++- .../org/maturita/maturita/ui/Components.kt | 80 ++++++++++++--- .../org/maturita/maturita/ui/Exercises.kt | 7 +- .../java/org/maturita/maturita/ui/Screens.kt | 13 ++- data/style.css | 1 + ios/Graduately/AppModel.swift | 6 +- ios/Graduately/UI/Components.swift | 99 +++++++++++++++---- ios/Graduately/UI/Exercises.swift | 3 +- ios/Graduately/UI/ExercisesMore.swift | 7 +- ios/Graduately/UI/PathMap.swift | 3 +- ios/Graduately/UI/Screens.swift | 60 ++++++----- macos/Graduately/GraduatelyApp.swift | 2 + src/cetba.c | 8 +- src/english.c | 4 +- src/fyzika.c | 4 +- src/graduately.h | 1 + src/hw.c | 2 +- src/literatura.c | 4 +- src/main.c | 3 + src/matematika.c | 4 +- src/navigation.c | 28 +++++- src/net.c | 4 +- src/obcanka.c | 4 +- src/obcanka2.c | 4 +- src/obcanka3.c | 4 +- src/obcanka4.c | 4 +- src/priroda.c | 4 +- src/quizyear.c | 2 +- src/search.c | 49 +++++++-- src/stats.c | 4 +- src/update.c | 18 +++- 32 files changed, 359 insertions(+), 121 deletions(-) diff --git a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt index bb41144..333ae5d 100644 --- a/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt +++ b/android/app/src/main/java/org/maturita/maturita/AppViewModel.kt @@ -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.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) diff --git a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt index 94d8215..ad9279b 100644 --- a/android/app/src/main/java/org/maturita/maturita/MainActivity.kt +++ b/android/app/src/main/java/org/maturita/maturita/MainActivity.kt @@ -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) } + } } } diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Components.kt b/android/app/src/main/java/org/maturita/maturita/ui/Components.kt index b796249..6798891 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Components.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Components.kt @@ -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 - Text( - text, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(bottom = 8.dp), - ) + 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, diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt index e876e30..f47c316 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Exercises.kt @@ -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) diff --git a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt index 16a60ce..b43c346 100644 --- a/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt +++ b/android/app/src/main/java/org/maturita/maturita/ui/Screens.kt @@ -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)) { diff --git a/data/style.css b/data/style.css index 9b0ebbc..d36cde0 100644 --- a/data/style.css +++ b/data/style.css @@ -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; diff --git a/ios/Graduately/AppModel.swift b/ios/Graduately/AppModel.swift index ce12ef6..8d8eac2 100644 --- a/ios/Graduately/AppModel.swift +++ b/ios/Graduately/AppModel.swift @@ -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 } } } diff --git a/ios/Graduately/UI/Components.swift b/ios/Graduately/UI/Components.swift index ebd2ec8..9b79bfd 100644 --- a/ios/Graduately/UI/Components.swift +++ b/ios/Graduately/UI/Components.swift @@ -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,21 +307,27 @@ struct FeedbackLine: View { let text: String let kind: String let palette: Palette + @Environment(\.accessibilityReduceMotion) private var reduceMotion var body: some View { - if !text.isEmpty { - Label { - Text(text) - } icon: { - Image(systemName: kind == "ok" ? "checkmark.circle.fill" : kind == "warn" ? "exclamationmark.triangle.fill" : "xmark.circle.fill") + Group { + if !text.isEmpty { + Label { + Text(text) + } icon: { + Image(systemName: kind == "ok" ? "checkmark.circle.fill" : kind == "warn" ? "exclamationmark.triangle.fill" : "xmark.circle.fill") + } + #if os(macOS) + .font(.body.weight(.semibold)) + #else + .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))) } - #if os(macOS) - .font(.body.weight(.semibold)) - #else - .font(.subheadline.weight(.semibold)) - #endif - .foregroundStyle(kind == "ok" ? palette.success : kind == "warn" ? palette.warning : palette.error) } + .animation(reduceMotion ? nil : Motion.feedback, value: text) + .animation(reduceMotion ? nil : Motion.feedback, value: kind) } } @@ -288,17 +336,23 @@ struct Meaning: View { let palette: Palette let visible: Bool + @Environment(\.accessibilityReduceMotion) private var reduceMotion + var body: some View { - if visible && !text.isEmpty { - Text(text) - #if os(macOS) - .font(.body) - #else - .font(.footnote) - #endif - .foregroundStyle(.secondary) - .padding(.bottom, 4) + Group { + if visible && !text.isEmpty { + Text(text) + #if os(macOS) + .font(.body) + #else + .font(.footnote) + #endif + .foregroundStyle(.secondary) + .padding(.bottom, 4) + .transition(.opacity) + } } + .animation(reduceMotion ? nil : Motion.settle, value: visible) } } @@ -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 { diff --git a/ios/Graduately/UI/Exercises.swift b/ios/Graduately/UI/Exercises.swift index 11b2d80..e71f134 100644 --- a/ios/Graduately/UI/Exercises.swift +++ b/ios/Graduately/UI/Exercises.swift @@ -35,10 +35,11 @@ struct ExerciseScaffold: 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) } } diff --git a/ios/Graduately/UI/ExercisesMore.swift b/ios/Graduately/UI/ExercisesMore.swift index 8ec7aa3..012fe22 100644 --- a/ios/Graduately/UI/ExercisesMore.swift +++ b/ios/Graduately/UI/ExercisesMore.swift @@ -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) } } diff --git a/ios/Graduately/UI/PathMap.swift b/ios/Graduately/UI/PathMap.swift index 4335da9..3271b2e 100644 --- a/ios/Graduately/UI/PathMap.swift +++ b/ios/Graduately/UI/PathMap.swift @@ -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) } diff --git a/ios/Graduately/UI/Screens.swift b/ios/Graduately/UI/Screens.swift index bf9b2f5..3be5b90 100644 --- a/ios/Graduately/UI/Screens.swift +++ b/ios/Graduately/UI/Screens.swift @@ -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 } diff --git a/macos/Graduately/GraduatelyApp.swift b/macos/Graduately/GraduatelyApp.swift index 143b584..45a44b0 100644 --- a/macos/Graduately/GraduatelyApp.swift +++ b/macos/Graduately/GraduatelyApp.swift @@ -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) } diff --git a/src/cetba.c b/src/cetba.c index def5085..bb0cc07 100644 --- a/src/cetba.c +++ b/src/cetba.c @@ -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) { diff --git a/src/english.c b/src/english.c index 26c1f14..135cedd 100644 --- a/src/english.c +++ b/src/english.c @@ -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); } } diff --git a/src/fyzika.c b/src/fyzika.c index 05da2d4..76aa339 100644 --- a/src/fyzika.c +++ b/src/fyzika.c @@ -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); } } diff --git a/src/graduately.h b/src/graduately.h index d131c52..8bb95fe 100644 --- a/src/graduately.h +++ b/src/graduately.h @@ -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); diff --git a/src/hw.c b/src/hw.c index 2ed989b..24ee850 100644 --- a/src/hw.c +++ b/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); } } diff --git a/src/literatura.c b/src/literatura.c index 57784dd..b5d6e6c 100644 --- a/src/literatura.c +++ b/src/literatura.c @@ -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); } } diff --git a/src/main.c b/src/main.c index 4eec333..1c960f3 100644 --- a/src/main.c +++ b/src/main.c @@ -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); } diff --git a/src/matematika.c b/src/matematika.c index 0eb6831..14705ec 100644 --- a/src/matematika.c +++ b/src/matematika.c @@ -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); } } diff --git a/src/navigation.c b/src/navigation.c index 2be7922..59eaa74 100644 --- a/src/navigation.c +++ b/src/navigation.c @@ -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; } diff --git a/src/net.c b/src/net.c index 9276dc7..f970e4b 100644 --- a/src/net.c +++ b/src/net.c @@ -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); } } diff --git a/src/obcanka.c b/src/obcanka.c index 3563f91..1facaa1 100644 --- a/src/obcanka.c +++ b/src/obcanka.c @@ -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); } } diff --git a/src/obcanka2.c b/src/obcanka2.c index 45d760b..7f58525 100644 --- a/src/obcanka2.c +++ b/src/obcanka2.c @@ -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); } } diff --git a/src/obcanka3.c b/src/obcanka3.c index 72bc3cf..431460a 100644 --- a/src/obcanka3.c +++ b/src/obcanka3.c @@ -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); } } diff --git a/src/obcanka4.c b/src/obcanka4.c index efc4878..1005aea 100644 --- a/src/obcanka4.c +++ b/src/obcanka4.c @@ -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); } } diff --git a/src/priroda.c b/src/priroda.c index 9a1ef94..1078036 100644 --- a/src/priroda.c +++ b/src/priroda.c @@ -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); } } diff --git a/src/quizyear.c b/src/quizyear.c index 96a7dfc..98b8540 100644 --- a/src/quizyear.c +++ b/src/quizyear.c @@ -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); } } diff --git a/src/search.c b/src/search.c index b72cd54..7b3d149 100644 --- a/src/search.c +++ b/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); } diff --git a/src/stats.c b/src/stats.c index 188328d..566f85f 100644 --- a/src/stats.c +++ b/src/stats.c @@ -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) { diff --git a/src/update.c b/src/update.c index 1f75eb0..bf39b9e 100644 --- a/src/update.c +++ b/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. */