Files
psu-schedule/ios/PSUSchedule/Views/ChangesView.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

115 lines
5.8 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
extension ScheduleChanges: Identifiable {
var id: String { "\(document)|\(group)|\(at.timeIntervalSince1970)" }
}
/**
* The overview's card after the university republished the group's document: how many lessons
* changed, "Показать" for the list, and a close button. Both mark the record as seen.
*/
struct ChangesCard: View {
var record: ScheduleChanges
var onShow: () -> Void
var onClose: () -> Void
var body: some View {
HStack(alignment: .top, spacing: 14) {
Image(systemName: "arrow.triangle.2.circlepath.doc.on.clipboard")
.font(.body.weight(.semibold))
.foregroundStyle(Color.onBrand)
.frame(width: 40, height: 40)
.background(Color.brand, in: .circle)
.accessibilityHidden(true)
VStack(alignment: .leading, spacing: 4) {
Text("Расписание обновлено · \(changesLabel(record.count))").font(.headline)
Text("Для \(record.group)" + (fileDateLabel(record.to).map { " · файл от \($0)" } ?? ""))
.font(.subheadline).foregroundStyle(.secondary)
Button("Показать", action: onShow)
.buttonStyle(.glassProminent)
.padding(.top, 6)
}
.frame(maxWidth: .infinity, alignment: .leading)
Button("Скрыть сообщение об изменениях", systemImage: "xmark", action: onClose)
.labelStyle(.iconOnly)
.buttonStyle(.glass)
.buttonBorderShape(.circle)
}
.card(padding: 18)
}
}
/// The list behind the card: changed, added and removed lessons, as in the PWA.
struct ChangesSheet: View {
@Environment(\.dismiss) private var dismiss
var record: ScheduleChanges
var body: some View {
NavigationStack {
List {
Section {
Text("\(record.group) · \(record.name.replacingOccurrences(of: #"\.docx?$"#, with: "", options: [.regularExpression, .caseInsensitive]))"
+ (fileDateLabel(record.to).map { " · файл от \($0)" } ?? "")
+ ". Сравнение с прошлой версией, сохранённой на этом устройстве.")
.font(.subheadline).foregroundStyle(.secondary)
}
if !record.changed.isEmpty {
Section("Изменено · \(record.changed.count)") {
ForEach(record.changed, id: \.self) { change in
VStack(alignment: .leading, spacing: 4) {
when(change.after)
Text(change.after.lesson.subject).font(.headline)
ForEach(change.fields, id: \.self) { f in
(Text("\(f.label): ").foregroundStyle(.secondary)
+ Text(f.from).strikethrough().foregroundStyle(.secondary)
+ Text(" → ").foregroundStyle(.secondary)
+ Text(f.to))
.font(.subheadline)
.accessibilityLabel("\(f.label): было \(f.from), стало \(f.to)")
}
if change.fields.isEmpty {
Text("Изменился текст ячейки Word").font(.subheadline).foregroundStyle(.secondary)
}
}
.padding(.vertical, 2)
}
}
}
if !record.added.isEmpty {
Section("Добавлено · \(record.added.count)") {
ForEach(record.added, id: \.self) { row in
item(row, details: [row.lesson.type, row.lesson.teachers.joined(separator: ", "),
row.lesson.rooms.map { "ауд. \($0)" }.joined(separator: ", "), row.lesson.condition])
}
}
}
if !record.removed.isEmpty {
Section("Удалено · \(record.removed.count)") {
ForEach(record.removed, id: \.self) { row in
item(row, removed: true, details: [row.lesson.type, row.lesson.condition])
}
}
}
Section {} footer: { Text("Сверяйтесь с документом Word: при сомнениях он главнее.") }
}
.navigationTitle("Изменения")
.toolbar { ToolbarItem(placement: .confirmationAction) { Button("Готово", systemImage: "checkmark") { dismiss() } } }
}
}
private func when(_ row: ChangedLesson) -> some View {
let day = row.date.flatMap { CivilDate($0) }.map { "\($0.shortWeekday), \($0.longLabel)" } ?? dayName(row.day)
return Text(day + " · " + row.lesson.start).font(.subheadline.weight(.semibold)).foregroundStyle(Color.brand)
}
private func item(_ row: ChangedLesson, removed: Bool = false, details: [String]) -> some View {
VStack(alignment: .leading, spacing: 4) {
when(row).foregroundStyle(removed ? .secondary : Color.brand)
Text(row.lesson.subject).font(.headline).strikethrough(removed).foregroundStyle(removed ? .secondary : .primary)
let text = details.filter { !$0.trimmed.isEmpty }.joined(separator: " · ")
if !text.isEmpty { Text(text).font(.subheadline).foregroundStyle(.secondary) }
}
.padding(.vertical, 2)
}
}