import SwiftUI /// Three sections and search: the calendar lives inside the plans, the subjects inside the overview. enum AppTab: Hashable { case home, schedule, plans, search } /// Where the screens send the student: another tab, a day of the schedule, the groups or settings. @MainActor @Observable final class Router { var tab: AppTab = .home var groups = false var settings = false /** A day or week the schedule should show next (from the overview, calendar, widgets). */ var focus: CivilDate? /** The schedule as a week grid instead of days; iPad starts with the grid. */ var weekView: Bool? /** The plans as a month calendar instead of a list. */ var plansCalendar = false /** What the search tab looks for: its field lives in the tab bar. */ var searchQuery = "" func openDay(_ date: CivilDate) { focus = date weekView = false tab = .schedule } func openWeek(_ date: CivilDate) { focus = date weekView = true tab = .schedule } } struct RootView: View { @Environment(AppModel.self) private var model // Decided once: picking the group mid-onboarding must not cut the remaining steps short. @State private var onboarding = Storage.loadSettings().current == nil @State private var router = Router() var body: some View { ZStack { if onboarding { OnboardingView { withAnimation(.smooth(duration: 0.5)) { onboarding = false } } .transition(.opacity.combined(with: .scale(scale: 1.04))) } else { MainTabs() .transition(.opacity) } } .environment(router) .onOpenURL { url in // A widget tapped: psuschedule://day/2026-10-19 opens that day. if url.host() == "day", let date = CivilDate(url.lastPathComponent) { router.openDay(date) } else if url.host() == "day" { router.openDay(model.today) } } .environment(\.palette, Palette(slots: model.timetable?.subjectSlots ?? [:], enabled: model.settings.display.colors)) .environment(\.display, model.settings.display) } } /// The sections in the Liquid Glass tab bar; on iPad they become a sidebar. struct MainTabs: View { @Environment(AppModel.self) private var model @Environment(Router.self) private var router var body: some View { @Bindable var router = router TabView(selection: $router.tab) { Tab("Обзор", systemImage: "house", value: AppTab.home) { HomeView() } Tab("Расписание", systemImage: "calendar.day.timeline.left", value: AppTab.schedule) { ScheduleView() } Tab("Планы", systemImage: "checklist", value: AppTab.plans) { PlansTab() } Tab("Поиск", systemImage: "magnifyingglass", value: AppTab.search, role: .search) { SearchView().searchable(text: $router.searchQuery, prompt: "Фамилия или номер аудитории") } } // Search stands apart from the three sections as its own glass button; tapping it turns // the button into the field above the keyboard, as in Apple's own apps. The field belongs // to the search tab only, so the other sections keep their bars clean. .tabViewSearchActivation(.searchTabSelection) .tabViewStyle(.sidebarAdaptable) .tabBarMinimizeBehavior(.onScrollDown) .sheet(isPresented: $router.groups) { NavigationStack { GroupsView(onboarding: false) { router.groups = false } } } .sheet(isPresented: $router.settings) { SettingsView() } } } /// The open group as a glass capsule in the toolbar: one tap from switching. struct GroupButton: View { @Environment(AppModel.self) private var model @Environment(Router.self) private var router var body: some View { let group = model.timetable?.group ?? model.settings.current?.group Button { router.groups = true } label: { Label(group ?? "Группа", systemImage: "person.2.fill").labelStyle(.titleAndIcon) } .accessibilityLabel(group.map { "Группа \($0), сменить" } ?? "Выбрать группу") } } /// The plans tab: the list of plans or the month calendar, switched from the toolbar. struct PlansTab: View { @Environment(Router.self) private var router var body: some View { ZStack { if router.plansCalendar { CalendarView().transition(.opacity) } else { PlansView().transition(.opacity) } } .animation(.smooth(duration: 0.3), value: router.plansCalendar) } } /// Switches the plans between the list and the calendar, as the schedule switches day and week. struct PlansModeButton: View { @Environment(Router.self) private var router var body: some View { Button(router.plansCalendar ? "Показать список" : "Показать календарь", systemImage: router.plansCalendar ? "list.bullet" : "calendar") { router.plansCalendar.toggle() } .contentTransition(.symbolEffect(.replace)) } } struct SettingsButton: View { @Environment(Router.self) private var router var body: some View { Button("Настройки", systemImage: "gearshape") { router.settings = true } } }