commit 3fa73e052ec2fb71d74b58e96fc6051822a34b0a Author: JGZ_YES Date: Tue Aug 11 21:11:24 2026 +0800 iOS版本初始化:睡了吗 SwiftUI 项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..266aef4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Xcode +*.xcuserstate +*.xcworkspace +xcuserdata/ +DerivedData/ +build/ +*.xcuserdata + +# CocoaPods +Pods/ + +# Carthage +Carthage/Build/ + +# Swift Package Manager +.build/ +*.xcodeproj/xcuserdata/ + +# Simulator +*.simulator + +# macOS +.DS_Store +Thumbs.db diff --git a/AreYouSleeping.xcodeproj/project.pbxproj b/AreYouSleeping.xcodeproj/project.pbxproj new file mode 100644 index 0000000..16ed6eb --- /dev/null +++ b/AreYouSleeping.xcodeproj/project.pbxproj @@ -0,0 +1,218 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 111111111111111111 /* AreYouSleepingApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222222; }; + 111111111111111112 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222223; }; + 111111111111111113 /* SleepRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222224; }; + 111111111111111114 /* TimeUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222225; }; + 111111111111111115 /* ApiClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222226; }; + 111111111111111116 /* BedtimeReminder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222227; }; + 111111111111111117 /* AgreementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222228; }; + 111111111111111118 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222229; }; + 111111111111111119 /* DashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222230; }; + 111111111111111120 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222231; }; + 111111111111111121 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222232; }; + 111111111111111122 /* StyleSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222233; }; + 111111111111111123 /* FriendControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 222222222222222234; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 333333333333333301 /* AreYouSleeping.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AreYouSleeping.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 222222222222222222 /* AreYouSleepingApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreYouSleepingApp.swift; sourceTree = ""; }; + 222222222222222223 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + 222222222222222224 /* SleepRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepRecord.swift; sourceTree = ""; }; + 222222222222222225 /* TimeUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeUtil.swift; sourceTree = ""; }; + 222222222222222226 /* ApiClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiClient.swift; sourceTree = ""; }; + 222222222222222227 /* BedtimeReminder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BedtimeReminder.swift; sourceTree = ""; }; + 222222222222222228 /* AgreementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgreementView.swift; sourceTree = ""; }; + 222222222222222229 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 222222222222222230 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = ""; }; + 222222222222222231 /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; + 222222222222222232 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + 222222222222222233 /* StyleSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleSettingsView.swift; sourceTree = ""; }; + 222222222222222234 /* FriendControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendControlView.swift; sourceTree = ""; }; + 222222222222222235 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 444444444444444401 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; + files = (); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 555555555555555501 = { + isa = PBXGroup; children = ( + 555555555555555502 /* AreYouSleeping */, + 555555555555555503 /* Products */, + ); + sourceTree = ""; + }; + 555555555555555502 /* AreYouSleeping */ = { + isa = PBXGroup; children = ( + 222222222222222222 /* AreYouSleepingApp.swift */, + 222222222222222223 /* Store.swift */, + 222222222222222235 /* Info.plist */, + 555555555555555504 /* Models */, + 555555555555555505 /* Utils */, + 555555555555555506 /* Services */, + 555555555555555507 /* Views */, + ); + path = AreYouSleeping; + sourceTree = ""; + }; + 555555555555555503 /* Products */ = { + isa = PBXGroup; children = (333333333333333301 /* AreYouSleeping.app */); + name = Products; sourceTree = ""; + }; + 555555555555555504 /* Models */ = { + isa = PBXGroup; children = (222222222222222224 /* SleepRecord.swift */); + path = Models; sourceTree = ""; + }; + 555555555555555505 /* Utils */ = { + isa = PBXGroup; children = (222222222222222225 /* TimeUtil.swift */); + path = Utils; sourceTree = ""; + }; + 555555555555555506 /* Services */ = { + isa = PBXGroup; children = ( + 222222222222222226 /* ApiClient.swift */, + 222222222222222227 /* BedtimeReminder.swift */, + ); + path = Services; sourceTree = ""; + }; + 555555555555555507 /* Views */ = { + isa = PBXGroup; children = ( + 222222222222222228 /* AgreementView.swift */, + 222222222222222229 /* ContentView.swift */, + 222222222222222230 /* DashboardView.swift */, + 222222222222222231 /* StatsView.swift */, + 222222222222222232 /* SettingsView.swift */, + 222222222222222233 /* StyleSettingsView.swift */, + 222222222222222234 /* FriendControlView.swift */, + ); + path = Views; sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 666666666666666601 /* AreYouSleeping */ = { + isa = PBXNativeTarget; + buildConfigurationList = 777777777777777701; + buildPhases = ( + 888888888888888801 /* Sources */, + 444444444444444401 /* Frameworks */, + ); + buildRules = (); + dependencies = (); + name = AreYouSleeping; + productName = AreYouSleeping; + productReference = 333333333333333301; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 999999999999999901 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + }; + buildConfigurationList = 777777777777777702; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = "zh-Hans"; + hasScannedForEncodings = 0; + knownRegions = ("zh-Hans", en, Base); + mainGroup = 555555555555555501; + productRefGroup = 555555555555555503; + projectDirPath = ""; + projectRoot = ""; + targets = (666666666666666601); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 888888888888888801 /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; + files = ( + 111111111111111111 /* AreYouSleepingApp.swift */, + 111111111111111112 /* Store.swift */, + 111111111111111113 /* SleepRecord.swift */, + 111111111111111114 /* TimeUtil.swift */, + 111111111111111115 /* ApiClient.swift */, + 111111111111111116 /* BedtimeReminder.swift */, + 111111111111111117 /* AgreementView.swift */, + 111111111111111118 /* ContentView.swift */, + 111111111111111119 /* DashboardView.swift */, + 111111111111111120 /* StatsView.swift */, + 111111111111111121 /* SettingsView.swift */, + 111111111111111122 /* StyleSettingsView.swift */, + 111111111111111123 /* FriendControlView.swift */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7A7A7A7A7A7A7A7A01 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = AreYouSleeping/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ausleep.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 7A7A7A7A7A7A7A7A02 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = AreYouSleeping/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.ausleep.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 777777777777777701 = { + isa = XCConfigurationList; + buildConfigurations = (7A7A7A7A7A7A7A7A01, 7A7A7A7A7A7A7A7A02); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 777777777777777702 = { + isa = XCConfigurationList; + buildConfigurations = (7A7A7A7A7A7A7A7A01, 7A7A7A7A7A7A7A7A02); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + + }; + rootObject = 999999999999999901; +} diff --git a/AreYouSleeping/AreYouSleepingApp.swift b/AreYouSleeping/AreYouSleepingApp.swift new file mode 100644 index 0000000..537ab07 --- /dev/null +++ b/AreYouSleeping/AreYouSleepingApp.swift @@ -0,0 +1,23 @@ +import SwiftUI + +@main +struct AreYouSleepingApp: App { + @StateObject private var store = Store.shared + + var body: some Scene { + WindowGroup { + if store.agreed { + ContentView() + .environmentObject(store) + .preferredColorScheme(store.darkMode ? .dark : .light) + .onAppear { + BedtimeReminder.requestPermission() + BedtimeReminder.schedule(store: store) + } + } else { + AgreementView() + .environmentObject(store) + } + } + } +} diff --git a/AreYouSleeping/Info.plist b/AreYouSleeping/Info.plist new file mode 100644 index 0000000..b7dc96d --- /dev/null +++ b/AreYouSleeping/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDevelopmentRegion + zh_CN + CFBundleDisplayName + 睡了吗 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + com.ausleep.ios + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + AreYouSleeping + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocalNetworkUsageDescription + 需要网络权限与服务器同步作息数据 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIColorName + AccentColor + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UIBackgroundModes + + fetch + processing + + BGTaskSchedulerPermittedIdentifiers + + com.ausleep.ios.refresh + + + diff --git a/AreYouSleeping/Models/SleepRecord.swift b/AreYouSleeping/Models/SleepRecord.swift new file mode 100644 index 0000000..aa9c8ee --- /dev/null +++ b/AreYouSleeping/Models/SleepRecord.swift @@ -0,0 +1,24 @@ +import Foundation + +/// 睡眠记录 +struct SleepRecord: Identifiable, Codable { + var id: String { nightDate } + let nightDate: String // "2026-08-10" + let bedMinutes: Int // 入睡分钟数 + let wakeMinutes: Int // 起床分钟数 + let sleepHours: Double // 睡眠时长 + + /// 序列化:nightDate|bedMinutes|wakeMinutes|sleepHours + func serialize() -> String { + String(format: "%@|%d|%d|%.2f", nightDate, bedMinutes, wakeMinutes, sleepHours) + } + + static func parse(_ line: String) -> SleepRecord? { + let parts = line.components(separatedBy: "|") + guard parts.count == 4, + let bed = Int(parts[1]), + let wake = Int(parts[2]), + let hours = Double(parts[3]) else { return nil } + return SleepRecord(nightDate: parts[0], bedMinutes: bed, wakeMinutes: wake, sleepHours: hours) + } +} diff --git a/AreYouSleeping/Services/ApiClient.swift b/AreYouSleeping/Services/ApiClient.swift new file mode 100644 index 0000000..fae53a3 --- /dev/null +++ b/AreYouSleeping/Services/ApiClient.swift @@ -0,0 +1,92 @@ +import Foundation + +/// HTTP 客户端:与 api.ausleep.parlz.com 通信 +enum ApiClient { + private static let base = "http://api.ausleep.parlz.com" + private static let decoder: JSONDecoder = { + let d = JSONDecoder(); d.keyDecodingStrategy = .convertFromSnakeCase; return d + }() + + struct ApiResult: Codable { + let ok: Bool + let error: String? + } + + /// 注册控制码 + static func bind(code: String, nickname: String) async -> ApiResult { + post("/register.php", body: ["code": code, "nickname": nickname]) + } + + /// 同步状态 + 拉取指令 + static func sync(code: String, body: [String: Any]) async -> ApiResult { + var b = body; b["code"] = code + return post("/sync.php", body: b) + } + + /// 查询朋友信息 + static func friendInfo(code: String) async -> [String: Any]? { + do { + let res = try await postDict("/friend_info.php", body: ["friend_code": code]) + return res + } catch { return nil } + } + + /// 朋友设置就寝时间 + static func friendSetBedtime(friendCode: String, bedtime: Int, waketime: Int) async -> ApiResult { + post("/friend_set_bedtime.php", body: ["friend_code": friendCode, "bedtime": bedtime, "waketime": waketime]) + } + + /// 朋友触发指令 (sleep/wake/message) + static func friendTrigger(friendCode: String, action: String, message: String = "") async -> ApiResult { + post("/friend_trigger.php", body: ["friend_code": friendCode, "action": action, "message": message]) + } + + /// 检查更新 + static func checkUpdate(versionCode: Int) async -> [String: Any]? { + do { + return try await postDict("/check_update.php", body: ["version_code": versionCode]) + } catch { return nil } + } + + // MARK: - 底层 HTTP + + private static func post(_ path: String, body: [String: Any]) -> ApiResult { + do { + let data = try postData(path, body: body) + return try decoder.decode(ApiResult.self, from: data) + } catch { + return ApiResult(ok: false, error: error.localizedDescription) + } + } + + private static func postDict(_ path: String, body: [String: Any]) async throws -> [String: Any] { + let data = try await postDataAsync(path, body: body) + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw URLError(.cannotParseResponse) + } + return json + } + + private static func postData(_ path: String, body: [String: Any]) throws -> Data { + var semaphoreResult: Result = .failure(URLError(.unknown)) + let semaphore = DispatchSemaphore(value: 0) + Task { + do { semaphoreResult = .success(try await postDataAsync(path, body: body)) } + catch { semaphoreResult = .failure(error) } + semaphore.signal() + } + semaphore.wait() + return try semaphoreResult.get() + } + + private static func postDataAsync(_ path: String, body: [String: Any]) async throws -> Data { + guard let url = URL(string: base + path) else { throw URLError(.badURL) } + var req = URLRequest(url: url) + req.httpMethod = "POST" + req.setValue("application/json", forHTTPHeaderField: "Content-Type") + req.timeoutInterval = 10 + req.httpBody = try JSONSerialization.data(withJSONObject: body) + let (data, _) = try await URLSession.shared.data(for: req) + return data + } +} diff --git a/AreYouSleeping/Services/BedtimeReminder.swift b/AreYouSleeping/Services/BedtimeReminder.swift new file mode 100644 index 0000000..fae03ac --- /dev/null +++ b/AreYouSleeping/Services/BedtimeReminder.swift @@ -0,0 +1,61 @@ +import UserNotifications + +/// 睡前 30 分钟提醒调度器(iOS 使用 UNUserNotificationCenter) +enum BedtimeReminder { + + static func schedule(store: Store) { + guard store.agreed, store.bedtimeReminderEnabled else { + cancel() + return + } + + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + guard settings.authorizationStatus == .authorized else { return } + + let triggerDate = calcNextTrigger(bedtimeMinutes: store.bedtimeMinutes) + let content = UNMutableNotificationContent() + content.title = "睡了吗 · 就寝提醒" + content.body = "还有 30 分钟到您的睡觉时间 \(TimeUtil.fmt(store.bedtimeMinutes)),准备休息啦~" + content.sound = .default + content.categoryIdentifier = "BEDTIME_REMINDER" + + let comps = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: triggerDate) + let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: false) + let request = UNNotificationRequest(identifier: "bedtime_reminder", + content: content, + trigger: trigger) + center.add(request) { error in + if let e = error { print("BedtimeReminder schedule error: \(e)") } + } + } + } + + static func cancel() { + UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ["bedtime_reminder"]) + } + + /// 计算下一次提醒时间(bedtime 前 30 分钟) + private static func calcNextTrigger(bedtimeMinutes: Int) -> Date { + let minutesBefore = 30 + var reminderMin = bedtimeMinutes - minutesBefore + if reminderMin < 0 { reminderMin += 24 * 60 } + + var cal = Calendar.current + var comps = cal.dateComponents([.year, .month, .day], from: Date()) + comps.hour = reminderMin / 60 + comps.minute = reminderMin % 60 + comps.second = 0 + + var candidate = cal.date(from: comps) ?? Date() + if candidate <= Date() { + candidate = cal.date(byAdding: .day, value: 1, to: candidate) ?? candidate + } + return candidate + } + + /// 请求通知权限 + static func requestPermission() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { _, _ in } + } +} diff --git a/AreYouSleeping/Store.swift b/AreYouSleeping/Store.swift new file mode 100644 index 0000000..8eeb2c7 --- /dev/null +++ b/AreYouSleeping/Store.swift @@ -0,0 +1,140 @@ +import Foundation + +/// 全局数据中心(基于 UserDefaults),等价于 Android SharedPreferences +@MainActor +final class Store: ObservableObject { + static let shared = Store() + + // MARK: - 设置项 + @Published var bedtimeMinutes: Int = 23 * 60 + @Published var waketimeMinutes: Int = 7 * 60 + @Published var supervisionEnabled: Bool = true + @Published var darkMode: Bool = false + @Published var leadMinutes: Int = 30 + @Published var nagMinutes: Int = 10 + @Published var navStyle: Int = 0 // 0=默认, 1=无边框 + @Published var fontScale: Double = 1.0 // 0.85~1.3 + @Published var fontStyle: Int = 0 // 0=默认, 1=圆体, 2=等宽 + @Published var cardRadius: Int = 18 // 4/18/28 + @Published var blurStrength: Int = 1 // 0=无, 1=轻, 2=强 + @Published var bedtimeReminderEnabled: Bool = true + @Published var backgroundRunning: Bool = true + @Published var controlCodeEnabled: Bool = true + + // MARK: - 控制码 & 账户 + @Published var controlCode: String = "" + @Published var nickname: String = "未命名" + @Published var agreed: Bool = false + + // MARK: - 运行状态 + @Published var isSleeping: Bool = false + @Published var sleepStartMillis: Double = 0 + @Published var lastNagMillis: Double = 0 + + // MARK: - 屏蔽应用包名(iOS 不支持实际屏蔽,仅记录) + @Published var blockedPackages: Set = [] + + // MARK: - 睡眠记录 + @Published var recordLines: Set = [] + + private let defaults = UserDefaults.standard + + private init() { load() } + + func load() { + let d = defaults + bedtimeMinutes = d.integer(forKey: "bed") + if bedtimeMinutes == 0 { bedtimeMinutes = 23 * 60 } + waketimeMinutes = d.integer(forKey: "wake") + if waketimeMinutes == 0 { waketimeMinutes = 7 * 60 } + supervisionEnabled = d.object(forKey: "sup") as? Bool ?? true + darkMode = d.bool(forKey: "dark") + leadMinutes = d.integer(forKey: "lead"); if leadMinutes == 0 { leadMinutes = 30 } + nagMinutes = d.integer(forKey: "nag"); if nagMinutes == 0 { nagMinutes = 10 } + navStyle = d.integer(forKey: "nav_style") + fontScale = d.double(forKey: "font_scale"); if fontScale == 0 { fontScale = 1.0 } + fontStyle = d.integer(forKey: "font_style") + cardRadius = d.integer(forKey: "card_radius"); if cardRadius == 0 { cardRadius = 18 } + blurStrength = d.integer(forKey: "blur_strength"); if blurStrength == 0 { blurStrength = 1 } + bedtimeReminderEnabled = d.object(forKey: "bed_reminder") as? Bool ?? true + backgroundRunning = d.object(forKey: "bg_run") as? Bool ?? true + controlCodeEnabled = d.object(forKey: "control_code_enabled") as? Bool ?? true + controlCode = d.string(forKey: "control_code") ?? "" + nickname = d.string(forKey: "nickname") ?? "未命名" + agreed = d.bool(forKey: "agreed") + isSleeping = d.bool(forKey: "sleeping") + sleepStartMillis = d.double(forKey: "start") + lastNagMillis = d.double(forKey: "lastNag") + blockedPackages = Set(d.stringArray(forKey: "blocked") ?? []) + recordLines = Set(d.stringArray(forKey: "records") ?? []) + } + + func save() { + let d = defaults + d.set(bedtimeMinutes, forKey: "bed") + d.set(waketimeMinutes, forKey: "wake") + d.set(supervisionEnabled, forKey: "sup") + d.set(darkMode, forKey: "dark") + d.set(leadMinutes, forKey: "lead") + d.set(nagMinutes, forKey: "nag") + d.set(navStyle, forKey: "nav_style") + d.set(fontScale, forKey: "font_scale") + d.set(fontStyle, forKey: "font_style") + d.set(cardRadius, forKey: "card_radius") + d.set(blurStrength, forKey: "blur_strength") + d.set(bedtimeReminderEnabled, forKey: "bed_reminder") + d.set(backgroundRunning, forKey: "bg_run") + d.set(controlCodeEnabled, forKey: "control_code_enabled") + d.set(controlCode, forKey: "control_code") + d.set(nickname, forKey: "nickname") + d.set(agreed, forKey: "agreed") + d.set(isSleeping, forKey: "sleeping") + d.set(sleepStartMillis, forKey: "start") + d.set(lastNagMillis, forKey: "lastNag") + d.set(Array(blockedPackages), forKey: "blocked") + d.set(Array(recordLines), forKey: "records") + } + + // MARK: - 控制码生成 + private static let codeChars = Array("ABCDEFGHJKLMNPQRSTUVWXYZ23456789") + + func ensureControlCode() { + guard controlCode.isEmpty else { return } + let raw = (0..<10).map { _ in + String(Store.codeChars[Int.random(in: 0.. 0 else { return } + let endMs = Date().timeIntervalSince1970 * 1000 + let hours = max(0, (endMs - sleepStartMillis) / 3_600_000) + let bedMin = TimeUtil.minuteOfDay(Date(timeIntervalSince1970: sleepStartMillis / 1000)) + let wakeMin = TimeUtil.minuteOfDay(Date(timeIntervalSince1970: endMs / 1000)) + let offset = bedMin < waketimeMinutes ? -1 : 0 + let night = TimeUtil.dateStr(Date(timeIntervalSince1970: sleepStartMillis / 1000), offsetDays: offset) + let rec = SleepRecord(nightDate: night, bedMinutes: bedMin, wakeMinutes: wakeMin, sleepHours: hours) + recordLines.removeAll { $0.hasPrefix(night + "|") } + recordLines.insert(rec.serialize()) + isSleeping = false + sleepStartMillis = 0 + save() + } + + func recentRecords(_ n: Int) -> [SleepRecord] { + recordLines + .compactMap(SleepRecord.parse) + .sorted { $0.nightDate > $1.nightDate } + .prefix(n) + .map { $0 } + } +} diff --git a/AreYouSleeping/Utils/TimeUtil.swift b/AreYouSleeping/Utils/TimeUtil.swift new file mode 100644 index 0000000..5f84429 --- /dev/null +++ b/AreYouSleeping/Utils/TimeUtil.swift @@ -0,0 +1,46 @@ +import Foundation + +/// 时间工具:统一用"自 00:00 起的分钟数" +enum TimeUtil { + /// 当前分钟数 + static func nowMinutes() -> Int { + let cal = Calendar.current + let h = cal.component(.hour, from: Date()) + let m = cal.component(.minute, from: Date()) + return h * 60 + m + } + + /// 格式化为 "HH:mm" + static func fmt(_ minutes: Int) -> String { + String(format: "%02d:%02d", minutes / 60, minutes % 60) + } + + /// 是否在睡眠窗口内(处理跨午夜) + static func inNightWindow(_ now: Int, bedtime: Int, waketime: Int) -> Bool { + if bedtime < waketime { + return now >= bedtime && now < waketime + } else { + return now >= bedtime || now < waketime + } + } + + /// 超出就寝时间的分钟数 + static func overdueMinutes(_ now: Int, bedtime: Int) -> Int { + inNightWindow(now, bedtime: bedtime, waketime: bedtime + 1) ? now - bedtime : 0 + } + + /// Date → 分钟数 + static func minuteOfDay(_ date: Date) -> Int { + let cal = Calendar.current + return cal.component(.hour, from: date) * 60 + cal.component(.minute, from: date) + } + + /// 日期字符串 "yyyy-MM-dd" + static func dateStr(_ date: Date, offsetDays: Int = 0) -> String { + let cal = Calendar.current + let d = cal.date(byAdding: .day, value: offsetDays, to: date) ?? date + let f = DateFormatter() + f.dateFormat = "yyyy-MM-dd" + return f.string(from: d) + } +} diff --git a/AreYouSleeping/Views/AgreementView.swift b/AreYouSleeping/Views/AgreementView.swift new file mode 100644 index 0000000..de92db2 --- /dev/null +++ b/AreYouSleeping/Views/AgreementView.swift @@ -0,0 +1,80 @@ +import SwiftUI + +struct AgreementView: View { + @EnvironmentObject var store: Store + + var body: some View { + VStack(spacing: 0) { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("用户协议与隐私说明") + .font(.title).bold() + + Text("欢迎使用「睡了吗」。\n\n本协议说明应用功能与数据使用方式,请仔细阅读。") + .font(.body).foregroundColor(.secondary) + + section("一、应用功能") { + Text("1. 睡眠监督:在您设定的就寝时段,提醒您按时入睡。\n(注:iOS 系统限制,本应用无法直接屏蔽其他应用,仅提供通知提醒。)\n2. 作息记录:记录您的入睡与起床时间,生成睡眠统计。\n3. 控制码功能(见第二条)。") + } + + section("二、控制码功能") { + Text("本应用会为您的手机生成一个独一无二的控制码。\n\n• 控制码绑定本机,卸载重装后才会变更。\n• 您可以将控制码分享给朋友。朋友输入您的控制码后可以远程设置您的就寝/起床时间、提醒您入睡/起床。\n• 您可以随时在设置中停止分享。\n• 请仅将控制码分享给您信任的人。") + } + + section("三、数据与隐私") { + Text("• 您的作息设置、睡眠记录会同步到服务器,以便朋友通过控制码查看和远程操作。\n• 服务器仅存储与功能相关的必要数据,不会出售或分享给第三方。\n• 您可以在应用设置中随时停止监督服务,断开与服务器同步。") + } + + section("四、所需权限") { + Text("• 通知:发送就寝提醒与朋友远程消息\n• 网络访问:与服务器同步作息数据、接收朋友远程指令") + } + + Spacer().frame(height: 16) + + VStack(spacing: 4) { + Text("解释权归 JGZ_YES 所有") + .font(.footnote).foregroundColor(.secondary) + Text("(c) 2026 JGZ_YES") + .font(.footnote).foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + } + .padding(24) + } + + // 底部按钮 + HStack(spacing: 12) { + Button("不同意") { + exit(0) + } + .buttonStyle(.bordered) + .tint(.secondary) + .controlSize(.large) + + Button("同意并继续") { + store.agreed = true + store.ensureControlCode() + store.save() + Task { + _ = await ApiClient.bind(code: store.controlCode, nickname: store.nickname) + } + } + .buttonStyle(.borderedProminent) + .tint(.blue) + .controlSize(.large) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background(.regularMaterial) + } + } + + @ViewBuilder + private func section(_ title: String, @ViewBuilder content: () -> some View) -> some View { + VStack(alignment: .leading, spacing: 8) { + Text(title).font(.headline) + content().font(.subheadline).foregroundColor(.secondary) + .lineSpacing(4) + } + } +} diff --git a/AreYouSleeping/Views/ContentView.swift b/AreYouSleeping/Views/ContentView.swift new file mode 100644 index 0000000..54da8cd --- /dev/null +++ b/AreYouSleeping/Views/ContentView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct ContentView: View { + @EnvironmentObject var store: Store + @State private var selectedTab = 0 + @State private var settingsRefresh = UUID() + + var body: some View { + TabView(selection: $selectedTab) { + DashboardView() + .tabItem { + Image(systemName: "house.fill") + Text("仪表盘") + } + .tag(0) + + StatsView() + .tabItem { + Image(systemName: "chart.bar.fill") + Text("统计") + } + .tag(1) + + SettingsView() + .tabItem { + Image(systemName: "gearshape.fill") + Text("设置") + } + .tag(2) + } + .tint(.blue) + .onAppear { + let appearance = UITabBarAppearance() + appearance.configureWithDefaultBackground() + UITabBar.appearance().scrollEdgeAppearance = appearance + } + } +} diff --git a/AreYouSleeping/Views/DashboardView.swift b/AreYouSleeping/Views/DashboardView.swift new file mode 100644 index 0000000..fd98a5a --- /dev/null +++ b/AreYouSleeping/Views/DashboardView.swift @@ -0,0 +1,134 @@ +import SwiftUI + +struct DashboardView: View { + @EnvironmentObject var store: Store + @State private var now = Date() + private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 20) { + + // 时钟 + 日期 + VStack(spacing: 4) { + Text(timeString) + .font(.system(size: 56, weight: .thin, design: .rounded)) + Text(dateString) + .font(.subheadline).foregroundColor(.secondary) + } + .padding(.top, 20) + + // 问候语 + Text(greeting) + .font(.title2) + + // 倒计时 + VStack(spacing: 8) { + Text(statusTitle) + .font(.headline) + Text(countdownText) + .font(.system(size: 28, weight: .medium)) + .foregroundColor(.blue) + } + .padding() + .frame(maxWidth: .infinity) + .background(.regularMaterial) + .clipShape(RoundedRectangle(cornerRadius: 16)) + + // 睡眠状态卡片 + HStack(spacing: 16) { + statusCard(title: "就寝时间", value: TimeUtil.fmt(store.bedtimeMinutes)) + statusCard(title: "起床时间", value: TimeUtil.fmt(store.waketimeMinutes)) + } + + // 监督状态 + HStack { + Image(systemName: store.supervisionEnabled ? "shield.checkered" : "shield.slash") + .foregroundColor(store.supervisionEnabled ? .green : .red) + Text(store.supervisionEnabled ? "监督运行中" : "监督已关闭") + .font(.subheadline) + } + .padding(.horizontal) + + // 入睡/起床按钮 + if store.isSleeping { + Button { + store.endSleep() + } label: { + Label("我起床了", systemImage: "sunrise.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .tint(.orange) + } else { + Button { + store.startSleep() + } label: { + Label("我要睡觉了", systemImage: "moon.zzz.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + .tint(.indigo) + } + } + .padding() + } + .navigationTitle("睡了吗") + .onReceive(timer) { t in now = t } + } + } + + private var timeString: String { + let f = DateFormatter(); f.dateFormat = "HH:mm"; return f.string(from: now) + } + + private var dateString: String { + let f = DateFormatter(); f.dateFormat = "yyyy年M月d日 EEEE"; f.locale = Locale(identifier: "zh_CN") + return f.string(from: now) + } + + private var greeting: String { + let h = Calendar.current.component(.hour, from: now) + switch h { + case 0..<6: return "夜深了,好好休息" + case 6..<12: return "早上好!新的一天开始了" + case 12..<18: return "下午好,注意休息" + default: return "晚上好,准备放松吧" + } + } + + private var statusTitle: String { + if store.isSleeping { return "正在睡眠中" } + let nowMin = TimeUtil.nowMinutes() + if TimeUtil.inNightWindow(nowMin, bedtime: store.bedtimeMinutes, waketime: store.waketimeMinutes) { + return "已到睡觉时间!" + } + return "距离睡觉还有" + } + + private var countdownText: String { + if store.isSleeping { + let elapsed = Int((Date().timeIntervalSince1970 * 1000 - store.sleepStartMillis) / 3_600_000) + return "已睡 \(elapsed) 小时" + } + let nowMin = TimeUtil.nowMinutes() + var remain = store.bedtimeMinutes - nowMin + if remain < 0 { remain += 24 * 60 } + return "\(remain / 60) 小时 \(remain % 60) 分钟" + } + + @ViewBuilder + private func statusCard(title: String, value: String) -> some View { + VStack(spacing: 4) { + Text(title).font(.caption).foregroundColor(.secondary) + Text(value).font(.title3).bold() + } + .frame(maxWidth: .infinity) + .padding() + .background(.regularMaterial) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} diff --git a/AreYouSleeping/Views/FriendControlView.swift b/AreYouSleeping/Views/FriendControlView.swift new file mode 100644 index 0000000..e02a934 --- /dev/null +++ b/AreYouSleeping/Views/FriendControlView.swift @@ -0,0 +1,67 @@ +import SwiftUI + +struct FriendControlView: View { + @EnvironmentObject var store: Store + @State private var friendCode = "" + @State private var friendInfo: String? + @State private var isLoading = false + @State private var errorMsg: String? + + var body: some View { + Form { + Section { + HStack { + TextField("输入朋友的控制码", text: $friendCode) + .textInputAutocapitalization(.characters) + .disableAutocorrection(true) + Button("查询") { lookupFriend() } + .buttonStyle(.borderedProminent) + .controlSize(.small) + .disabled(friendCode.isEmpty || isLoading) + } + } + + if let info = friendInfo { + Section("朋友信息") { + Text(info).font(.headline) + } + } + + if let error = errorMsg { + Section { Text(error).foregroundColor(.red) } + } + + if friendInfo != nil { + Section("操作") { + Button { triggerFriend("sleep") } label: { + Label("催朋友睡觉", systemImage: "moon.zzz.fill") + } + Button { triggerFriend("wake") } label: { + Label("叫朋友起床", systemImage: "sunrise.fill") + } + } + } + } + .navigationTitle("控制朋友") + .overlay { if isLoading { ProgressView() } } + } + + private func lookupFriend() { + isLoading = true; errorMsg = nil; friendInfo = nil + Task { + if let res = await ApiClient.friendInfo(code: friendCode) { + friendInfo = (res["nickname"] as? String) ?? friendCode + } else { + errorMsg = "未找到该用户" + } + isLoading = false + } + } + + private func triggerFriend(_ action: String) { + Task { + let res = await ApiClient.friendTrigger(friendCode: friendCode, action: action) + if !res.ok { errorMsg = res.error ?? "操作失败" } + } + } +} diff --git a/AreYouSleeping/Views/SettingsView.swift b/AreYouSleeping/Views/SettingsView.swift new file mode 100644 index 0000000..5f34e4a --- /dev/null +++ b/AreYouSleeping/Views/SettingsView.swift @@ -0,0 +1,145 @@ +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject var store: Store + @State private var showAgreement = false + @State private var showStyleSettings = false + + var body: some View { + NavigationStack { + Form { + // MARK: - 控制码 + Section { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("我的控制码").font(.caption).foregroundColor(.secondary) + Text(store.controlCode.isEmpty ? "----" : store.controlCode) + .font(.title3).bold().foregroundColor(.blue) + } + Spacer() + if !store.controlCode.isEmpty { + ShareLink(item: shareText) { + Image(systemName: "square.and.arrow.up") + } + } + } + Toggle("启用控制码功能", isOn: $store.controlCodeEnabled) + .onChange(of: store.controlCodeEnabled) { _ in store.save() } + } header: { Text("控制码") } + + // MARK: - 作息 + Section { + HStack { + Text("目标就寝时间") + Spacer() + Text(TimeUtil.fmt(store.bedtimeMinutes)).foregroundColor(.blue) + } + .contentShape(Rectangle()) + .onTapGesture { pickTime(isBed: true) } + + HStack { + Text("目标起床时间") + Spacer() + Text(TimeUtil.fmt(store.waketimeMinutes)).foregroundColor(.blue) + } + .contentShape(Rectangle()) + .onTapGesture { pickTime(isBed: false) } + } header: { Text("作息") } + + // MARK: - 提醒 + Section { + Toggle("睡前 30 分钟提醒", isOn: $store.bedtimeReminderEnabled) + .onChange(of: store.bedtimeReminderEnabled) { _ in + store.save() + BedtimeReminder.schedule(store: store) + } + } header: { Text("提醒") } footer: { + Text("睡觉时间前发送通知提醒你准备休息") + } + + // MARK: - 监督 + Section { + Toggle("监督模式", isOn: $store.supervisionEnabled) + .onChange(of: store.supervisionEnabled) { _ in store.save() } + } header: { Text("监督") } footer: { + Text("就寝时段提醒你放下手机") + } + + // MARK: - 后台运行 + Section { + Toggle("允许后台运行", isOn: $store.backgroundRunning) + .onChange(of: store.backgroundRunning) { _ in store.save() } + } header: { Text("后台运行") } footer: { + Text("关闭后应用切到后台将停止服务") + } + + // MARK: - 样式 + Section { + NavigationLink { + StyleSettingsView() + } label: { + Text("样式") + } + } header: { Text("外观") } + + // MARK: - 朋友 + Section { + NavigationLink("控制朋友") { + FriendControlView() + } + } header: { Text("朋友") } + + // MARK: - 其他 + Section { + Button("用户协议与隐私说明") { showAgreement = true } + } + } + .navigationTitle("设置") + .sheet(isPresented: $showAgreement) { + AgreementView() + } + } + } + + private var shareText: String { + let code = store.controlCode + let webLink = "http://api.ausleep.parlz.com/control?code=\(code)" + return """ + 我在用「睡了吗」监督睡眠,这是我的控制码:\(code) + 点击链接直接连接(需安装「睡了吗」): + \(webLink) + """ + } + + private func pickTime(isBed: Bool) { + let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene + guard let root = scene?.windows.first?.rootViewController else { return } + + let vc = UIViewController() + let picker = UIDatePicker() + picker.datePickerMode = .time + picker.preferredDatePickerStyle = .wheels + picker.frame = CGRect(x: 0, y: 50, width: 320, height: 216) + + let alert = UIAlertController(title: isBed ? "就寝时间" : "起床时间", message: nil, preferredStyle: .alert) + alert.view.addSubview(picker) + + alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in + let cal = Calendar.current + let h = cal.component(.hour, from: picker.date) + let m = cal.component(.minute, from: picker.date) + let v = h * 60 + m + if isBed { + store.bedtimeMinutes = v + BedtimeReminder.schedule(store: store) + } else { + store.waketimeMinutes = v + } + store.save() + }) + alert.addAction(UIAlertAction(title: "取消", style: .cancel)) + + alert.view.frame = CGRect(x: 0, y: 0, width: 320, height: 350) + root.present(alert, animated: true) + } +} diff --git a/AreYouSleeping/Views/StatsView.swift b/AreYouSleeping/Views/StatsView.swift new file mode 100644 index 0000000..8dea86c --- /dev/null +++ b/AreYouSleeping/Views/StatsView.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct StatsView: View { + @EnvironmentObject var store: Store + + private var records: [SleepRecord] { store.recentRecords(7) } + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 20) { + // KPI 卡片 + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) { + kpiCard(title: "平均睡眠", value: avgHours, unit: "小时", color: .blue) + kpiCard(title: "连续达标", value: "\(streakDays)", unit: "天", color: .green) + kpiCard(title: "准时入睡", value: "\(onTimeCount)/\(max(records.count, 1))", unit: "晚", color: .orange) + kpiCard(title: "达标率", value: String(format: "%.0f%%", goalRate * 100), unit: "", color: .purple) + } + + // 最近记录 + VStack(alignment: .leading, spacing: 8) { + Text("最近 7 晚").font(.headline).padding(.horizontal) + if records.isEmpty { + Text("暂无睡眠记录").foregroundColor(.secondary).padding() + } else { + ForEach(records) { rec in + recordRow(rec) + } + } + } + } + .padding() + } + .navigationTitle("统计") + } + } + + private var avgHours: String { + guard !records.isEmpty else { return "-" } + return String(format: "%.1f", records.map(\.sleepHours).reduce(0, +) / Double(records.count)) + } + + private var goalRate: Double { + guard !records.isEmpty else { return 0 } + return Double(records.filter { $0.sleepHours >= 7 }.count) / Double(records.count) + } + + private var streakDays: Int { + var count = 0 + for r in records { + if r.sleepHours >= 7 { count += 1 } else { break } + } + return count + } + + private var onTimeCount: Int { + records.filter { $0.bedMinutes <= store.bedtimeMinutes + 30 }.count + } + + @ViewBuilder + private func kpiCard(title: String, value: String, unit: String, color: Color) -> some View { + VStack(spacing: 4) { + Text(title).font(.caption).foregroundColor(.secondary) + HStack(alignment: .lastTextBaseline, spacing: 2) { + Text(value).font(.title2).bold().foregroundColor(color) + if !unit.isEmpty { Text(unit).font(.caption).foregroundColor(.secondary) } + } + } + .frame(maxWidth: .infinity) + .padding() + .background(.regularMaterial) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + + @ViewBuilder + private func recordRow(_ rec: SleepRecord) -> some View { + HStack { + VStack(alignment: .leading) { + Text(rec.nightDate).font(.subheadline).bold() + Text("\(TimeUtil.fmt(rec.bedMinutes)) → \(TimeUtil.fmt(rec.wakeMinutes))") + .font(.caption).foregroundColor(.secondary) + } + Spacer() + Text(String(format: "%.1f 小时", rec.sleepHours)) + .font(.headline).foregroundColor(rec.sleepHours >= 7 ? .green : .orange) + } + .padding() + .background(.regularMaterial) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } +} diff --git a/AreYouSleeping/Views/StyleSettingsView.swift b/AreYouSleeping/Views/StyleSettingsView.swift new file mode 100644 index 0000000..2b07b70 --- /dev/null +++ b/AreYouSleeping/Views/StyleSettingsView.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct StyleSettingsView: View { + @EnvironmentObject var store: Store + + var body: some View { + Form { + Section { + Toggle("暗色模式", isOn: $store.darkMode) + .onChange(of: store.darkMode) { _ in store.save() } + } + + Section { + Picker("导航栏样式", selection: $store.navStyle) { + Text("默认").tag(0) + Text("无边框").tag(1) + } + .pickerStyle(.segmented) + .onChange(of: store.navStyle) { _ in store.save() } + } header: { Text("导航栏") } + + Section { + Picker("字体风格", selection: $store.fontStyle) { + Text("默认").tag(0) + Text("圆体").tag(1) + Text("等宽").tag(2) + } + .pickerStyle(.segmented) + .onChange(of: store.fontStyle) { _ in store.save() } + + VStack { + HStack { + Text("字体大小: \(String(format: "%.0f%%", store.fontScale * 100))") + Spacer() + Button("重置") { store.fontScale = 1.0; store.save() } + .buttonStyle(.borderless) + } + Slider(value: $store.fontScale, in: 0.85...1.30, step: 0.05) + .onChange(of: store.fontScale) { _ in store.save() } + } + } header: { Text("字体") } + + Section { + Picker("卡片圆角", selection: $store.cardRadius) { + Text("小 (4)").tag(4) + Text("中 (18)").tag(18) + Text("大 (28)").tag(28) + } + .onChange(of: store.cardRadius) { _ in store.save() } + } header: { Text("卡片") } + + Section { + Picker("模糊强度", selection: $store.blurStrength) { + Text("无").tag(0) + Text("轻微").tag(1) + Text("强烈").tag(2) + } + .onChange(of: store.blurStrength) { _ in store.save() } + } header: { Text("模糊") } + } + .navigationTitle("样式设置") + } +} diff --git a/AreYouSleeping/project_config.json b/AreYouSleeping/project_config.json new file mode 100644 index 0000000..e60656e --- /dev/null +++ b/AreYouSleeping/project_config.json @@ -0,0 +1,23 @@ +{ + "sourceFiles": [ + "AreYouSleepingApp.swift", + "Store.swift", + "Models/SleepRecord.swift", + "Utils/TimeUtil.swift", + "Services/ApiClient.swift", + "Services/BedtimeReminder.swift", + "Views/AgreementView.swift", + "Views/ContentView.swift", + "Views/DashboardView.swift", + "Views/StatsView.swift", + "Views/SettingsView.swift", + "Views/StyleSettingsView.swift", + "Views/FriendControlView.swift" + ], + "info": { + "deploymentTarget": "iOS 16.0", + "bundleIdentifier": "com.ausleep.ios", + "displayName": "睡了吗", + "team": "" + } +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..dda4eea --- /dev/null +++ b/Package.swift @@ -0,0 +1,17 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "AreYouSleeping", + platforms: [.iOS(.v16)], + products: [ + .library(name: "AreYouSleeping", targets: ["AreYouSleeping"]), + ], + targets: [ + .target( + name: "AreYouSleeping", + path: "AreYouSleeping", + resources: [.process("Info.plist")] + ), + ] +)