- 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.
97 lines
6.0 KiB
Swift
97 lines
6.0 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|
||
}
|