Files
psu-schedule/ios/Shared/Storage.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") }
}