- Parser 14 (server/places.js): "Занятия в Полоцке/Новополоцке/г. Новополоцк" over a day's first row is no longer a lesson but a note of the day for its column (`places`); lessons of that day get `place`. A lesson below the note in the same cell keeps its own subject.
- A town on its own line ("Новополоцк", "г.Новополоцк") or after a room ("246(Новополоцк)") becomes the lesson's `place` and leaves the subject and room. "История Полоцка" is not a town.
- API 14: `place` on lessons and `places` on days of /api/timetable, `place` in the directory. The day notes are shared by the server and the PWA (shared/places.js); the same note in every subgroup reads as the whole group's.
- Android, PWA and iOS: a "Занятия в Полоцке" card above the day's lessons (day pages, calendar, overview, week view, desk stand), "Полоцк" under the date in the week grid, "249Ж · Полоцк" on lesson cards, a "Город" row in details, the town in widgets, tile, reminders and change lists.
- Snapshot lines show the town and the day notes; tests for notes, towns in cells and the API.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
5.8 KiB
Swift
115 lines
5.8 KiB
Swift
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: ", "),
|
||
withPlace(row.lesson.rooms.map { "ауд. \($0)" }.joined(separator: ", "), row.lesson.place) ?? "", 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)
|
||
}
|
||
}
|