Add tag support to Weblog entries

This commit is contained in:
Otavio Cordeiro 2025-12-21 17:10:14 +01:00 committed by Otávio
parent d840caa026
commit 1392ad0cc0
29 changed files with 361 additions and 33 deletions

View file

@ -5,7 +5,7 @@ public extension String {
/// Creates a weblog entry body with frontmatter from the string content.
///
/// This method formats the string as a weblog entry by adding frontmatter
/// with the specified publication date and status. The resulting format follows the
/// with the specified publication date, status, and tags. The resulting format follows the
/// OMG.LOL weblog API requirements with ISO 8601 date formatting.
///
/// The output format is:
@ -13,6 +13,7 @@ public extension String {
/// ---
/// Date: YYYY-MM-DD HH:MM
/// Status: [status value]
/// Tags: Tag1, Tag2, Tag3
/// ---
///
/// [string content]
@ -22,20 +23,27 @@ public extension String {
/// - date: The publication date to include in the frontmatter
/// - status: The publication status to include in the frontmatter (e.g., "Draft", "Live", "Feed Only", "Web
/// Only", "Unlisted")
/// - tags: An array of tags to include in the frontmatter. Tags are comma-separated.
/// - Returns: UTF-8 encoded data containing the formatted weblog entry body
func weblogEntryBody(
date: Date,
status: String
status: String,
tags: [String]
) -> Data {
let formattedString = DateFormatter.iso8601WithShortTime.string(from: date)
let body = """
var frontmatter = """
---
Date: \(formattedString)
Status: \(status)
---
\(self)
"""
return body.data(using: .utf8) ?? Data()
if !tags.isEmpty {
let tagsString = tags.joined(separator: ", ")
frontmatter += "\nTags: \(tagsString)"
}
frontmatter += "\n---\n\n\(self)"
return frontmatter.data(using: .utf8) ?? Data()
}
}

View file

@ -105,17 +105,20 @@ public enum WeblogRequestFactory {
/// - address: The user address (username) to create the entry for
/// - content: The markdown content body of the weblog entry
/// - status: The publication status of the entry (e.g., "Draft", "Live", "Feed Only", "Web Only", "Unlisted")
/// - tags: An array of tags associated with the entry
/// - date: The publication date for the entry
/// - Returns: A configured network request for creating a weblog entry
public static func makeCreateWeblogEntryRequest(
address: String,
content: String,
status: String,
tags: [String],
date: Date
) -> NetworkRequest<Data, CreateOrUpdateWeblogEntryResponse> {
let body = content.weblogEntryBody(
date: date,
status: status
status: status,
tags: tags
)
return .init(
@ -152,6 +155,7 @@ public enum WeblogRequestFactory {
/// - content: The updated markdown content body of the weblog entry
/// - status: The updated publication status of the entry (e.g., "Draft", "Live", "Feed Only", "Web Only",
/// "Unlisted")
/// - tags: An array of tags associated with the entry
/// - date: The updated publication date for the entry
/// - Returns: A configured network request for updating the weblog entry
public static func makeUpdateWeblogEntryRequest(
@ -159,11 +163,13 @@ public enum WeblogRequestFactory {
entryID: String,
content: String,
status: String,
tags: [String],
date: Date
) -> NetworkRequest<Data, CreateOrUpdateWeblogEntryResponse> {
let body = content.weblogEntryBody(
date: date,
status: status
status: status,
tags: tags
)
return .init(

View file

@ -1,3 +1,5 @@
import Foundation
public struct WeblogEntryResponse: Decodable, Identifiable, Sendable {
// MARK: - Nested types
@ -30,4 +32,23 @@ public struct WeblogEntryResponse: Decodable, Identifiable, Sendable {
public let body: String
public let output: String
public let metadata: String
/// Extracts tags from the metadata JSON string.
///
/// The metadata field contains a JSON string with entry metadata which might include tags.
/// Tags are stored as a dictionary where keys are lowercase slugs and values
/// preserve the original case (e.g., `{"tags":{"foo":"Foo","bar":"Bar"}}`).
///
/// - Returns: An array of tag strings, preserving original case. Returns empty array if parsing fails or no tags
/// exist.
public var tags: [String] {
guard let data = metadata.data(using: .utf8),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let tagsDict = json["tags"] as? [String: String]
else {
return []
}
return .init(tagsDict.values).sorted()
}
}

View file

@ -26,7 +26,7 @@ struct EditPictureView: View {
VStack {
makeEditorView()
}
.frame(minWidth: 400, idealWidth: 640, maxWidth: 640)
.frame(minWidth: 640, idealWidth: 640, maxWidth: 800)
.toolbar {
makeToolbarContent()
}

View file

@ -26,7 +26,7 @@ struct UploadView: View {
var body: some View {
makeContentView()
.frame(minWidth: 400, idealWidth: 640, maxWidth: 640)
.frame(minWidth: 640, idealWidth: 640, maxWidth: 800)
.toolbar {
makeToolbarContent()
}

View file

@ -33,6 +33,9 @@ public struct EditWeblogEntry: Codable, Hashable {
/// The publication status of the weblog entry.
public let status: String?
/// An array of tags associated with the weblog entry.
public let tags: [String]
// MARK: - Lifecycle
public init(
@ -40,12 +43,14 @@ public struct EditWeblogEntry: Codable, Hashable {
body: String,
date: Date,
entryID: String?,
status: String?
status: String?,
tags: [String]
) {
self.address = address
self.body = body
self.date = date
self.entryID = entryID
self.status = status
self.tags = tags
}
}

View file

@ -41,6 +41,7 @@ final class ViewModelFactory: Sendable {
timestamp: entry.date,
address: entry.address,
location: entry.location,
tags: entry.tags ?? [],
repository: container.resolve(),
clipboardService: container.resolve()
)
@ -64,7 +65,8 @@ final class ViewModelFactory: Sendable {
body: String,
date: Date,
entryID: String?,
status: WeblogEntryStatus
status: WeblogEntryStatus,
tags: [String]
) -> EditorViewModel {
.init(
address: address,
@ -72,6 +74,7 @@ final class ViewModelFactory: Sendable {
date: date,
entryID: entryID,
status: status,
tags: tags,
repository: container.resolve()
)
}

View file

@ -14,6 +14,7 @@
date: .init(),
entryID: nil,
status: .draft,
tags: .init(),
repository: WeblogRepositoryMother.makeWeblogRepository()
)
}

View file

@ -16,6 +16,7 @@
timestamp: 12_312_312,
address: "otaviocc",
location: "/2022/12/my-weblog-post",
tags: .init(),
repository: WeblogRepositoryMother.makeWeblogRepository(),
clipboardService: ClipboardServiceMother.makeClipboardService()
)

View file

@ -24,6 +24,7 @@
address: String,
content: String,
status: String,
tags: [String],
date: Date
) async throws -> EntryResponse {
EntryResponseMother.makeEntryResponse()
@ -34,6 +35,7 @@
entryID: String,
content: String,
status: String,
tags: [String],
date: Date
) async throws -> EntryResponse {
EntryResponseMother.makeEntryResponse()

View file

@ -39,14 +39,16 @@
// MARK: - Public
func fetchEntries() async throws {}
func deleteEntry(address: String, entryID: String) async throws {}
func createOrUpdateEntry(
address: String,
entryID: String?,
body: String,
status: String,
tags: [String],
date: Date
) async throws {}
func deleteEntry(address: String, entryID: String) async throws {}
}
// MARK: - Public

View file

@ -28,6 +28,7 @@ struct EditWeblogEntryScene: Scene {
date: entry?.date ?? .init(),
entryID: entry?.entryID,
status: entry?.status.flatMap(WeblogEntryStatus.init) ?? .draft,
tags: entry?.tags ?? .init(),
address: entry?.address ?? ""
)
}
@ -46,6 +47,7 @@ struct EditWeblogEntryScene: Scene {
date: Date,
entryID: String?,
status: WeblogEntryStatus,
tags: [String],
address: String
) -> some View {
let viewModel = environment.viewModelFactory
@ -54,12 +56,14 @@ struct EditWeblogEntryScene: Scene {
body: body,
date: date,
entryID: entryID,
status: status
status: status,
tags: tags
)
EditorView(
viewModel: viewModel
)
.frame(minWidth: 420, idealWidth: 420, maxWidth: 640)
.modelContainer(environment.modelContainer)
.frame(minWidth: 640, idealWidth: 640, maxWidth: 800)
}
}

View file

@ -89,7 +89,8 @@ struct WeblogApp: View {
body: "# Title of your post\n\nThis is the body of your post...",
date: .init(),
entryID: nil,
status: nil
status: nil,
tags: .init()
)
)
} label: {

View file

@ -1,13 +1,16 @@
import DesignSystem
import FoundationExtensions
import SwiftData
import SwiftUI
import WeblogPersistenceService
struct EditorView: View {
// MARK: - Properties
@State private var viewModel: EditorViewModel
@State private var isPopoverPresented = false
@Environment(\.dismiss) private var dismiss
@Query(WeblogTag.fetchDescriptor()) private var existingTags: [WeblogTag]
// MARK: - Lifecycle
@ -20,10 +23,14 @@ struct EditorView: View {
// MARK: - Public
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack(alignment: .top) {
makeComposeView()
makeSidebarView()
}
makeTagsView()
}
.toolbar {
makeToolbarContent()
}
@ -48,8 +55,8 @@ struct EditorView: View {
@ViewBuilder
private func makeSidebarView() -> some View {
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) {
GridRow {
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 12) {
GridRow(alignment: .firstTextBaseline) {
Text("Date")
.gridColumnAlignment(.trailing)
@ -60,7 +67,7 @@ struct EditorView: View {
.help("Select publication date")
}
GridRow {
GridRow(alignment: .firstTextBaseline) {
Text("Time")
.gridColumnAlignment(.trailing)
@ -71,7 +78,7 @@ struct EditorView: View {
.help("Select publication time")
}
GridRow {
GridRow(alignment: .firstTextBaseline) {
Text("Status")
.gridColumnAlignment(.trailing)
@ -87,10 +94,82 @@ struct EditorView: View {
.pickerStyle(.radioGroup)
.labelsHidden()
}
GridRow(alignment: .firstTextBaseline) {
Text("Tags")
.gridColumnAlignment(.trailing)
VStack {
makeTagInputView()
makeTagSuggestionsView()
makeTagInputDescription()
}
}
}
.frame(width: 200)
.padding()
}
@ViewBuilder
private func makeTagsView() -> some View {
makeSelectedTagsView()
}
@ViewBuilder
private func makeTagInputView() -> some View {
TextField("Add tag", text: $viewModel.tagInput)
.autocorrectionDisabled(true)
.font(.body.monospaced())
.textFieldCard()
.help("Enter a tag and press the return key to add it")
.onSubmit {
withAnimation {
viewModel.addTag(viewModel.tagInput)
}
}
.onChange(of: viewModel.tagInput) {
viewModel.updateTagSuggestions(from: existingTags.map(\.title))
}
}
@ViewBuilder
private func makeTagSuggestionsView() -> some View {
if !viewModel.suggestedTags.isEmpty {
TagListView(
tags: viewModel.suggestedTags,
helpText: { "Add existing tag '\($0)'" }
) { tag in
withAnimation {
viewModel.addTag(tag)
}
}
}
}
@ViewBuilder
private func makeTagInputDescription() -> some View {
if viewModel.suggestedTags.isEmpty {
Text("Type a tag and press Return")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
@ViewBuilder
private func makeSelectedTagsView() -> some View {
if !viewModel.tags.isEmpty {
TagListView(
tags: viewModel.tags,
style: .remove,
helpText: { "Remove tag '\($0)'" }
) { tag in
withAnimation {
viewModel.removeTag(tag)
}
}
}
}
@ViewBuilder
private func makePublishToolbarItem() -> some View {
Button {

View file

@ -1,4 +1,5 @@
import Foundation
import FoundationExtensions
import Observation
import WeblogRepository
@ -12,7 +13,10 @@ final class EditorViewModel {
var entryID: String?
var date: Date
var status: WeblogEntryStatus
var tagInput = ""
var shouldDismiss = false
private(set) var tags: [String] = []
private(set) var suggestedTags: [String] = []
private(set) var isSubmitting = false
private let address: String
@ -40,6 +44,7 @@ final class EditorViewModel {
date: Date,
entryID: String?,
status: WeblogEntryStatus,
tags: [String],
repository: any WeblogRepositoryProtocol
) {
self.address = address
@ -47,6 +52,7 @@ final class EditorViewModel {
self.date = date
self.entryID = entryID
self.status = status
self.tags = tags
self.repository = repository
}
@ -64,6 +70,7 @@ final class EditorViewModel {
entryID: entryID,
body: body,
status: status.rawValue,
tags: tags,
date: date
)
shouldDismiss = true
@ -72,4 +79,40 @@ final class EditorViewModel {
}
}
}
func updateTagSuggestions(
from existingTags: [String]
) {
let trimmedInput = tagInput
.slugified()
.lowercased()
guard !trimmedInput.isEmpty else {
suggestedTags = []
return
}
suggestedTags = existingTags
.filter { tag in
tag.lowercased().contains(trimmedInput) && !tags.contains(tag)
}
.prefix(5)
.map(\.self)
}
func addTag(_ tag: String) {
let trimmedTag = tag.slugified()
guard !trimmedTag.isEmpty, !tags.contains(trimmedTag) else {
tagInput = ""
return
}
tags.append(trimmedTag)
tagInput = ""
}
func removeTag(_ tag: String) {
tags.removeAll { $0 == tag }
}
}

View file

@ -172,7 +172,8 @@ struct WeblogEntryView: View {
body: viewModel.body,
date: viewModel.publishedDate,
entryID: viewModel.id,
status: viewModel.status
status: viewModel.status,
tags: viewModel.tags
)
)
}

View file

@ -15,6 +15,7 @@ final class WeblogEntryViewModel: Identifiable {
let timestamp: Double
let address: String
let location: String
let tags: [String]
var publishedDate: Date {
Date(timeIntervalSince1970: timestamp)
@ -51,6 +52,7 @@ final class WeblogEntryViewModel: Identifiable {
timestamp: Double,
address: String,
location: String,
tags: [String],
repository: any WeblogRepositoryProtocol,
clipboardService: ClipboardServiceProtocol
) {
@ -61,6 +63,7 @@ final class WeblogEntryViewModel: Identifiable {
self.timestamp = timestamp
self.address = address
self.location = location
self.tags = tags
self.repository = repository
self.clipboardService = clipboardService
}

View file

@ -52,6 +52,14 @@ public struct EntryResponse: Identifiable, Equatable, Sendable {
/// multi-address support within the same application instance.
public let address: String
/// An array of tags associated with the weblog entry for categorization and discovery.
///
/// These user-defined tags enable content organization, filtering, and search
/// functionality. Tags can be used to group related entries or identify
/// specific themes, events, or characteristics of the content. The array may
/// be empty if no tags have been assigned to the entry.
public let tags: [String]
// MARK: - Public
public init(
@ -61,7 +69,8 @@ public struct EntryResponse: Identifiable, Equatable, Sendable {
status: String,
title: String,
body: String,
address: String
address: String,
tags: [String] = []
) {
self.id = id
self.location = location
@ -70,5 +79,6 @@ public struct EntryResponse: Identifiable, Equatable, Sendable {
self.title = title
self.body = body
self.address = address
self.tags = tags
}
}

View file

@ -52,6 +52,7 @@ public protocol WeblogNetworkServiceProtocol: AnyObject, Sendable {
/// - address: The user address (username) to create the entry for
/// - content: The markdown content body of the weblog entry
/// - status: The publication status of the entry (e.g., "Draft", "Live", "Feed Only", "Web Only", "Unlisted")
/// - tags: An array of tags associated with the entry
/// - date: The publication date for the entry
/// - Returns: The created weblog entry with metadata and generated ID
/// - Throws: Network errors, authentication errors, or API-specific errors.
@ -59,6 +60,7 @@ public protocol WeblogNetworkServiceProtocol: AnyObject, Sendable {
address: String,
content: String,
status: String,
tags: [String],
date: Date
) async throws -> EntryResponse
@ -78,6 +80,7 @@ public protocol WeblogNetworkServiceProtocol: AnyObject, Sendable {
/// - content: The updated markdown content body of the weblog entry
/// - status: The updated publication status of the entry (e.g., "Draft", "Live", "Feed Only", "Web Only",
/// "Unlisted")
/// - tags: An array of tags associated with the entry
/// - date: The updated publication date for the entry
/// - Returns: The updated weblog entry with new content and metadata
/// - Throws: Network errors, authentication errors, or API-specific errors if the entry is not found.
@ -86,6 +89,7 @@ public protocol WeblogNetworkServiceProtocol: AnyObject, Sendable {
entryID: String,
content: String,
status: String,
tags: [String],
date: Date
) async throws -> EntryResponse
@ -149,6 +153,7 @@ actor WeblogNetworkService: WeblogNetworkServiceProtocol {
address: String,
content: String,
status: String,
tags: [String],
date: Date
) async throws -> EntryResponse {
let response = try await networkClient.run(
@ -156,6 +161,7 @@ actor WeblogNetworkService: WeblogNetworkServiceProtocol {
address: address,
content: content,
status: status,
tags: tags,
date: date
)
)
@ -171,6 +177,7 @@ actor WeblogNetworkService: WeblogNetworkServiceProtocol {
entryID: String,
content: String,
status: String,
tags: [String],
date: Date
) async throws -> EntryResponse {
let response = try await networkClient.run(
@ -179,6 +186,7 @@ actor WeblogNetworkService: WeblogNetworkServiceProtocol {
entryID: entryID,
content: content,
status: status,
tags: tags,
date: date
)
)
@ -220,6 +228,7 @@ private extension EntryResponse {
title = weblogEntryResponse.title
body = weblogEntryResponse.body
address = weblogEntryResponse.address
tags = weblogEntryResponse.tags
}
}
@ -228,6 +237,8 @@ private extension EntryResponse {
/// Initializes the `CreateWeblogEntryResponse` model from the network response
/// model, so that the client doesn't depend on network models.
///
/// Tags not returned in create/update response, will be synced on next fetch.
///
/// - Parameter createWeblogEntryResponse: The network model to be mapped.
init(
address: String,
@ -239,6 +250,7 @@ private extension EntryResponse {
status = createWeblogEntryResponse.status
title = createWeblogEntryResponse.title
body = createWeblogEntryResponse.body
tags = .init()
self.address = address
}
}

View file

@ -0,0 +1,19 @@
import Foundation
import SwiftData
public extension WeblogTag {
/// Creates a fetch descriptor for retrieving tags sorted alphabetically by title.
///
/// This method provides a pre-configured fetch descriptor that sorts tags
/// by title in ascending order (A-Z). This is the standard sorting order
/// for displaying tag collections, presenting them in a predictable alphabetical
/// sequence for easy browsing and selection.
///
/// - Returns: A `FetchDescriptor<WeblogTag>` configured for alphabetical title sorting.
static func fetchDescriptor() -> FetchDescriptor<WeblogTag> {
.init(
sortBy: [.init(\.title)]
)
}
}

View file

@ -10,7 +10,8 @@ extension WeblogEntry {
date: storableEntry.date,
status: storableEntry.status,
location: storableEntry.location,
address: storableEntry.address
address: storableEntry.address,
tags: storableEntry.tags.isEmpty ? nil : storableEntry.tags
)
}
}

View file

@ -64,13 +64,13 @@ public struct WeblogPersistenceServiceFactory: WeblogPersistenceServiceFactoryPr
) -> any WeblogPersistenceServiceProtocol {
let configuration = ModelConfiguration(
"entries",
schema: .init([WeblogEntry.self]),
schema: .init([WeblogEntry.self, WeblogTag.self]),
isStoredInMemoryOnly: inMemory,
allowsSave: true
)
let container = try? ModelContainer(
for: WeblogEntry.self,
for: WeblogEntry.self, WeblogTag.self,
configurations: configuration
)

View file

@ -0,0 +1,8 @@
extension WeblogTag {
static func makeTag(
title: String
) -> WeblogTag {
.init(title: title)
}
}

View file

@ -54,6 +54,14 @@ public struct StorableEntry {
/// proper data organization and multi-address support in local storage.
let address: String
/// An array of tags associated with the weblog entry for categorization and discovery.
///
/// These user-defined tags enable content organization, filtering, and search
/// functionality. Tags can be used to group related entries or identify
/// specific themes, events, or characteristics of the content. The array may
/// be empty if no tags have been assigned to the entry.
let tags: [String]
// MARK: - Lifecycle
/// Initializes a new storable entry with all required weblog entry data.
@ -69,6 +77,7 @@ public struct StorableEntry {
/// - status: The publication status of the entry.
/// - location: The URL slug for the entry.
/// - address: The domain where the entry is hosted.
/// - tags: An array of tags associated with the entry. Defaults to empty array.
public init(
id: String,
title: String,
@ -76,7 +85,8 @@ public struct StorableEntry {
date: Double,
status: String,
location: String,
address: String
address: String,
tags: [String]
) {
self.id = id
self.title = title
@ -85,5 +95,6 @@ public struct StorableEntry {
self.status = status
self.location = location
self.address = address
self.tags = tags
}
}

View file

@ -64,6 +64,17 @@ public final class WeblogEntry {
/// for multiple weblogs within the same app instance.
public private(set) var address: String
/// An array of tags associated with the weblog entry for categorization and discovery.
///
/// These user-defined tags enable content organization, filtering, and search
/// functionality. Tags can be used to group related entries or identify
/// specific themes, events, or characteristics of the content. The array may
/// be empty if no tags have been assigned to the entry.
///
/// This property is optional to support migration from older database schemas
/// that didn't include tags. When nil, it should be treated as an empty array.
public private(set) var tags: [String]?
// MARK: - Unique constraints
/// Ensures each entry has a unique identifier in the database.
@ -87,6 +98,7 @@ public final class WeblogEntry {
/// - status: The publication status of the entry.
/// - location: The URL slug for the entry.
/// - address: The domain where the entry is hosted.
/// - tags: An optional array of tags associated with the entry. Defaults to nil (treated as empty array).
public init(
id: String,
title: String,
@ -94,7 +106,8 @@ public final class WeblogEntry {
date: Double,
status: String,
location: String,
address: String
address: String,
tags: [String]? = nil
) {
self.id = id
self.title = title
@ -103,5 +116,6 @@ public final class WeblogEntry {
self.status = status
self.location = location
self.address = address
self.tags = tags
}
}

View file

@ -0,0 +1,46 @@
import Foundation
import SwiftData
/// A SwiftData model representing a tag for categorizing and organizing weblog entries.
///
/// This model stores unique tag titles that can be associated with weblog entries
/// for organization, filtering, and discovery purposes. Tags enable users to
/// group related content and facilitate content navigation through categorization.
///
/// The model uses SwiftData's `@Model` macro for automatic persistence capabilities
/// and includes unique constraints on the tag title to prevent duplicate tags.
/// Tags are designed to be reusable across multiple weblog entries.
///
/// Usage example:
/// ```swift
/// let tag = WeblogTag(title: "swift")
/// modelContext.insert(tag)
/// ```
@Model
public final class WeblogTag {
/// The title or label of the tag.
///
/// This is the display text for the tag and serves as its unique identifier
/// within the local database. Tag titles are case-sensitive and should be
/// user-friendly labels that describe the category or characteristic they represent.
public private(set) var title: String
// MARK: - Unique constraints
/// Ensures each tag title is unique in the database.
///
/// This constraint prevents duplicate tags from being created and ensures
/// that tag titles remain unique across all stored tags. This enables
/// consistent tag reuse and prevents fragmentation of categorization.
#Unique<WeblogTag>([\.title])
/// Initializes a new tag with the specified title.
///
/// - Parameter title: The display text and unique identifier for the tag.
public init(
title: String
) {
self.title = title
}
}

View file

@ -110,6 +110,7 @@ actor WeblogPersistenceService: WeblogPersistenceServiceProtocol {
for await _ in authSessionService.observeLogoutEvents() {
Task { @MainActor [weak self] in
try self?.container.mainContext.delete(model: WeblogEntry.self)
try self?.container.mainContext.delete(model: WeblogTag.self)
try self?.container.mainContext.save()
}
}
@ -129,9 +130,25 @@ actor WeblogPersistenceService: WeblogPersistenceServiceProtocol {
) throws {
let model = WeblogEntry.makeEntry(storableEntry: entry)
container.mainContext.insert(model)
try storeTags(entry.tags)
try container.mainContext.save()
}
@MainActor
private func storeTags(
_ tags: [String]
) throws {
try tags.forEach(storeTag)
}
@MainActor
private func storeTag(
_ title: String
) throws {
let newTag = WeblogTag.makeTag(title: title)
container.mainContext.insert(newTag)
}
@MainActor
private func removeDeletedEntries(
_ entries: [StorableEntry]

View file

@ -13,7 +13,8 @@ extension StorableEntry {
date: entryResponse.date,
status: entryResponse.status,
location: entryResponse.location,
address: entryResponse.address
address: entryResponse.address,
tags: entryResponse.tags
)
}
}

View file

@ -61,6 +61,7 @@ public protocol WeblogRepositoryProtocol: Sendable {
/// - entryID: The unique identifier of the entry to update. If nil, creates a new entry.
/// - body: The content body of the weblog entry in markdown format
/// - status: The publication status of the entry (e.g., "Draft", "Live", "Feed Only", "Web Only", "Unlisted")
/// - tags: An array of tags associated with the entry
/// - date: The publication date for the entry
/// - Throws: Network errors from the remote operation or persistence errors
/// from the local storage operations.
@ -69,6 +70,7 @@ public protocol WeblogRepositoryProtocol: Sendable {
entryID: String?,
body: String,
status: String,
tags: [String],
date: Date
) async throws
@ -142,6 +144,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
entryID: String? = nil,
body: String,
status: String,
tags: [String],
date: Date
) async throws {
if let entryID {
@ -150,6 +153,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
entryID: entryID,
body: body,
status: status,
tags: tags,
date: date
)
} else {
@ -157,6 +161,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
address: address,
body: body,
status: status,
tags: tags,
date: date
)
}
@ -168,6 +173,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
address: String,
body: String,
status: String,
tags: [String],
date: Date
) async throws {
guard await authSessionService.isLoggedIn else {
@ -178,6 +184,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
address: address,
content: body,
status: status,
tags: tags,
date: date
)
@ -189,6 +196,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
entryID: String,
body: String,
status: String,
tags: [String],
date: Date
) async throws {
guard await authSessionService.isLoggedIn else {
@ -200,6 +208,7 @@ actor WeblogRepository: WeblogRepositoryProtocol {
entryID: entryID,
content: body,
status: status,
tags: tags,
date: date
)