Add date and time handling, models, schedule management, storage, and widget support
- 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.
This commit is contained in:
432
ios/Shared/Models.swift
Normal file
432
ios/Shared/Models.swift
Normal file
@@ -0,0 +1,432 @@
|
||||
import Foundation
|
||||
|
||||
// Shapes of the server JSON (see server/timetable.js and server/directory.js), as in the
|
||||
// Android client's Models.kt. Missing keys fall back to defaults, so an older or newer
|
||||
// server never breaks decoding.
|
||||
|
||||
struct Timetable: Codable, Hashable {
|
||||
var group: String
|
||||
var document: DocumentRef
|
||||
var period: String?
|
||||
var semester: Semester?
|
||||
var note: String?
|
||||
var subgroups: [String] = []
|
||||
var bells: [Bell] = []
|
||||
var from: String
|
||||
var days: [Day] = []
|
||||
var weekly: [WeeklyDay] = []
|
||||
var notes: [String] = []
|
||||
}
|
||||
|
||||
struct DocumentRef: Codable, Hashable {
|
||||
var id: String
|
||||
var name: String
|
||||
var faculty: String = ""
|
||||
var course: String = ""
|
||||
var modified: String?
|
||||
}
|
||||
|
||||
struct Semester: Codable, Hashable {
|
||||
var id: String
|
||||
var label: String
|
||||
var start: String
|
||||
var end: String
|
||||
}
|
||||
|
||||
struct Bell: Codable, Hashable {
|
||||
var pair: Int
|
||||
var start: String
|
||||
var end: String
|
||||
var halves: [[String]] = []
|
||||
}
|
||||
|
||||
struct Week: Codable, Hashable {
|
||||
var n: Int
|
||||
var color: String
|
||||
var white: Bool { color == "white" }
|
||||
}
|
||||
|
||||
struct Day: Codable, Hashable {
|
||||
var date: String
|
||||
var weekday: Int
|
||||
var week: Week?
|
||||
var lessons: [Lesson] = []
|
||||
}
|
||||
|
||||
struct WeeklyDay: Codable, Hashable {
|
||||
var day: Int
|
||||
var lessons: [Lesson] = []
|
||||
}
|
||||
|
||||
struct Lesson: Codable, Hashable {
|
||||
var key: String
|
||||
var start: String
|
||||
var end: String?
|
||||
var pair: Int?
|
||||
var subject: String
|
||||
var type: String = ""
|
||||
var teachers: [String] = []
|
||||
var rooms: [String] = []
|
||||
var roomMoved: Bool = false
|
||||
var distance: Bool = false
|
||||
var condition: String = ""
|
||||
var variant: String = "all"
|
||||
var subgroup: String?
|
||||
var groups: [String] = []
|
||||
var uncertain: Bool = false
|
||||
var raw: String = ""
|
||||
|
||||
/** A recurring lesson the student may hide: the subject with its kind, in every week. */
|
||||
var hideKey: String { subject.trimmed.lowercased() + "|" + type.trimmed.lowercased() }
|
||||
}
|
||||
|
||||
struct GroupsResponse: Codable {
|
||||
var results: [GroupResult] = []
|
||||
var facets: Facets = Facets()
|
||||
var total: Int = 0
|
||||
var year: String = ""
|
||||
}
|
||||
|
||||
struct GroupResult: Codable, Hashable, Identifiable {
|
||||
var group: String
|
||||
var fileId: String
|
||||
var file: String = ""
|
||||
var faculty: String = ""
|
||||
var course: String = ""
|
||||
var form: String = ""
|
||||
var period: String = ""
|
||||
var id: String { fileId + "|" + group }
|
||||
}
|
||||
|
||||
struct Facets: Codable {
|
||||
var faculties: [Facet] = []
|
||||
var courses: [Facet] = []
|
||||
var forms: [Facet] = []
|
||||
}
|
||||
|
||||
struct Facet: Codable, Hashable {
|
||||
var name: String
|
||||
var count: Int
|
||||
}
|
||||
|
||||
struct DirectorySearch: Codable {
|
||||
var teachers: [TeacherHit] = []
|
||||
var rooms: [RoomHit] = []
|
||||
}
|
||||
|
||||
struct TeacherHit: Codable, Hashable {
|
||||
var name: String
|
||||
var count: Int = 0
|
||||
var faculties: [String] = []
|
||||
}
|
||||
|
||||
struct RoomHit: Codable, Hashable {
|
||||
var room: String
|
||||
var count: Int = 0
|
||||
}
|
||||
|
||||
struct DirectoryDay: Codable {
|
||||
var date: String
|
||||
var lessons: [DirectoryLesson] = []
|
||||
var coverage: Coverage
|
||||
}
|
||||
|
||||
struct DirectoryLesson: Codable, Hashable {
|
||||
var start: String
|
||||
var end: String?
|
||||
var subject: String
|
||||
var type: String = ""
|
||||
var teachers: [String] = []
|
||||
var rooms: [String] = []
|
||||
var moved: Bool = false
|
||||
var distance: Bool = false
|
||||
var groups: [String] = []
|
||||
var condition: String = ""
|
||||
var uncertain: Bool = false
|
||||
}
|
||||
|
||||
struct Coverage: Codable {
|
||||
var documents: Int
|
||||
var total: Int
|
||||
var failed: [String] = []
|
||||
var withoutSemester: [String] = []
|
||||
var year: String = ""
|
||||
}
|
||||
|
||||
struct CalendarInfo: Codable {
|
||||
var bells: [Bell] = []
|
||||
}
|
||||
|
||||
/** An exam, credit or consultation of the group, from any document of the catalog (/api/exams). */
|
||||
struct Exam: Codable, Hashable {
|
||||
var date: String
|
||||
var start: String
|
||||
var end: String?
|
||||
var type: String
|
||||
var subject: String
|
||||
var teachers: [String] = []
|
||||
var rooms: [String] = []
|
||||
/** Consultations go into the list but not into the countdown. */
|
||||
var counts: Bool { type != "Консультация" }
|
||||
}
|
||||
|
||||
struct ExamsResponse: Codable {
|
||||
var group: String = ""
|
||||
var exams: [Exam] = []
|
||||
}
|
||||
|
||||
/** A group the student opened; kept on the device for quick switching. */
|
||||
struct SavedGroup: Codable, Hashable {
|
||||
var docId: String
|
||||
var group: String
|
||||
var faculty: String = ""
|
||||
var course: String = ""
|
||||
/** "docId|group": notes, marks and choices of one group never show up in another. */
|
||||
var key: String { docId + "|" + group }
|
||||
}
|
||||
|
||||
/**
|
||||
* A homework task or a plain note pinned to one lesson (subject, date, start time),
|
||||
* or a general plan with an empty subject. Tasks can be ticked off; notes just remind.
|
||||
*/
|
||||
struct Note: Codable, Hashable, Identifiable {
|
||||
var id: String = UUID().uuidString
|
||||
var group: String
|
||||
var subject: String
|
||||
var date: String
|
||||
var start: String
|
||||
var text: String
|
||||
var task: Bool = true
|
||||
var done: Bool = false
|
||||
var created: Double = Date().timeIntervalSince1970 * 1000
|
||||
|
||||
/** A plan not pinned to any lesson: just a day. */
|
||||
var general: Bool { subject.trimmed.isEmpty }
|
||||
}
|
||||
|
||||
/** Whether the student was at one lesson. */
|
||||
struct Mark: Codable, Hashable {
|
||||
var group: String
|
||||
var subject: String
|
||||
var date: String
|
||||
var start: String
|
||||
var present: Bool
|
||||
var id: String { [group, date, start, subject.trimmed.lowercased()].joined(separator: "|") }
|
||||
}
|
||||
|
||||
extension String {
|
||||
var trimmed: String { trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
}
|
||||
|
||||
// MARK: - Lenient decoding
|
||||
|
||||
/// Keys may be missing in older answers or caches: every optional field falls back to its default.
|
||||
extension Timetable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
group = try c.decode(String.self, forKey: .group)
|
||||
document = try c.decode(DocumentRef.self, forKey: .document)
|
||||
period = try c.decodeIfPresent(String.self, forKey: .period)
|
||||
semester = try c.decodeIfPresent(Semester.self, forKey: .semester)
|
||||
note = try c.decodeIfPresent(String.self, forKey: .note)
|
||||
subgroups = try c.decodeIfPresent([String].self, forKey: .subgroups) ?? []
|
||||
bells = try c.decodeIfPresent([Bell].self, forKey: .bells) ?? []
|
||||
from = try c.decodeIfPresent(String.self, forKey: .from) ?? ""
|
||||
days = try c.decodeIfPresent([Day].self, forKey: .days) ?? []
|
||||
weekly = try c.decodeIfPresent([WeeklyDay].self, forKey: .weekly) ?? []
|
||||
notes = try c.decodeIfPresent([String].self, forKey: .notes) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension DocumentRef {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
id = try c.decode(String.self, forKey: .id)
|
||||
name = try c.decodeIfPresent(String.self, forKey: .name) ?? ""
|
||||
faculty = try c.decodeIfPresent(String.self, forKey: .faculty) ?? ""
|
||||
course = try c.decodeIfPresent(String.self, forKey: .course) ?? ""
|
||||
modified = try c.decodeIfPresent(String.self, forKey: .modified)
|
||||
}
|
||||
}
|
||||
|
||||
extension Bell {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
pair = try c.decode(Int.self, forKey: .pair)
|
||||
start = try c.decode(String.self, forKey: .start)
|
||||
end = try c.decode(String.self, forKey: .end)
|
||||
halves = try c.decodeIfPresent([[String]].self, forKey: .halves) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension Day {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
date = try c.decode(String.self, forKey: .date)
|
||||
weekday = try c.decodeIfPresent(Int.self, forKey: .weekday) ?? 0
|
||||
week = try c.decodeIfPresent(Week.self, forKey: .week)
|
||||
lessons = try c.decodeIfPresent([Lesson].self, forKey: .lessons) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension WeeklyDay {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
day = try c.decode(Int.self, forKey: .day)
|
||||
lessons = try c.decodeIfPresent([Lesson].self, forKey: .lessons) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension Lesson {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
key = try c.decodeIfPresent(String.self, forKey: .key) ?? UUID().uuidString
|
||||
start = try c.decode(String.self, forKey: .start)
|
||||
end = try c.decodeIfPresent(String.self, forKey: .end)
|
||||
pair = try c.decodeIfPresent(Int.self, forKey: .pair)
|
||||
subject = try c.decode(String.self, forKey: .subject)
|
||||
type = try c.decodeIfPresent(String.self, forKey: .type) ?? ""
|
||||
teachers = try c.decodeIfPresent([String].self, forKey: .teachers) ?? []
|
||||
rooms = try c.decodeIfPresent([String].self, forKey: .rooms) ?? []
|
||||
roomMoved = try c.decodeIfPresent(Bool.self, forKey: .roomMoved) ?? false
|
||||
distance = try c.decodeIfPresent(Bool.self, forKey: .distance) ?? false
|
||||
condition = try c.decodeIfPresent(String.self, forKey: .condition) ?? ""
|
||||
variant = try c.decodeIfPresent(String.self, forKey: .variant) ?? "all"
|
||||
subgroup = try c.decodeIfPresent(String.self, forKey: .subgroup)
|
||||
groups = try c.decodeIfPresent([String].self, forKey: .groups) ?? []
|
||||
uncertain = try c.decodeIfPresent(Bool.self, forKey: .uncertain) ?? false
|
||||
raw = try c.decodeIfPresent(String.self, forKey: .raw) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupsResponse {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
results = try c.decodeIfPresent([GroupResult].self, forKey: .results) ?? []
|
||||
facets = try c.decodeIfPresent(Facets.self, forKey: .facets) ?? Facets()
|
||||
total = try c.decodeIfPresent(Int.self, forKey: .total) ?? 0
|
||||
year = try c.decodeIfPresent(String.self, forKey: .year) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupResult {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
group = try c.decode(String.self, forKey: .group)
|
||||
fileId = try c.decode(String.self, forKey: .fileId)
|
||||
file = try c.decodeIfPresent(String.self, forKey: .file) ?? ""
|
||||
faculty = try c.decodeIfPresent(String.self, forKey: .faculty) ?? ""
|
||||
course = try c.decodeIfPresent(String.self, forKey: .course) ?? ""
|
||||
form = try c.decodeIfPresent(String.self, forKey: .form) ?? ""
|
||||
period = try c.decodeIfPresent(String.self, forKey: .period) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension Facets {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
faculties = try c.decodeIfPresent([Facet].self, forKey: .faculties) ?? []
|
||||
courses = try c.decodeIfPresent([Facet].self, forKey: .courses) ?? []
|
||||
forms = try c.decodeIfPresent([Facet].self, forKey: .forms) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension DirectorySearch {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
teachers = try c.decodeIfPresent([TeacherHit].self, forKey: .teachers) ?? []
|
||||
rooms = try c.decodeIfPresent([RoomHit].self, forKey: .rooms) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension TeacherHit {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try c.decode(String.self, forKey: .name)
|
||||
count = try c.decodeIfPresent(Int.self, forKey: .count) ?? 0
|
||||
faculties = try c.decodeIfPresent([String].self, forKey: .faculties) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomHit {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
room = try c.decode(String.self, forKey: .room)
|
||||
count = try c.decodeIfPresent(Int.self, forKey: .count) ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
extension DirectoryDay {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
date = try c.decode(String.self, forKey: .date)
|
||||
lessons = try c.decodeIfPresent([DirectoryLesson].self, forKey: .lessons) ?? []
|
||||
coverage = try c.decode(Coverage.self, forKey: .coverage)
|
||||
}
|
||||
}
|
||||
|
||||
extension DirectoryLesson {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
start = try c.decode(String.self, forKey: .start)
|
||||
end = try c.decodeIfPresent(String.self, forKey: .end)
|
||||
subject = try c.decode(String.self, forKey: .subject)
|
||||
type = try c.decodeIfPresent(String.self, forKey: .type) ?? ""
|
||||
teachers = try c.decodeIfPresent([String].self, forKey: .teachers) ?? []
|
||||
rooms = try c.decodeIfPresent([String].self, forKey: .rooms) ?? []
|
||||
moved = try c.decodeIfPresent(Bool.self, forKey: .moved) ?? false
|
||||
distance = try c.decodeIfPresent(Bool.self, forKey: .distance) ?? false
|
||||
groups = try c.decodeIfPresent([String].self, forKey: .groups) ?? []
|
||||
condition = try c.decodeIfPresent(String.self, forKey: .condition) ?? ""
|
||||
uncertain = try c.decodeIfPresent(Bool.self, forKey: .uncertain) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
extension Coverage {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
documents = try c.decodeIfPresent(Int.self, forKey: .documents) ?? 0
|
||||
total = try c.decodeIfPresent(Int.self, forKey: .total) ?? 0
|
||||
failed = try c.decodeIfPresent([String].self, forKey: .failed) ?? []
|
||||
withoutSemester = try c.decodeIfPresent([String].self, forKey: .withoutSemester) ?? []
|
||||
year = try c.decodeIfPresent(String.self, forKey: .year) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension CalendarInfo {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
bells = try c.decodeIfPresent([Bell].self, forKey: .bells) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension Exam {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
date = try c.decode(String.self, forKey: .date)
|
||||
start = try c.decodeIfPresent(String.self, forKey: .start) ?? ""
|
||||
end = try c.decodeIfPresent(String.self, forKey: .end)
|
||||
type = try c.decodeIfPresent(String.self, forKey: .type) ?? ""
|
||||
subject = try c.decode(String.self, forKey: .subject)
|
||||
teachers = try c.decodeIfPresent([String].self, forKey: .teachers) ?? []
|
||||
rooms = try c.decodeIfPresent([String].self, forKey: .rooms) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension ExamsResponse {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
group = try c.decodeIfPresent(String.self, forKey: .group) ?? ""
|
||||
exams = try c.decodeIfPresent([Exam].self, forKey: .exams) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
extension SavedGroup {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
docId = try c.decode(String.self, forKey: .docId)
|
||||
group = try c.decode(String.self, forKey: .group)
|
||||
faculty = try c.decodeIfPresent(String.self, forKey: .faculty) ?? ""
|
||||
course = try c.decodeIfPresent(String.self, forKey: .course) ?? ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user