Files
edgeever/apps/ios/EdgeEver/Data/Models/Models.swift
T

604 lines
15 KiB
Swift

import Foundation
// MARK: - Auth
struct AuthUser: Codable, Equatable, Sendable, Identifiable {
var id: String
var username: String
var displayName: String?
var role: String
}
struct AuthSession: Codable, Equatable, Sendable {
var authRequired: Bool
var authenticated: Bool
var demoMode: Bool
var user: AuthUser?
var sessionToken: String?
}
struct LoginInput: Encodable, Sendable {
var username: String
var password: String
var deviceId: String
}
struct MobileSession: Codable, Equatable, Sendable {
var baseUrl: String
var token: String
var user: AuthUser?
}
struct InstanceStorageDiagnostics: Codable, Equatable, Sendable {
var database: String?
var resources: String?
}
struct InstanceHealth: Codable, Equatable, Sendable {
var ok: Bool
var name: String
var runtime: String?
var containerImageSource: String?
var authMode: String?
var build: String?
var deploymentVersionCreatedAt: String?
var migration: String?
var storage: InstanceStorageDiagnostics?
var objectStorageProvider: String?
}
struct InstanceRelease: Codable, Equatable, Sendable {
var version: String
}
struct LoginDeviceSession: Codable, Equatable, Sendable, Identifiable {
var id: String
var userAgent: String?
var label: String?
var ipAddress: String?
var ipCountry: String?
var ipRegion: String?
var isCurrent: Bool
var createdAt: String
var lastSeenAt: String
var expiresAt: String
}
// MARK: - Notebook / Memo
struct Notebook: Codable, Equatable, Sendable, Identifiable {
var id: String
var parentId: String?
var name: String
var slug: String?
var icon: String?
var color: String?
var sortOrder: Int
var memoCount: Int
var lastMemoUpdatedAt: String?
var createdAt: String
var updatedAt: String
}
struct MemoSummary: Codable, Equatable, Sendable, Identifiable {
var id: String
var notebookId: String
var title: String?
var excerpt: String
var tags: [String]
var isPinned: Bool
var isArchived: Bool
var isDeleted: Bool
var revision: Int
var createdAt: String
var updatedAt: String
var deletedAt: String?
}
struct MemoDetail: Codable, Equatable, Sendable, Identifiable {
var id: String
var notebookId: String
var title: String?
var excerpt: String
var tags: [String]
var isPinned: Bool
var isArchived: Bool
var isDeleted: Bool
var revision: Int
var createdAt: String
var updatedAt: String
var deletedAt: String?
var contentJson: JSONValue
var contentMarkdown: String
var contentText: String
var contentHash: String
var sourceMemoIds: [String]
var mergeSourceCount: Int
var mergedIntoMemoId: String?
var displayTitle: String {
let t = title?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return t.isEmpty ? "无标题笔记" : t
}
func asSummary() -> MemoSummary {
MemoSummary(
id: id,
notebookId: notebookId,
title: title,
excerpt: excerpt,
tags: tags,
isPinned: isPinned,
isArchived: isArchived,
isDeleted: isDeleted,
revision: revision,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt
)
}
}
struct MemoEditSession: Codable, Equatable, Sendable {
var id: String
var memoId: String
var baseRevision: Int
var baseContentHash: String
var expiresAt: String
}
// MARK: - AI note processing
enum AiAction: String, Codable, CaseIterable, Sendable, Identifiable {
case summarize
case extractKeyPoints = "extract-key-points"
case extractTodos = "extract-todos"
case rewriteProofread = "rewrite-proofread"
case translate
case improveWriting = "improve-writing"
case fixSpellingGrammar = "fix-spelling-grammar"
case makeShorter = "make-shorter"
case makeLonger = "make-longer"
case simplifyLanguage = "simplify-language"
case changeTone = "change-tone"
case continueWriting = "continue-writing"
case custom
var id: String { rawValue }
}
struct AiGenerateInput: Encodable, Sendable {
var action: AiAction
var promptId: String? = nil
var locale: String? = nil
var title: String
var contentMarkdown: String
var targetLanguage: String?
var tone: String? = nil
var instruction: String? = nil
}
struct AiGenerationResultBoundary: Decodable, Sendable {
var start: String
var end: String
}
struct AiPreparedGeneration: Decodable, Sendable {
var provider: String
var baseUrl: String
var apiKey: String
var modelId: String
var system: String
var prompt: String
var maxOutputTokens: Int
var resultBoundary: AiGenerationResultBoundary
}
struct AiPreparedTagSuggestions: Decodable, Sendable {
var provider: String
var baseUrl: String
var apiKey: String
var modelId: String
var system: String
var prompt: String
var maxOutputTokens: Int
var currentTags: [String]
var canonicalTags: [String: String]
}
struct AiTagSuggestionsInput: Encodable, Sendable {
var title: String
var contentMarkdown: String
var currentTags: [String]
var locale: String
}
struct AiTagSuggestion: Codable, Equatable, Sendable, Identifiable {
var name: String
var existing: Bool
var id: String { name }
}
struct AiTagSuggestionsResponse: Codable, Equatable, Sendable {
var suggestions: [AiTagSuggestion]
}
enum AiPromptParameterKind: String, Codable, Sendable {
case none
case targetLanguage = "target-language"
case tone
}
enum AiPromptResultMode: String, Codable, Sendable {
case append
case replace
case both
}
struct AiAssistantLastActionPreference: Codable, Equatable, Sendable {
var action: AiAction
var promptId: String?
var seedKey: String?
var targetLanguage: String?
var tone: String?
}
struct AiAssistantLastActionStore: Codable, Equatable, Sendable {
var selected: AiAssistantLastActionPreference? = nil
var wholeNote: AiAssistantLastActionPreference? = nil
}
struct AiPromptTemplate: Codable, Equatable, Sendable, Identifiable {
var id: String
var origin: String
var seedKey: String?
var action: AiAction
var parameterKind: AiPromptParameterKind
var resultMode: AiPromptResultMode
var nameCustomized: Bool
var descriptionCustomized: Bool
var instructionCustomized: Bool
var name: String
var description: String?
var instruction: String
var createdAt: String
var updatedAt: String
}
struct AiStreamEvent: Decodable, Sendable {
var type: String
var text: String?
var code: String?
var message: String?
var finishReason: String?
var inputTokens: Int?
var outputTokens: Int?
}
struct Resource: Codable, Equatable, Sendable, Identifiable {
var id: String
var memoId: String
var originalMemoId: String?
var kind: String
var mimeType: String?
var filename: String?
var byteSize: Int
var sha256: String?
var width: Int?
var height: Int?
var createdAt: String
var updatedAt: String
var url: String
}
struct ApiToken: Codable, Equatable, Sendable, Identifiable {
var id: String
var name: String
var token: String?
var scopes: [String]
var lastUsedAt: String?
var expiresAt: String?
var isRevoked: Bool
var createdAt: String
}
struct CreatedApiToken: Codable, Equatable, Sendable {
var token: String
var apiToken: ApiToken
}
struct TagSummary: Codable, Equatable, Sendable {
var name: String
var memoCount: Int
var updatedAt: String?
}
struct MemoRevision: Codable, Equatable, Sendable, Identifiable {
var id: String
var memoId: String
var revision: Int
var title: String?
var tags: [String]
var contentMarkdown: String
var contentText: String
var contentHash: String
var createdBy: String
var createdAt: String
}
struct MemoShare: Codable, Equatable, Sendable {
var memoId: String
var token: String
var createdAt: String
var updatedAt: String
var passwordProtected: Bool?
}
struct InstanceUser: Codable, Equatable, Sendable, Identifiable {
var id: String
var username: String
var displayName: String?
var role: String
var isDisabled: Bool
var lastLoginAt: String?
var createdAt: String
}
// MARK: - Sync wire types
struct MobileSyncBootstrapPage: Codable, Sendable {
var notebooks: [Notebook]
var memos: [MemoDetail]
var snapshotCursor: Int
var syncIdentity: String?
var totalCount: Int
var nextAfterId: String?
}
struct MobileSyncChange: Codable, Sendable {
var cursor: Int
var entityType: String
var entityId: String
var operation: String
var notebook: Notebook?
var memo: MemoDetail?
}
struct MobileSyncChangesPage: Codable, Sendable {
var changes: [MobileSyncChange]
var cursor: Int
var hasMore: Bool
var serverCursor: Int?
var syncIdentity: String?
}
// MARK: - Envelope helpers
struct MemoResponse: Codable, Sendable {
var memo: MemoDetail
}
struct NotebookResponse: Codable, Sendable {
var notebook: Notebook
}
struct NotebooksResponse: Codable, Sendable {
var notebooks: [Notebook]
}
struct EditSessionResponse: Codable, Sendable {
var editSession: MemoEditSession
}
struct ResourceResponse: Codable, Sendable {
var resource: Resource
}
struct OkResponse: Codable, Sendable {
var ok: Bool
}
struct SessionsResponse: Codable, Sendable {
var sessions: [LoginDeviceSession]
}
struct ApiTokensResponse: Codable, Sendable {
var apiTokens: [ApiToken]
var availableScopes: [String]
}
struct TagsResponse: Codable, Sendable {
var tags: [TagSummary]
}
struct AiPromptsResponse: Codable, Sendable {
var prompts: [AiPromptTemplate]
}
// MARK: - Outbox / drafts domain
enum MemoSortMode: String, CaseIterable, Sendable {
case updatedDesc = "updated-desc"
case createdDesc = "created-desc"
case titleAsc = "title-asc"
}
enum MemoFilterMode: String, CaseIterable, Sendable {
case all
case tagged
case untagged
case pinned
}
struct MemoCreatePayload: Codable, Equatable, Sendable {
var memoId: String
var title: String
var contentMarkdown: String
/// TipTap JSON when available (preserves image width attrs markdown drops).
var contentJson: String? = nil
var notebookId: String
var tags: [String]
var createdAt: String
}
struct MemoUpdatePayload: Codable, Equatable, Sendable {
var memoId: String
var expectedRevision: Int
var expectedContentHash: String
var title: String
var contentMarkdown: String
/// TipTap JSON when available (preserves image width attrs markdown drops).
var contentJson: String? = nil
var notebookId: String
var tags: [String]
}
enum OutboxKind: String, Codable, Sendable {
case memoCreate = "memo.create"
case memoUpdate = "memo.update"
}
enum OutboxStatus: String, Codable, Sendable {
case pending
case syncing
case conflict
case error
}
struct OutboxItem: Codable, Equatable, Sendable, Identifiable {
var id: String
var kind: OutboxKind
var memoId: String
var status: OutboxStatus
var payloadJSON: String
var attemptCount: Int
var lastError: String?
var nextAttemptAt: String?
var createdAt: String
var updatedAt: String
var version: Int
func createPayload() throws -> MemoCreatePayload {
try EdgeEverJSON.decoder.decode(MemoCreatePayload.self, from: Data(payloadJSON.utf8))
}
func updatePayload() throws -> MemoUpdatePayload {
try EdgeEverJSON.decoder.decode(MemoUpdatePayload.self, from: Data(payloadJSON.utf8))
}
}
struct MemoDraft: Codable, Equatable, Sendable {
var draftKey: String
var title: String
var contentMarkdown: String
var contentJson: String?
var notebookId: String
var tagsText: String
var expectedRevision: Int?
var updatedAt: String
}
struct SyncRunResult: Equatable, Sendable {
var attempted = 0
var synced = 0
var failed = 0
var conflicted = 0
}
struct BootstrapProgress: Equatable, Sendable {
var loadedCount: Int
var totalCount: Int
}
// MARK: - Flexible JSON for TipTap docs
enum JSONValue: Codable, Equatable, Sendable {
case string(String)
case number(Double)
case bool(Bool)
case object([String: JSONValue])
case array([JSONValue])
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([String: JSONValue].self) {
self = .object(value)
} else if let value = try? container.decode([JSONValue].self) {
self = .array(value)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON")
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let value): try container.encode(value)
case .number(let value): try container.encode(value)
case .bool(let value): try container.encode(value)
case .object(let value): try container.encode(value)
case .array(let value): try container.encode(value)
case .null: try container.encodeNil()
}
}
static var emptyDoc: JSONValue {
.object([
"type": .string("doc"),
"content": .array([.object(["type": .string("paragraph")])]),
])
}
func jsonString() throws -> String {
let data = try EdgeEverJSON.encoder.encode(self)
return String(data: data, encoding: .utf8) ?? "{}"
}
static func parse(_ string: String) throws -> JSONValue {
try EdgeEverJSON.decoder.decode(JSONValue.self, from: Data(string.utf8))
}
}
extension MemoDetail {
static func localPlaceholder(
id: String,
notebookId: String,
title: String,
contentMarkdown: String,
tags: [String],
createdAt: String
) -> MemoDetail {
MemoDetail(
id: id,
notebookId: notebookId,
title: title,
excerpt: String(contentMarkdown.prefix(160)),
tags: tags,
isPinned: false,
isArchived: false,
isDeleted: false,
revision: 0,
createdAt: createdAt,
updatedAt: createdAt,
deletedAt: nil,
contentJson: .emptyDoc,
contentMarkdown: contentMarkdown,
contentText: contentMarkdown,
contentHash: "",
sourceMemoIds: [],
mergeSourceCount: 0,
mergedIntoMemoId: nil
)
}
}