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:
109
ios/Shared/Storage.swift
Normal file
109
ios/Shared/Storage.swift
Normal file
@@ -0,0 +1,109 @@
|
||||
import Foundation
|
||||
|
||||
enum ThemeMode: String, Codable, CaseIterable {
|
||||
case system, light, dark
|
||||
}
|
||||
|
||||
enum PlansLayout: String, Codable, CaseIterable {
|
||||
case agenda, list, board
|
||||
}
|
||||
|
||||
/** How the Plans screen lays out tasks, as in School Planner's plan settings. */
|
||||
struct PlansPrefs: Codable, Hashable {
|
||||
var layout: PlansLayout = .agenda
|
||||
/** The day's lessons above its plans; off by default, so plans come first. */
|
||||
var lessons: Bool = false
|
||||
/** Ticked-off tasks stay in the list. */
|
||||
var done: Bool = true
|
||||
}
|
||||
|
||||
/** What lesson cards, the week grid and the overview show; everything is on by default. */
|
||||
struct Display: Codable, Hashable {
|
||||
var colors = true
|
||||
var teachers = true
|
||||
var rooms = true
|
||||
var pairs = true
|
||||
var tags = true
|
||||
var notes = true
|
||||
}
|
||||
|
||||
/** Everything the student chose; stays on the device, shared with the widgets. */
|
||||
struct UserSettings: Codable, Hashable {
|
||||
var current: SavedGroup?
|
||||
/** The chosen subgroup per group ("docId|group"). */
|
||||
var subgroups: [String: String] = [:]
|
||||
var theme: ThemeMode = .system
|
||||
var reminders = false
|
||||
var reminderMinutes = 10
|
||||
var recent: [SavedGroup] = []
|
||||
var display = Display()
|
||||
/** Hidden lessons per group, as Lesson.hideKey values. */
|
||||
var hiddenByGroup: [String: [String]] = [:]
|
||||
var plans = PlansPrefs()
|
||||
|
||||
var subgroup: String? { current.flatMap { subgroups[$0.key] } }
|
||||
var hidden: Set<String> { Set(current.flatMap { hiddenByGroup[$0.key] } ?? []) }
|
||||
}
|
||||
|
||||
// Every field may be missing in an older save.
|
||||
extension UserSettings {
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
current = try c.decodeIfPresent(SavedGroup.self, forKey: .current)
|
||||
subgroups = try c.decodeIfPresent([String: String].self, forKey: .subgroups) ?? [:]
|
||||
theme = try c.decodeIfPresent(ThemeMode.self, forKey: .theme) ?? .system
|
||||
reminders = try c.decodeIfPresent(Bool.self, forKey: .reminders) ?? false
|
||||
reminderMinutes = try c.decodeIfPresent(Int.self, forKey: .reminderMinutes) ?? 10
|
||||
recent = try c.decodeIfPresent([SavedGroup].self, forKey: .recent) ?? []
|
||||
display = try c.decodeIfPresent(Display.self, forKey: .display) ?? Display()
|
||||
hiddenByGroup = try c.decodeIfPresent([String: [String]].self, forKey: .hiddenByGroup) ?? [:]
|
||||
plans = try c.decodeIfPresent(PlansPrefs.self, forKey: .plans) ?? PlansPrefs()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Files and settings in the app group container, so the widgets read the same timetable,
|
||||
* notes and choices as the app. Everything stays on the device.
|
||||
*/
|
||||
enum Storage {
|
||||
static let group = "group.org.xtimms.psu.schedule"
|
||||
|
||||
static var directory: URL {
|
||||
if let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group) { return url }
|
||||
// Without the app group (an unsigned build) the app still works on its own.
|
||||
let url = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
||||
try? FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
||||
return url
|
||||
}
|
||||
|
||||
static var defaults: UserDefaults { UserDefaults(suiteName: group) ?? .standard }
|
||||
|
||||
static let encoder = JSONEncoder()
|
||||
static let decoder = JSONDecoder()
|
||||
|
||||
static func read<T: Decodable>(_ type: T.Type, _ name: String) -> T? {
|
||||
guard let data = try? Data(contentsOf: directory.appendingPathComponent(name)) else { return nil }
|
||||
return try? decoder.decode(type, from: data)
|
||||
}
|
||||
|
||||
static func write<T: Encodable>(_ value: T, _ name: String) {
|
||||
guard let data = try? encoder.encode(value) else { return }
|
||||
try? data.write(to: directory.appendingPathComponent(name), options: .atomic)
|
||||
}
|
||||
|
||||
static func remove(_ name: String) {
|
||||
try? FileManager.default.removeItem(at: directory.appendingPathComponent(name))
|
||||
}
|
||||
|
||||
static func loadSettings() -> UserSettings {
|
||||
guard let data = defaults.data(forKey: "settings"),
|
||||
let s = try? decoder.decode(UserSettings.self, from: data) else { return UserSettings() }
|
||||
return s
|
||||
}
|
||||
|
||||
static func saveSettings(_ s: UserSettings) {
|
||||
if let data = try? encoder.encode(s) { defaults.set(data, forKey: "settings") }
|
||||
}
|
||||
|
||||
static func loadTimetable() -> Timetable? { read(Timetable.self, "timetable.json") }
|
||||
}
|
||||
Reference in New Issue
Block a user