113 lines
4.5 KiB
Swift
113 lines
4.5 KiB
Swift
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
|
|
/** A notification when the background refresh finds changes in the group's document. */
|
|
var changeAlerts = true
|
|
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
|
|
changeAlerts = try c.decodeIfPresent(Bool.self, forKey: .changeAlerts) ?? true
|
|
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") }
|
|
}
|