Files
psu-schedule/ios/PSUSchedule/Views/SettingsView.swift
Zakhar Timoshenko f8984e72d9 feat: implement changes tracking for university documents
- Added a new data structure `ScheduleChanges` to represent changes when a university document is republished.
- Implemented methods to read and write changes to a local JSON file.
- Enhanced the `Repository` class to detect changes between the current and fresh timetable versions.
- Updated the `MainViewModel` to expose changes to the UI and handle marking changes as seen.
- Created UI components for displaying changes in both Home and Settings screens.
- Added support for changes in iOS views, including a new `ChangesCard` and `ChangesSheet`.
- Implemented logic to compare timetable versions and identify added, removed, and changed lessons.
2026-10-19 23:57:10 +03:00

97 lines
6.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct SettingsView: View {
@Environment(AppModel.self) private var model
@Environment(\.dismiss) private var dismiss
@State private var changes: ScheduleChanges?
var body: some View {
let s = model.settings
let d = s.display
NavigationStack {
Form {
Section("Тема") {
Picker("Тема", selection: Binding(get: { s.theme }, set: { model.setTheme($0) })) {
Text("Авто").tag(ThemeMode.system)
Text("Светлая").tag(ThemeMode.light)
Text("Тёмная").tag(ThemeMode.dark)
}
.pickerStyle(.segmented)
}
Section {
toggle("Цвета предметов", "Каждый предмет своим цветом в карточках, сетке и обзоре", d.colors) { var x = d; x.colors = $0; model.setDisplay(x) }
toggle("Преподаватели", "Фамилии в карточках занятий", d.teachers) { var x = d; x.teachers = $0; model.setDisplay(x) }
toggle("Аудитории", "Номера аудиторий в карточках и в сетке", d.rooms) { var x = d; x.rooms = $0; model.setDisplay(x) }
toggle("Номера пар", "«2 пара» рядом со временем", d.pairs) { var x = d; x.pairs = $0; model.setDisplay(x) }
toggle("Метки занятий", "Лекция или практика, белые и зелёные недели, общее занятие", d.tags) { var x = d; x.tags = $0; model.setDisplay(x) }
toggle("Пометки из Word", "Например, «3-13н.» — даты по ним уже рассчитаны", d.notes) { var x = d; x.notes = $0; model.setDisplay(x) }
} header: {
Text("Расписание")
} footer: {
Text("Полные сведения о занятии всегда открываются по нажатию на карточку.")
}
if !s.hidden.isEmpty {
Section("Скрытые занятия") {
ForEach(s.hidden.sorted(), id: \.self) { key in
let parts = key.split(separator: "|", maxSplits: 1).map(String.init)
HStack {
Text((parts.first ?? key).prefix(1).uppercased() + (parts.first ?? key).dropFirst() + (parts.count > 1 && !parts[1].isEmpty ? " · " + parts[1] : ""))
Spacer()
Button("Показать") { model.setHidden(key, false) }
}
}
}
}
Section {
Toggle(isOn: Binding(get: { s.reminders }, set: { on in
if on { Task { model.setReminders(await Reminders.authorize()) } } else { model.setReminders(false) }
})) {
VStack(alignment: .leading, spacing: 2) {
Text("Перед парой")
Text("Уведомление с предметом и аудиторией. Работает без интернета, по сохранённому расписанию.")
.font(.footnote).foregroundStyle(.secondary)
}
}
if s.reminders {
Picker("За сколько минут", selection: Binding(get: { s.reminderMinutes }, set: { model.setReminderMinutes($0) })) {
ForEach([5, 10, 15, 30], id: \.self) { Text("\($0)").tag($0) }
}
.pickerStyle(.segmented)
}
} header: {
Text("Напоминания")
}
Section {
LabeledContent("Документ", value: model.timetable?.document.name ?? "Группа не выбрана")
if let sem = model.timetable?.semester { LabeledContent("Семестр", value: sem.label) }
// The last republication of the document, also after its card was closed.
if let record = model.changes {
Button { changes = record } label: {
LabeledContent("Последние изменения", value: changesLabel(record.count) + " · " + CivilDate(record.at).longLabel)
.foregroundStyle(.primary)
}
}
LabeledContent("Сервер", value: model.api.base.absoluteString)
} header: {
Text("О расписании")
} footer: {
Text("Расписание берётся из публичного каталога ПГУ и пересчитывается по таблице белых и зелёных недель. Приложение неофициальное: при сомнениях сверяйтесь с документом Word.")
}
}
.navigationTitle("Настройки")
.navigationBarTitleDisplayMode(.inline)
.toolbar { ToolbarItem(placement: .confirmationAction) { Button("Готово", systemImage: "checkmark") { dismiss() } } }
.sheet(item: $changes) { ChangesSheet(record: $0).presentationDetents([.medium, .large]) }
}
}
private func toggle(_ title: String, _ text: String, _ on: Bool, _ set: @escaping (Bool) -> Void) -> some View {
Toggle(isOn: Binding(get: { on }, set: set)) {
VStack(alignment: .leading, spacing: 2) {
Text(title)
Text(text).font(.footnote).foregroundStyle(.secondary)
}
}
}
}