feat: 添加云盘与消息中心功能

This commit is contained in:
2026-07-09 22:37:43 +08:00
parent 8e356973bd
commit f20ec7f06c
91 changed files with 5437 additions and 89 deletions

View File

@ -0,0 +1,301 @@
//
// MessageCenterModels.swift
// suixinkan
//
import Foundation
/// JSON `extra_data`
enum MessageJSONValue: Decodable, Hashable, Sendable {
case string(String)
case number(Double)
case bool(Bool)
case object([String: MessageJSONValue])
case array([MessageJSONValue])
case null
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
self = .null
} else if let value = try? container.decode(Bool.self) {
self = .bool(value)
} else if let value = try? container.decode(Double.self) {
self = .number(value)
} else if let value = try? container.decode(String.self) {
self = .string(value)
} else if let value = try? container.decode([MessageJSONValue].self) {
self = .array(value)
} else {
self = .object(try container.decode([String: MessageJSONValue].self))
}
}
}
/// Android `MsgResponse`
struct MessageListResponse: Decodable, Equatable, Sendable {
let hasMore: Bool
let lastId: Int
let items: [MessageItem]
let isRefresh: Bool
enum CodingKeys: String, CodingKey {
case hasMore = "has_more"
case lastId = "last_id"
case items
case isRefresh = "is_refresh"
}
init(hasMore: Bool = false, lastId: Int = 0, items: [MessageItem] = [], isRefresh: Bool = false) {
self.hasMore = hasMore
self.lastId = lastId
self.items = items
self.isRefresh = isRefresh
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
hasMore = try container.decodeLossyBool(forKey: .hasMore) ?? false
lastId = try container.decodeLossyInt(forKey: .lastId) ?? 0
items = (try? container.decodeIfPresent([MessageItem].self, forKey: .items)) ?? []
isRefresh = try container.decodeLossyBool(forKey: .isRefresh) ?? false
}
}
/// Android `MsgEntity`
struct MessageItem: Decodable, Hashable, Sendable {
let id: Int
let receiverId: Int
let receiverType: String
let type: Int
let typeName: String
let title: String
let content: String
let pushAt: String
let isRead: Bool
let readAt: String
let pushChannel: Int
let pushChannelName: String
let extraData: [String: MessageJSONValue]?
let createdAt: String
let updatedAt: String
enum CodingKeys: String, CodingKey {
case id
case receiverId = "receiver_id"
case receiverType = "receiver_type"
case type
case typeName = "type_name"
case title
case content
case pushAt = "push_at"
case isRead = "is_read"
case readAt = "read_at"
case pushChannel = "push_channel"
case pushChannelName = "push_channel_name"
case extraData = "extra_data"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
init(
id: Int = 0,
receiverId: Int = 0,
receiverType: String = "",
type: Int = 0,
typeName: String = "",
title: String = "",
content: String = "",
pushAt: String = "",
isRead: Bool = false,
readAt: String = "",
pushChannel: Int = 0,
pushChannelName: String = "",
extraData: [String: MessageJSONValue]? = nil,
createdAt: String = "",
updatedAt: String = ""
) {
self.id = id
self.receiverId = receiverId
self.receiverType = receiverType
self.type = type
self.typeName = typeName
self.title = title
self.content = content
self.pushAt = pushAt
self.isRead = isRead
self.readAt = readAt
self.pushChannel = pushChannel
self.pushChannelName = pushChannelName
self.extraData = extraData
self.createdAt = createdAt
self.updatedAt = updatedAt
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decodeLossyInt(forKey: .id) ?? 0
receiverId = try container.decodeLossyInt(forKey: .receiverId) ?? 0
receiverType = try container.decodeLossyString(forKey: .receiverType)
type = try container.decodeLossyInt(forKey: .type) ?? 0
typeName = try container.decodeLossyString(forKey: .typeName)
title = try container.decodeLossyString(forKey: .title)
content = try container.decodeLossyString(forKey: .content)
pushAt = try container.decodeLossyString(forKey: .pushAt)
isRead = try container.decodeLossyBool(forKey: .isRead) ?? false
readAt = try container.decodeLossyString(forKey: .readAt)
pushChannel = try container.decodeLossyInt(forKey: .pushChannel) ?? 0
pushChannelName = try container.decodeLossyString(forKey: .pushChannelName)
extraData = try? container.decodeIfPresent([String: MessageJSONValue].self, forKey: .extraData)
createdAt = try container.decodeLossyString(forKey: .createdAt)
updatedAt = try container.decodeLossyString(forKey: .updatedAt)
}
/// Android 使 `type_name`
var displayTitle: String {
typeName.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Android `push_at` `created_at`
var displayTime: String {
let pushTime = pushAt.trimmingCharacters(in: .whitespacesAndNewlines)
if !pushTime.isEmpty { return pushTime }
return MessageDateFormatter.formatDateTime(createdAt) ?? createdAt
}
///
func markedRead() -> MessageItem {
MessageItem(
id: id,
receiverId: receiverId,
receiverType: receiverType,
type: type,
typeName: typeName,
title: title,
content: content,
pushAt: pushAt,
isRead: true,
readAt: readAt,
pushChannel: pushChannel,
pushChannelName: pushChannelName,
extraData: extraData,
createdAt: createdAt,
updatedAt: updatedAt
)
}
}
/// Android `MsgDeleteRequest`
struct MessageDeleteRequest: Encodable, Sendable {
let id: Int
}
/// Android `DateUtils.formatDateTime`
enum MessageDateFormatter {
private static let calendar = Calendar.current
static func formatDateTime(_ input: String?) -> String? {
guard let input = input?.trimmingCharacters(in: .whitespacesAndNewlines), !input.isEmpty else { return nil }
guard let date = parseDate(input) else { return nil }
let today = Date()
let startOfInput = calendar.startOfDay(for: date)
let startOfToday = calendar.startOfDay(for: today)
let dayDifference = calendar.dateComponents([.day], from: startOfInput, to: startOfToday).day ?? 0
let inputComponents = calendar.dateComponents([.year, .month, .day, .weekday], from: date)
let todayComponents = calendar.dateComponents([.year, .month], from: today)
if dayDifference == 0 {
return "今天"
}
if (1 ... 6).contains(dayDifference), let weekday = inputComponents.weekday {
return weekdayName(weekday)
}
if inputComponents.year == todayComponents.year, inputComponents.month == todayComponents.month {
return "本月\(inputComponents.day ?? 0)"
}
if inputComponents.year == todayComponents.year {
return "\(inputComponents.month ?? 0)\(inputComponents.day ?? 0)"
}
return "\(inputComponents.year ?? 0)\(inputComponents.month ?? 0)\(inputComponents.day ?? 0)"
}
private static func parseDate(_ input: String) -> Date? {
if let timestamp = Double(input) {
let seconds = timestamp > 1_000_000_000_000 ? timestamp / 1000 : timestamp
return Date(timeIntervalSince1970: seconds)
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = .current
for format in ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd"] {
formatter.dateFormat = format
if let date = formatter.date(from: input) {
return date
}
}
return nil
}
private static func weekdayName(_ weekday: Int) -> String {
switch weekday {
case 1: return "周日"
case 2: return "周一"
case 3: return "周二"
case 4: return "周三"
case 5: return "周四"
case 6: return "周五"
case 7: return "周六"
default: return ""
}
}
}
private extension KeyedDecodingContainer {
func decodeLossyString(forKey key: Key) throws -> String {
if let value = try? decodeIfPresent(String.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return String(value)
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return String(value)
}
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value ? "true" : "false"
}
return ""
}
func decodeLossyInt(forKey key: Key) throws -> Int? {
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Double.self, forKey: key) {
return Int(value)
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines)
if let intValue = Int(text) { return intValue }
if let doubleValue = Double(text) { return Int(doubleValue) }
}
return nil
}
func decodeLossyBool(forKey key: Key) throws -> Bool? {
if let value = try? decodeIfPresent(Bool.self, forKey: key) {
return value
}
if let value = try? decodeIfPresent(Int.self, forKey: key) {
return value != 0
}
if let value = try? decodeIfPresent(String.self, forKey: key) {
let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if ["true", "1", "yes"].contains(text) { return true }
if ["false", "0", "no"].contains(text) { return false }
}
return nil
}
}