- Introduced `Dates.swift` for handling date and time in the university's timezone. - Created `Models.swift` to define data structures for timetable, lessons, and directory search results. - Implemented `Schedule.swift` for managing lesson slots and timetable functionalities. - Developed `Storage.swift` for user settings and data persistence. - Added `Widgets.swift` to create widgets displaying lessons and upcoming classes.
133 lines
5.3 KiB
Swift
133 lines
5.3 KiB
Swift
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 }
|
|
}
|
|
}
|