Fix id of Items, saving

This commit is contained in:
Christoph Hagen
2025-06-11 08:19:44 +02:00
parent 5970ce2e9f
commit 1d0eba9d78
64 changed files with 233 additions and 217 deletions

View File

@ -1767,7 +1767,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.2; MARKETING_VERSION = 1.3;
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.CHDataManagement; PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.CHDataManagement;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
@ -1806,7 +1806,7 @@
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.0; MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.2; MARKETING_VERSION = 1.3;
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.CHDataManagement; PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.CHDataManagement;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;

View File

@ -37,7 +37,7 @@ struct GalleryBlock: BlockLineProcessor {
$0.imageSet(width: imageWidth, height: imageWidth, language: language) $0.imageSet(width: imageWidth, height: imageWidth, language: language)
} }
imageSets.forEach(results.require) imageSets.forEach(results.require)
let id = firstImage.id.replacingOccurrences(of: ".", with: "-") let id = firstImage.identifier.replacingOccurrences(of: ".", with: "-")
let gallery = ImageGallery(id: id, images: imageSets, standalone: true) let gallery = ImageGallery(id: id, images: imageSets, standalone: true)
results.require(footer: gallery.standaloneFooter) results.require(footer: gallery.standaloneFooter)
results.require(headers: .swiperJs, .swiperCss) results.require(headers: .swiperJs, .swiperCss)

View File

@ -50,7 +50,7 @@ struct PhoneScreensBlock: OrderedKeyBlockProcessor {
} }
if key == .tall { if key == .tall {
if tall != nil { if tall != nil {
print("Another tall image: \(file.id)") print("Another tall image: \(file.identifier)")
invalid(markdown) invalid(markdown)
return "" return ""
} }
@ -69,7 +69,7 @@ struct PhoneScreensBlock: OrderedKeyBlockProcessor {
} }
// key == .wide // key == .wide
if wide != nil { if wide != nil {
print("Another wide image: \(file.id)") print("Another wide image: \(file.identifier)")
invalid(markdown) invalid(markdown)
return "" return ""
} }

View File

@ -185,7 +185,7 @@ struct HtmlCommand: CommandProcessor {
results.missing(file: fileId, source: "HTML: \(source)") results.missing(file: fileId, source: "HTML: \(source)")
return return
} }
results.warning("Could not determine image version for file '\(file.id)' for \(source)") results.warning("Could not determine image version for file '\(file.identifier)' for \(source)")
} }
private func findFileWith(relativePath: String, type: FileType, source: String) { private func findFileWith(relativePath: String, type: FileType, source: String) {

View File

@ -58,19 +58,19 @@ final class ImageGenerator {
} }
guard let data = version.image.dataContent() else { guard let data = version.image.dataContent() else {
print("ImageGenerator: Failed to load data for image \(version.image.id)") print("ImageGenerator: Failed to load data for image \(version.image.identifier)")
return false return false
} }
guard let originalImage = NSImage(data: data) else { guard let originalImage = NSImage(data: data) else {
print("ImageGenerator: Failed to load image \(version.image.id)") print("ImageGenerator: Failed to load image \(version.image.identifier)")
return false return false
} }
let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight)) let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight))
guard let data = create(image: representation, type: version.type, quality: version.quality) else { guard let data = create(image: representation, type: version.type, quality: version.quality) else {
print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.id)") print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.identifier)")
return false return false
} }
@ -161,7 +161,7 @@ final class ImageGenerator {
process.waitUntilExit() process.waitUntilExit()
if process.terminationStatus != 0 { if process.terminationStatus != 0 {
print("ImageGenerator: Failed to create AVIF image \(version.image.id)") print("ImageGenerator: Failed to create AVIF image \(version.image.identifier)")
let outputData = pipe.fileHandleForReading.readDataToEndOfFile() let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
let outputString = String(data: outputData, encoding: .utf8) ?? "" let outputString = String(data: outputData, encoding: .utf8) ?? ""
print(outputString) print(outputString)

View File

@ -56,14 +56,14 @@ struct ImageVersion {
extension ImageVersion: Identifiable { extension ImageVersion: Identifiable {
var id: String { var id: String {
image.id + "-" + versionId image.identifier + "-" + versionId
} }
} }
extension ImageVersion: Equatable { extension ImageVersion: Equatable {
static func == (lhs: ImageVersion, rhs: ImageVersion) -> Bool { static func == (lhs: ImageVersion, rhs: ImageVersion) -> Bool {
lhs.image.id == rhs.image.id && lhs.image.identifier == rhs.image.identifier &&
lhs.maximumWidth == rhs.maximumWidth && lhs.maximumWidth == rhs.maximumWidth &&
lhs.maximumHeight == rhs.maximumHeight && lhs.maximumHeight == rhs.maximumHeight &&
lhs.type == rhs.type lhs.type == rhs.type
@ -73,7 +73,7 @@ extension ImageVersion: Equatable {
extension ImageVersion: Hashable { extension ImageVersion: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(image.id) hasher.combine(image.identifier)
hasher.combine(maximumWidth) hasher.combine(maximumWidth)
hasher.combine(maximumHeight) hasher.combine(maximumHeight)
hasher.combine(type) hasher.combine(type)

View File

@ -33,7 +33,7 @@ final class PageGenerator {
language: language, results: results) language: language, results: results)
let rawPageContent: String let rawPageContent: String
if let existing = content.storage.pageContent(for: page.id, language: language) { if let existing = content.storage.pageContent(for: page.identifier, language: language) {
rawPageContent = existing rawPageContent = existing
} else { } else {
rawPageContent = makeEmptyPageContent(in: language) rawPageContent = makeEmptyPageContent(in: language)

View File

@ -44,7 +44,7 @@ struct PostContentGenerator {
} }
private var postDescription: String { private var postDescription: String {
"content of post \(post.id) (\(language.shortText))" "content of post \(post.identifier) (\(language.shortText))"
} }
private func handleLink( private func handleLink(

View File

@ -95,7 +95,7 @@ final class PostListPageGenerator {
post: post).generate() post: post).generate()
return FeedEntryData( return FeedEntryData(
entryId: post.id, entryId: post.identifier,
title: localized.title, title: localized.title,
textAboveTitle: post.dateText(in: language), textAboveTitle: post.dateText(in: language),
link: linkUrl, link: linkUrl,

View File

@ -161,7 +161,7 @@ final class GenerationResults: ObservableObject {
update { self.unsavedOutputFiles = unsavedOutputFiles } update { self.unsavedOutputFiles = unsavedOutputFiles }
let emptyPages = cache.values.filter { $0.pageIsEmpty }.map { $0.itemId }.compactMap { id -> LocalizedPageId? in let emptyPages = cache.values.filter { $0.pageIsEmpty }.map { $0.itemId }.compactMap { id -> LocalizedPageId? in
guard case .page(let page) = id.itemType else { return nil } guard case .page(let page) = id.itemType else { return nil }
return LocalizedPageId(language: id.language, pageId: page.id) return LocalizedPageId(language: id.language, pageId: page.identifier)
}.asSet() }.asSet()
update { self.emptyPages = emptyPages } update { self.emptyPages = emptyPages }
let redirects = cache.values.compactMap { $0.redirect }.reduce(into: [:]) { $0[$1.originalUrl] = $1.newUrl } let redirects = cache.values.compactMap { $0.redirect }.reduce(into: [:]) { $0[$1.originalUrl] = $1.newUrl }

View File

@ -11,7 +11,7 @@ extension ImageToGenerate: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(size) hasher.combine(size)
hasher.combine(image.id) hasher.combine(image.identifier)
} }
} }
@ -284,7 +284,7 @@ final class PageGenerationResults: ObservableObject {
func markPageAsEmpty() { func markPageAsEmpty() {
guard case .page(let page) = itemId.itemType else { return } guard case .page(let page) = itemId.itemType else { return }
onMain { self.pageIsEmpty = true } onMain { self.pageIsEmpty = true }
delegate.empty(.init(language: itemId.language, pageId: page.id)) delegate.empty(.init(language: itemId.language, pageId: page.identifier))
} }
func redirect(from originalUrl: String, to newUrl: String) { func redirect(from originalUrl: String, to newUrl: String) {

View File

@ -2,7 +2,7 @@ import SwiftUI
protocol MainContentView: View { protocol MainContentView: View {
associatedtype Item: Identifiable associatedtype Item
init(item: Item) init(item: Item)

View File

@ -12,7 +12,7 @@ struct SelectedDetailView<Contained>: View where Contained: MainContentView {
var body: some View { var body: some View {
if let item = selected { if let item = selected {
Contained(item: item) Contained(item: item)
.id(item.id) //.id(item.id)
} else { } else {
EmptyView() EmptyView()
} }

View File

@ -20,6 +20,6 @@ extension FileResource {
} }
static var mock: FileResource { static var mock: FileResource {
Content.mock.files.first(where: { $0.id == "my-file.txt" })! Content.mock.files.first(where: { $0.identifier == "my-file.txt" })!
} }
} }

View File

@ -28,13 +28,13 @@ extension Page {
lastModified: nil, lastModified: nil,
originalUrl: "projects/electronics/my-first-project/en.html"), originalUrl: "projects/electronics/my-first-project/en.html"),
tags: [ tags: [
content.tags.first(where: { $0.id == "electronics" })! content.tags.first(where: { $0.identifier == "electronics" })!
]) ])
] ]
} }
static var empty: Page { static var empty: Page {
Content.mock.pages.first(where: { $0.id == "my-id" })! Content.mock.pages.first(where: { $0.identifier == "my-id" })!
} }
} }
} }

View File

@ -27,9 +27,9 @@ extension Post {
startDate: .now, startDate: .now,
endDate: nil, endDate: nil,
tags: [ tags: [
content.tags.first(where: { $0.id == "nature" })!, content.tags.first(where: { $0.identifier == "nature" })!,
content.tags.first(where: { $0.id == "sports" })!, content.tags.first(where: { $0.identifier == "sports" })!,
content.tags.first(where: { $0.id == "hiking" })! content.tags.first(where: { $0.identifier == "hiking" })!
], ],
german: .init( german: .init(
content: content, content: content,
@ -47,44 +47,44 @@ extension Post {
createdDate: .now, createdDate: .now,
startDate: .now.addingTimeInterval(-86400), endDate: .now, startDate: .now.addingTimeInterval(-86400), endDate: .now,
tags: [ tags: [
content.tags.first(where: { $0.id == "nature" })!, content.tags.first(where: { $0.identifier == "nature" })!,
content.tags.first(where: { $0.id == "sports" })!, content.tags.first(where: { $0.identifier == "sports" })!,
content.tags.first(where: { $0.id == "hiking" })!, content.tags.first(where: { $0.identifier == "hiking" })!,
content.tags.first(where: { $0.id == "mountains" })! content.tags.first(where: { $0.identifier == "mountains" })!
], ],
german: LocalizedPost( german: LocalizedPost(
content: content, content: content,
title: "Eine lange Wanderung", title: "Eine lange Wanderung",
text: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.", text: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.",
images: [ images: [
content.files.first(where: { $0.id == "image1" })!, content.files.first(where: { $0.identifier == "image1" })!,
content.files.first(where: { $0.id == "image2" })!, content.files.first(where: { $0.identifier == "image2" })!,
content.files.first(where: { $0.id == "image3" })!, content.files.first(where: { $0.identifier == "image3" })!,
content.files.first(where: { $0.id == "image4" })! content.files.first(where: { $0.identifier == "image4" })!
]), ]),
english: LocalizedPost( english: LocalizedPost(
content: content, content: content,
title: "A longer hike", title: "A longer hike",
text: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.", text: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
images: [ images: [
content.files.first(where: { $0.id == "image1" })!, content.files.first(where: { $0.identifier == "image1" })!,
content.files.first(where: { $0.id == "image2" })!, content.files.first(where: { $0.identifier == "image2" })!,
content.files.first(where: { $0.id == "image3" })!, content.files.first(where: { $0.identifier == "image3" })!,
content.files.first(where: { $0.id == "image4" })! content.files.first(where: { $0.identifier == "image4" })!
])) ]))
] ]
} }
static var empty: Post { static var empty: Post {
Content.mock.posts.first(where: { $0.id == "empty" })! Content.mock.posts.first(where: { $0.identifier == "empty" })!
} }
static var hike: Post { static var hike: Post {
Content.mock.posts.first(where: { $0.id == "hike" })! Content.mock.posts.first(where: { $0.identifier == "hike" })!
} }
static var hike2: Post { static var hike2: Post {
Content.mock.posts.first(where: { $0.id == "hike2" })! Content.mock.posts.first(where: { $0.identifier == "hike2" })!
} }
} }
} }

View File

@ -13,7 +13,7 @@ extension Tag {
urlComponent: "elektronik", urlComponent: "elektronik",
name: "Elektronik", name: "Elektronik",
linkPreview: .init(description: "Eine Beschreibung des Tags", linkPreview: .init(description: "Eine Beschreibung des Tags",
image: content.files.first(where: { $0.id == "image2" })!), image: content.files.first(where: { $0.identifier == "image2" })!),
originalUrl: "projects/electronics" originalUrl: "projects/electronics"
), ),
english: .init( english: .init(
@ -22,7 +22,7 @@ extension Tag {
name: "Electronics", name: "Electronics",
linkPreview: .init( linkPreview: .init(
description: "Some description of the tag", description: "Some description of the tag",
image: content.files.first(where: { $0.id == "image1" })!), image: content.files.first(where: { $0.identifier == "image1" })!),
originalUrl: "projects/electronics") originalUrl: "projects/electronics")
), ),
Tag( Tag(
@ -53,23 +53,23 @@ extension Tag {
} }
static var electronics: Tag { static var electronics: Tag {
Content.mock.tags.first(where: { $0.id == "electronics" })! Content.mock.tags.first(where: { $0.identifier == "electronics" })!
} }
static var nature: Tag { static var nature: Tag {
Content.mock.tags.first(where: { $0.id == "nature" })! Content.mock.tags.first(where: { $0.identifier == "nature" })!
} }
static var sports: Tag { static var sports: Tag {
Content.mock.tags.first(where: { $0.id == "sports" })! Content.mock.tags.first(where: { $0.identifier == "sports" })!
} }
static var hiking: Tag { static var hiking: Tag {
Content.mock.tags.first(where: { $0.id == "hiking" })! Content.mock.tags.first(where: { $0.identifier == "hiking" })!
} }
static var mountains: Tag { static var mountains: Tag {
Content.mock.tags.first(where: { $0.id == "mountains" })! Content.mock.tags.first(where: { $0.identifier == "mountains" })!
} }
} }
} }

View File

@ -69,7 +69,7 @@ extension Content {
continue continue
} }
let path = file.absoluteUrl let path = file.absoluteUrl
if !storage.copy(file: file.id, to: path) { if !storage.copy(file: file.identifier, to: path) {
results.general.unsavedOutput(path, source: .general) results.general.unsavedOutput(path, source: .general)
} }
} }
@ -147,23 +147,23 @@ extension Content {
// MARK: Find items by id // MARK: Find items by id
func page(_ pageId: String) -> Page? { func page(_ pageId: String) -> Page? {
pages.first { $0.id == pageId } pages.first { $0.identifier == pageId }
} }
func image(_ imageId: String) -> FileResource? { func image(_ imageId: String) -> FileResource? {
files.first { $0.id == imageId && $0.type.isImage } files.first { $0.identifier == imageId && $0.type.isImage }
} }
func video(_ videoId: String) -> FileResource? { func video(_ videoId: String) -> FileResource? {
files.first { $0.id == videoId && $0.type.isVideo } files.first { $0.identifier == videoId && $0.type.isVideo }
} }
func file(_ fileId: String) -> FileResource? { func file(_ fileId: String) -> FileResource? {
files.first { $0.id == fileId } files.first { $0.identifier == fileId }
} }
func tag(_ tagId: String) -> Tag? { func tag(_ tagId: String) -> Tag? {
tags.first { $0.id == tagId } tags.first { $0.identifier == tagId }
} }
// MARK: Generation input // MARK: Generation input
@ -322,12 +322,12 @@ extension Content {
let pageUrl = settings.general.url + relativePageUrl let pageUrl = settings.general.url + relativePageUrl
guard let content = pageGenerator.generate(page: page, language: language, results: results, pageUrl: pageUrl) else { guard let content = pageGenerator.generate(page: page, language: language, results: results, pageUrl: pageUrl) else {
print("Failed to generate page \(page.id) in language \(language)") print("Failed to generate page \(page.identifier) in language \(language)")
return return
} }
guard storage.write(content, to: filePath) else { guard storage.write(content, to: filePath) else {
print("Failed to save page \(page.id)") print("Failed to save page \(page.identifier)")
return return
} }

View File

@ -83,16 +83,16 @@ extension Content {
func removeUnlinkedFiles() -> Bool { func removeUnlinkedFiles() -> Bool {
var success = true var success = true
if !storage.deletePostFiles(notIn: posts.map { $0.id }) { if !storage.deletePostFiles(notIn: posts.map { $0.identifier }) {
success = false success = false
} }
if !storage.deletePageFiles(notIn: pages.map { $0.id }) { if !storage.deletePageFiles(notIn: pages.map { $0.identifier }) {
success = false success = false
} }
if !storage.deleteTagFiles(notIn: tags.map { $0.id }) { if !storage.deleteTagFiles(notIn: tags.map { $0.identifier }) {
success = false success = false
} }
if !storage.deleteFileResources(notIn: files.map { $0.id }) { if !storage.deleteFileResources(notIn: files.map { $0.identifier }) {
success = false success = false
} }
return success return success

View File

@ -7,19 +7,19 @@ extension Content {
private static let disallowedCharactersInFileIds = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted private static let disallowedCharactersInFileIds = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted
func isNewIdForTag(_ id: String) -> Bool { func isNewIdForTag(_ id: String) -> Bool {
tagOverview?.id != id && !tags.contains { $0.id == id } tagOverview?.identifier != id && !tags.contains { $0.identifier == id }
} }
func isNewIdForPage(_ id: String) -> Bool { func isNewIdForPage(_ id: String) -> Bool {
!pages.contains { $0.id == id } !pages.contains { $0.identifier == id }
} }
func isNewIdForPost(_ id: String) -> Bool { func isNewIdForPost(_ id: String) -> Bool {
!posts.contains { $0.id == id } !posts.contains { $0.identifier == id }
} }
func isNewIdForFile(_ id: String) -> Bool { func isNewIdForFile(_ id: String) -> Bool {
!files.contains { $0.id == id } !files.contains { $0.identifier == id }
} }
func isValidIdForTagOrPageOrPost(_ id: String) -> Bool { func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {

View File

@ -2,7 +2,7 @@ import Foundation
import SwiftUI import SwiftUI
import Combine import Combine
final class Content: ObservableObject { final class Content: ChangeObservableItem {
@ObservedObject @ObservedObject
var storage: Storage var storage: Storage
@ -47,6 +47,8 @@ final class Content: ObservableObject {
var errorCallback: ((StorageError) -> Void)? var errorCallback: ((StorageError) -> Void)?
var cancellables: Set<AnyCancellable> = []
/// A cache of file sizes /// A cache of file sizes
private var fileSizes: [String: Int] = [:] private var fileSizes: [String: Int] = [:]
@ -200,9 +202,9 @@ final class Content: ObservableObject {
for file in self.files { for file in self.files {
guard file.type.isVideo else { continue } guard file.type.isVideo else { continue }
guard !file.isExternallyStored else { continue } guard !file.isExternallyStored else { continue }
guard !storage.hasVideoThumbnail(for: file.id) else { continue } guard !storage.hasVideoThumbnail(for: file.identifier) else { continue }
if await imageGenerator.createVideoThumbnail(for: file.id) { if await imageGenerator.createVideoThumbnail(for: file.identifier) {
print("Generated thumbnail for \(file.id)") print("Generated thumbnail for \(file.identifier)")
file.didChange() file.didChange()
} }
} }
@ -229,6 +231,10 @@ final class Content: ObservableObject {
self.lastSave = .now self.lastSave = .now
} }
func needsSaving() {
needsSave()
}
// MARK: File sizes // MARK: File sizes
func size(of file: String) -> Int? { func size(of file: String) -> Int? {

View File

@ -2,7 +2,7 @@ import Foundation
protocol DateItem { protocol DateItem {
var id: String { get } var identifier: String { get }
var startDate: Date { get } var startDate: Date { get }
@ -20,7 +20,7 @@ extension Sequence where Element: DateItem {
func sortedByStartDateAndId() -> [Element] { func sortedByStartDateAndId() -> [Element] {
sorted { (lhs, rhs) -> Bool in sorted { (lhs, rhs) -> Bool in
if lhs.startDate == rhs.startDate { if lhs.startDate == rhs.startDate {
return lhs.id < rhs.id return lhs.identifier < rhs.identifier
} }
return lhs.startDate > rhs.startDate return lhs.startDate > rhs.startDate
} }

View File

@ -49,18 +49,18 @@ final class FileResource: Item, LocalizedItem {
/// The dimensions of the image /// The dimensions of the image
var imageDimensions: CGSize? { var imageDimensions: CGSize? {
get { content.dimensions(of: id) } get { content.dimensions(of: identifier) }
set { set {
content.cache(dimensions: newValue, of: id) content.cache(dimensions: newValue, of: identifier)
didChange(save: false) didChange(save: false)
} }
} }
/// The size of the file in bytes /// The size of the file in bytes
var fileSize: Int? { var fileSize: Int? {
get { content.size(of: id) } get { content.size(of: identifier) }
set { set {
content.cache(size: newValue, of: id) content.cache(size: newValue, of: identifier)
didChange(save: false) didChange(save: false)
} }
} }
@ -114,11 +114,11 @@ final class FileResource: Item, LocalizedItem {
// MARK: Text // MARK: Text
func textContent() -> String { func textContent() -> String {
content.storage.fileContent(for: id) ?? "" content.storage.fileContent(for: identifier) ?? ""
} }
func save(textContent: String) -> Bool { func save(textContent: String) -> Bool {
guard content.storage.save(fileContent: textContent, for: id) else { guard content.storage.save(fileContent: textContent, for: identifier) else {
return false return false
} }
modifiedDate = .now modifiedDate = .now
@ -126,7 +126,7 @@ final class FileResource: Item, LocalizedItem {
} }
func dataContent() -> Foundation.Data? { func dataContent() -> Foundation.Data? {
content.storage.fileData(for: id) content.storage.fileData(for: identifier)
} }
// MARK: Images // MARK: Images
@ -165,7 +165,7 @@ final class FileResource: Item, LocalizedItem {
} }
update(fileSize: displayImageData.count) update(fileSize: displayImageData.count)
guard let loadedImage = NSImage(data: displayImageData) else { guard let loadedImage = NSImage(data: displayImageData) else {
print("Failed to create image \(id)") print("Failed to create image \(identifier)")
return nil return nil
} }
update(imageDimensions: loadedImage.size) update(imageDimensions: loadedImage.size)
@ -191,14 +191,14 @@ final class FileResource: Item, LocalizedItem {
private var displayImageData: Foundation.Data? { private var displayImageData: Foundation.Data? {
if type.isImage { if type.isImage {
guard let data = content.storage.fileData(for: id) else { guard let data = content.storage.fileData(for: identifier) else {
print("Failed to load data for image \(id)") print("Failed to load data for image \(identifier)")
return nil return nil
} }
return data return data
} }
if type.isVideo { if type.isVideo {
return content.storage.getVideoThumbnail(for: id) return content.storage.getVideoThumbnail(for: identifier)
} }
return nil return nil
} }
@ -234,7 +234,7 @@ final class FileResource: Item, LocalizedItem {
func determineFileSize() { func determineFileSize() {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let size = self.content.storage.size(of: self.id) let size = self.content.storage.size(of: self.identifier)
self.update(fileSize: size) self.update(fileSize: size)
} }
} }
@ -249,7 +249,7 @@ final class FileResource: Item, LocalizedItem {
/// The path to the output folder where image versions are stored (no leading slash) /// The path to the output folder where image versions are stored (no leading slash)
var outputImageFolder: String { var outputImageFolder: String {
"\(content.settings.paths.imagesOutputFolderPath)/\(id.fileNameWithoutExtension)" "\(content.settings.paths.imagesOutputFolderPath)/\(identifier.fileNameWithoutExtension)"
} }
func outputPath(width: Int, height: Int, type: FileType?) -> String { func outputPath(width: Int, height: Int, type: FileType?) -> String {
@ -293,9 +293,9 @@ final class FileResource: Item, LocalizedItem {
func createVideoThumbnail() { func createVideoThumbnail() {
guard type.isVideo else { return } guard type.isVideo else { return }
guard !content.storage.hasVideoThumbnail(for: id) else { return } guard !content.storage.hasVideoThumbnail(for: identifier) else { return }
Task { Task {
if await content.imageGenerator.createVideoThumbnail(for: id) { if await content.imageGenerator.createVideoThumbnail(for: identifier) {
didChange() didChange()
} }
} }
@ -322,7 +322,7 @@ final class FileResource: Item, LocalizedItem {
return "/" + customOutputPath return "/" + customOutputPath
} }
} }
let path = pathPrefix + "/" + id let path = pathPrefix + "/" + identifier
return makeCleanAbsolutePath(path) return makeCleanAbsolutePath(path)
} }
@ -353,14 +353,14 @@ final class FileResource: Item, LocalizedItem {
@discardableResult @discardableResult
func update(id newId: String) -> Bool { func update(id newId: String) -> Bool {
guard !isExternallyStored else { guard !isExternallyStored else {
id = newId identifier = newId
return true return true
} }
guard content.storage.move(file: id, to: newId) else { guard content.storage.move(file: identifier, to: newId) else {
print("Failed to move file \(id) to \(newId)") print("Failed to move file \(identifier) to \(newId)")
return false return false
} }
id = newId identifier = newId
return true return true
} }
} }
@ -368,7 +368,7 @@ final class FileResource: Item, LocalizedItem {
extension FileResource: CustomStringConvertible { extension FileResource: CustomStringConvertible {
var description: String { var description: String {
id identifier
} }
} }
@ -418,6 +418,6 @@ extension FileResource: StorageItem {
} }
func saveToDisk(_ data: Data) -> Bool { func saveToDisk(_ data: Data) -> Bool {
content.storage.save(fileResource: data, for: id) content.storage.save(fileResource: data, for: identifier)
} }
} }

View File

@ -7,11 +7,17 @@ class Item: ChangeObservingItem, Identifiable {
@Published @Published
private var changeToggle = false private var changeToggle = false
/// A session-id for the item for identification
let id = UUID()
/// The unique, persistent identifier of the item
///
/// This identifier is not used for `Identifiable`, since it may be changed through the UI.
@Published @Published
var id: String var identifier: String
init(content: Content, id: String) { init(content: Content, id: String) {
self.id = id self.identifier = id
super.init(content: content) super.init(content: content)
observeChanges() observeChanges()
@ -44,14 +50,14 @@ class Item: ChangeObservingItem, Identifiable {
} }
var itemId: ItemId { var itemId: ItemId {
.init(type: itemType, id: id) .init(type: itemType, id: identifier)
} }
} }
extension Item: Equatable { extension Item: Equatable {
static func == (lhs: Item, rhs: Item) -> Bool { static func == (lhs: Item, rhs: Item) -> Bool {
lhs.id == rhs.id && lhs.itemType == rhs.itemType lhs.id == rhs.id
} }
} }
@ -59,13 +65,12 @@ extension Item: Hashable {
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
hasher.combine(id) hasher.combine(id)
hasher.combine(itemType)
} }
} }
extension Item: Comparable { extension Item: Comparable {
static func < (lhs: Item, rhs: Item) -> Bool { static func < (lhs: Item, rhs: Item) -> Bool {
lhs.id < rhs.id && lhs.itemType < rhs.itemType lhs.identifier < rhs.identifier && lhs.itemType < rhs.itemType
} }
} }

View File

@ -31,11 +31,11 @@ extension ItemReference: Identifiable {
case .feed: case .feed:
return "1-feed" return "1-feed"
case .post(let post): case .post(let post):
return "2-post-\(post.id)" return "2-post-\(post.identifier)"
case .page(let page): case .page(let page):
return "3-page-\(page.id)" return "3-page-\(page.identifier)"
case .tagPage(let tag): case .tagPage(let tag):
return "5-tag-\(tag.id)" return "5-tag-\(tag.identifier)"
case .tagOverview: case .tagOverview:
return "4-tag-overview" return "4-tag-overview"
} }
@ -76,11 +76,11 @@ extension ItemReference: CustomStringConvertible {
case .feed: case .feed:
return "Feed" return "Feed"
case .post(let post): case .post(let post):
return "Post \(post.id)" return "Post \(post.identifier)"
case .page(let page): case .page(let page):
return "Page \(page.id)" return "Page \(page.identifier)"
case .tagPage(let tag): case .tagPage(let tag):
return "Tag \(tag.id)" return "Tag \(tag.identifier)"
case .tagOverview: case .tagOverview:
return "Tag Overview" return "Tag Overview"
} }

View File

@ -37,7 +37,7 @@ final class LinkPreview: ObservableObject {
// MARK: Storage // MARK: Storage
var data: Data { var data: Data {
.init(title: title, description: description, image: image?.id) .init(title: title, description: description, image: image?.identifier)
} }
init(context: LoadingContext, data: Data) { init(context: LoadingContext, data: Data) {

View File

@ -27,7 +27,7 @@ final class LoadingContext {
posts: posts.values.sortedByStartDateAndId(), posts: posts.values.sortedByStartDateAndId(),
pages: pages.values.sortedByStartDateAndId(), pages: pages.values.sortedByStartDateAndId(),
tags: tags.values.sorted(), tags: tags.values.sorted(),
files: files.values.sorted { $0.id }, files: files.values.sorted { $0.identifier },
tagOverview: tagOverview, tagOverview: tagOverview,
errors: errors.sorted().map { StorageError(message: $0) }) errors: errors.sorted().map { StorageError(message: $0) })
} }

View File

@ -96,7 +96,7 @@ extension LocalizedPost {
} }
var data: Data { var data: Data {
.init(images: images.map { $0.id }, .init(images: images.map { $0.identifier },
labels: labels.map { $0.data }.nonEmpty, labels: labels.map { $0.data }.nonEmpty,
title: title, title: title,
text: text, text: text,

View File

@ -75,11 +75,11 @@ final class Page: Item, DateItem, LocalizedItem {
@discardableResult @discardableResult
func update(id newId: String) -> Bool { func update(id newId: String) -> Bool {
guard content.storage.move(page: id, to: newId) else { guard content.storage.move(page: identifier, to: newId) else {
print("Failed to move files of page \(id)") print("Failed to move files of page \(identifier)")
return false return false
} }
id = newId identifier = newId
return true return true
} }
@ -146,11 +146,11 @@ final class Page: Item, DateItem, LocalizedItem {
} }
func pageContent(in language: ContentLanguage) -> String? { func pageContent(in language: ContentLanguage) -> String? {
content.storage.pageContent(for: id, language: language) content.storage.pageContent(for: identifier, language: language)
} }
func removeContent(in language: ContentLanguage) -> Bool { func removeContent(in language: ContentLanguage) -> Bool {
guard content.storage.remove(pageContent: id, in: language) else { guard content.storage.remove(pageContent: identifier, in: language) else {
return false return false
} }
if localized(in: language).update(hasContent: false) { if localized(in: language).update(hasContent: false) {
@ -160,7 +160,7 @@ final class Page: Item, DateItem, LocalizedItem {
} }
func save(pageContent: String, in language: ContentLanguage) -> Bool { func save(pageContent: String, in language: ContentLanguage) -> Bool {
guard content.storage.save(pageContent: pageContent, for: id, in: language) else { guard content.storage.save(pageContent: pageContent, for: identifier, in: language) else {
return false return false
} }
if localized(in: language).update(hasContent: true) { if localized(in: language).update(hasContent: true) {
@ -175,7 +175,7 @@ final class Page: Item, DateItem, LocalizedItem {
func updateContentExistence() { func updateContentExistence() {
var didUpdate = false var didUpdate = false
for language in ContentLanguage.allCases { for language in ContentLanguage.allCases {
let hasContent = content.storage.hasPageContent(for: id, language: language) let hasContent = content.storage.hasPageContent(for: identifier, language: language)
if localized(in: language).update(hasContent: hasContent) { if localized(in: language).update(hasContent: hasContent) {
didUpdate = true didUpdate = true
} }
@ -234,7 +234,7 @@ extension Page: StorageItem {
.init( .init(
isDraft: isDraft, isDraft: isDraft,
externalLink: externalLink, externalLink: externalLink,
tags: tags.map { $0.id }, tags: tags.map { $0.identifier },
hideDate: hideDate ? true : nil, hideDate: hideDate ? true : nil,
createdDate: createdDate, createdDate: createdDate,
startDate: startDate, startDate: startDate,
@ -244,6 +244,6 @@ extension Page: StorageItem {
} }
func saveToDisk(_ data: Data) -> Bool { func saveToDisk(_ data: Data) -> Bool {
content.storage.save(page: data, for: id) content.storage.save(page: data, for: identifier)
} }
} }

View File

@ -118,11 +118,11 @@ final class Post: Item, DateItem, LocalizedItem {
A title for the UI, not the generation. A title for the UI, not the generation.
*/ */
override func title(in language: ContentLanguage) -> String { override func title(in language: ContentLanguage) -> String {
localized(in: language).title ?? id localized(in: language).title ?? identifier
} }
func contains(_ string: String) -> Bool { func contains(_ string: String) -> Bool {
id.contains(string) || identifier.contains(string) ||
german.contains(string) || german.contains(string) ||
english.contains(string) english.contains(string)
} }
@ -135,11 +135,11 @@ final class Post: Item, DateItem, LocalizedItem {
@discardableResult @discardableResult
func update(id newId: String) -> Bool { func update(id newId: String) -> Bool {
guard content.storage.move(post: id, to: newId) else { guard content.storage.move(post: identifier, to: newId) else {
print("Failed to move file of post \(id)") print("Failed to move file of post \(identifier)")
return false return false
} }
id = newId identifier = newId
return true return true
} }
@ -149,10 +149,10 @@ final class Post: Item, DateItem, LocalizedItem {
} }
func makePage() -> Page { func makePage() -> Page {
var id = self.id var id = self.identifier
var number = 2 var number = 2
while !content.isNewIdForPage(id) { while !content.isNewIdForPage(id) {
id += "\(self.id)-\(number)" id += "\(self.identifier)-\(number)"
number += 1 number += 1
} }
// Move tags to page // Move tags to page
@ -210,13 +210,13 @@ extension Post: StorageItem {
createdDate: createdDate, createdDate: createdDate,
startDate: startDate, startDate: startDate,
endDate: endDate, endDate: endDate,
tags: tags.map { $0.id }, tags: tags.map { $0.identifier },
german: german.data, german: german.data,
english: english.data, english: english.data,
linkedPageId: linkedPage?.id) linkedPageId: linkedPage?.identifier)
} }
func saveToDisk(_ data: Data) -> Bool { func saveToDisk(_ data: Data) -> Bool {
content.storage.save(post: data, for: id) content.storage.save(post: data, for: identifier)
} }
} }

View File

@ -61,8 +61,8 @@ extension AudioPlayerSettings {
var data: Data { var data: Data {
.init(playlistCoverImageSize: playlistCoverImageSize, .init(playlistCoverImageSize: playlistCoverImageSize,
smallCoverImageSize: smallCoverImageSize, smallCoverImageSize: smallCoverImageSize,
audioPlayerJsFile: audioPlayerJsFile?.id, audioPlayerJsFile: audioPlayerJsFile?.identifier,
audioPlayerCssFile: audioPlayerCssFile?.id, audioPlayerCssFile: audioPlayerCssFile?.identifier,
german: german.data, german: german.data,
english: english.data) english: english.data)
} }

View File

@ -65,7 +65,7 @@ extension GeneralSettings {
remotePortForUpload: remotePortForUpload, remotePortForUpload: remotePortForUpload,
remotePathForUpload: remotePathForUpload, remotePathForUpload: remotePathForUpload,
urlForPushNotification: urlForPushNotification, urlForPushNotification: urlForPushNotification,
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted()) requiredFiles: requiredFiles.nonEmpty?.map { $0.identifier }.sorted())
} }
struct Data: Codable, Equatable { struct Data: Codable, Equatable {

View File

@ -113,13 +113,13 @@ extension PageSettings {
.init(contentWidth: contentWidth, .init(contentWidth: contentWidth,
largeImageWidth: largeImageWidth, largeImageWidth: largeImageWidth,
pageLinkImageSize: pageLinkImageSize, pageLinkImageSize: pageLinkImageSize,
defaultCssFile: defaultCssFile?.id, defaultCssFile: defaultCssFile?.identifier,
codeHighlightingJsFile: codeHighlightingJsFile?.id, codeHighlightingJsFile: codeHighlightingJsFile?.identifier,
modelViewerJsFile: modelViewerJsFile?.id, modelViewerJsFile: modelViewerJsFile?.identifier,
imageCompareJsFile: imageCompareJsFile?.id, imageCompareJsFile: imageCompareJsFile?.identifier,
imageCompareCssFile: imageCompareCssFile?.id, imageCompareCssFile: imageCompareCssFile?.identifier,
manifestFile: manifestFile?.id, manifestFile: manifestFile?.identifier,
routeJsFile: routeJsFile?.id, routeJsFile: routeJsFile?.identifier,
german: german.data, german: german.data,
english: english.data) english: english.data)
} }

View File

@ -72,9 +72,9 @@ extension PostSettings {
var data: PostSettings.Data { var data: PostSettings.Data {
.init(postsPerPage: postsPerPage, .init(postsPerPage: postsPerPage,
contentWidth: contentWidth, contentWidth: contentWidth,
swiperCssFile: swiperCssFile?.id, swiperCssFile: swiperCssFile?.identifier,
swiperJsFile: swiperJsFile?.id, swiperJsFile: swiperJsFile?.identifier,
defaultCssFile: defaultCssFile?.id, defaultCssFile: defaultCssFile?.identifier,
german: german.data, german: german.data,
english: english.data) english: english.data)
} }

View File

@ -37,11 +37,11 @@ class Tag: Item, LocalizedItem {
@discardableResult @discardableResult
func update(id newId: String) -> Bool { func update(id newId: String) -> Bool {
guard content.storage.move(tag: id, to: newId) else { guard content.storage.move(tag: identifier, to: newId) else {
print("Failed to move files of tag \(id)") print("Failed to move files of tag \(identifier)")
return false return false
} }
id = newId identifier = newId
return true return true
} }
@ -106,6 +106,6 @@ extension Tag: StorageItem {
} }
func saveToDisk(_ data: Data) -> Bool { func saveToDisk(_ data: Data) -> Bool {
content.storage.save(tag: data, for: id) content.storage.save(tag: data, for: identifier)
} }
} }

View File

@ -2,6 +2,6 @@
final class TagOverview: Tag { final class TagOverview: Tag {
override var itemId: ItemId { override var itemId: ItemId {
.init(type: .tagOverview, id: id) .init(type: .tagOverview, id: identifier)
} }
} }

View File

@ -78,6 +78,10 @@ struct AddFileView: View {
} }
private func importSelectedFiles() { private func importSelectedFiles() {
guard !filesToAdd.isEmpty else {
dismiss()
return
}
for file in filesToAdd { for file in filesToAdd {
guard file.isSelected else { guard file.isSelected else {
print("Skipping unselected file \(file.uniqueId)") print("Skipping unselected file \(file.uniqueId)")

View File

@ -41,7 +41,7 @@ struct FileContentView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
case .text, .code: case .text, .code:
TextFileContentView(file: file) TextFileContentView(file: file)
.id(file.id + file.modifiedDate.description) .id(file.identifier + file.modifiedDate.description)
case .video: case .video:
VStack { VStack {
if let image = file.imageToDisplay { if let image = file.imageToDisplay {

View File

@ -72,11 +72,12 @@ struct FileDetailView: View {
} }
IdPropertyView( IdPropertyView(
id: $file.id, id: $file.identifier,
title: "Name", title: "Name",
footer: "The unique name of the file, which is also used to reference it in posts and pages.", footer: "The unique name of the file, which is also used to reference it in posts and pages.",
validation: file.isValid, validation: file.isValid,
update: { file.update(id: $0) }) update: { file.update(id: $0) })
.id(file.id)
switch language { switch language {
case .english: case .english:
@ -154,7 +155,7 @@ struct FileDetailView: View {
} }
private func showFileInFinder() { private func showFileInFinder() {
content.storage.openFinderWindow(withSelectedFile: file.id) content.storage.openFinderWindow(withSelectedFile: file.identifier)
} }
private func markFileAsChanged() { private func markFileAsChanged() {
@ -169,11 +170,11 @@ struct FileDetailView: View {
private func replaceFile() { private func replaceFile() {
guard let url = openFilePanel() else { guard let url = openFilePanel() else {
print("File '\(file.id)': No file selected as replacement") print("File '\(file.identifier)': No file selected as replacement")
return return
} }
guard content.storage.importExternalFile(at: url, fileId: file.id) else { guard content.storage.importExternalFile(at: url, fileId: file.identifier) else {
print("File '\(file.id)': Failed to replace file") print("File '\(file.identifier)': Failed to replace file")
return return
} }
@ -197,7 +198,7 @@ struct FileDetailView: View {
let response = panel.runModal() let response = panel.runModal()
guard response == .OK else { guard response == .OK else {
print("File '\(file.id)': Failed to select file to replace") print("File '\(file.identifier)': Failed to select file to replace")
return nil return nil
} }
@ -209,8 +210,8 @@ struct FileDetailView: View {
return return
} }
guard content.storage.removeFileContent(file: file.id) else { guard content.storage.removeFileContent(file: file.identifier) else {
print("File '\(file.id)': Failed to delete file to make it external") print("File '\(file.identifier)': Failed to delete file to make it external")
return return
} }
DispatchQueue.main.async { DispatchQueue.main.async {
@ -220,8 +221,8 @@ struct FileDetailView: View {
} }
private func deleteFile() { private func deleteFile() {
guard content.storage.delete(file: file.id) else { guard content.storage.delete(file: file.identifier) else {
print("File '\(file.id)': Failed to delete file in content folder") print("File '\(file.identifier)': Failed to delete file in content folder")
return return
} }
content.remove(file) content.remove(file)

View File

@ -32,7 +32,7 @@ struct FileListView: View {
guard !searchString.isEmpty else { guard !searchString.isEmpty else {
return filesBySelectedType return filesBySelectedType
} }
return filesBySelectedType.filter { $0.id.contains(searchString) } return filesBySelectedType.filter { $0.identifier.contains(searchString) }
} }
var body: some View { var body: some View {
@ -55,10 +55,10 @@ struct FileListView: View {
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
ForEach(filteredFiles) { file in ForEach(filteredFiles) { file in
SelectableListItem(selected: selectedFile == file) { SelectableListItem(selected: selectedFile == file) {
Text(file.id) Text(file.identifier)
.lineLimit(1) .lineLimit(1)
} }
.id(file.id) .id(file.identifier)
.onTapGesture { .onTapGesture {
selectedFile = file selectedFile = file
} }

View File

@ -30,7 +30,7 @@ final class FileToAdd: ObservableObject {
} }
var idAlreadyExists: Bool { var idAlreadyExists: Bool {
content.files.contains { $0.id == uniqueId } content.files.contains { $0.identifier == uniqueId }
} }
} }

View File

@ -43,7 +43,7 @@ struct MultiFileSelectionView: View {
guard !searchString.isEmpty else { guard !searchString.isEmpty else {
return filesBySelectedType return filesBySelectedType
} }
return filesBySelectedType.filter { $0.id.contains(searchString) } return filesBySelectedType.filter { $0.identifier.contains(searchString) }
} }
var body: some View { var body: some View {
@ -59,7 +59,7 @@ struct MultiFileSelectionView: View {
.foregroundStyle(.red) .foregroundStyle(.red)
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { deselect(file: file) } .onTapGesture { deselect(file: file) }
Text(file.id) Text(file.identifier)
Spacer() Spacer()
} }
} }
@ -99,7 +99,7 @@ struct MultiFileSelectionView: View {
Image(systemSymbol: .plusCircleFill) Image(systemSymbol: .plusCircleFill)
.foregroundStyle(.green) .foregroundStyle(.green)
} }
Text(file.id) Text(file.identifier)
Spacer() Spacer()
} }
.contentShape(Rectangle()) .contentShape(Rectangle())

View File

@ -49,9 +49,9 @@ struct TextFileContentView: View {
private func reload() { private func reload() {
fileContent = file.textContent() fileContent = file.textContent()
loadedFile = file.id loadedFile = file.identifier
loadedFileDate = file.modifiedDate loadedFileDate = file.modifiedDate
print("Loaded content of file \(file.id)") print("Loaded content of file \(file.identifier)")
} }
private func save() { private func save() {
@ -59,25 +59,25 @@ struct TextFileContentView: View {
print("[ERROR] Text File View: No file loaded to save") print("[ERROR] Text File View: No file loaded to save")
return return
} }
guard loadedFile == file.id else { guard loadedFile == file.identifier else {
print("[ERROR] Text File View: Not saving since file changed") print("[ERROR] Text File View: Not saving since file changed")
reload() reload()
return return
} }
guard loadedFileDate == file.modifiedDate else { guard loadedFileDate == file.modifiedDate else {
print("Text File View: Not saving changed file \(file.id)") print("Text File View: Not saving changed file \(file.identifier)")
reload() reload()
return return
} }
guard fileContent != "" else { guard fileContent != "" else {
print("Text File View: Not saving empty file \(file.id)") print("Text File View: Not saving empty file \(file.identifier)")
return return
} }
guard file.save(textContent: fileContent) else { guard file.save(textContent: fileContent) else {
print("[ERROR] Text File View: Failed to save file \(file.id)") print("[ERROR] Text File View: Failed to save file \(file.identifier)")
return return
} }
loadedFileDate = file.modifiedDate loadedFileDate = file.modifiedDate
print("Text File View: Saved file \(file.id)") print("Text File View: Saved file \(file.identifier)")
} }
} }

View File

@ -72,11 +72,11 @@ struct GenerationContentView: View {
GenerationStringIssuesView( GenerationStringIssuesView(
text: "required files", text: "required files",
statusWhenNonEmpty: .nominal, statusWhenNonEmpty: .nominal,
items: content.results.requiredFiles) { $0.id } items: content.results.requiredFiles) { $0.identifier }
GenerationStringIssuesView( GenerationStringIssuesView(
text: "external files", text: "external files",
statusWhenNonEmpty: .nominal, statusWhenNonEmpty: .nominal,
items: content.results.externalFiles) { $0.id } items: content.results.externalFiles) { $0.identifier }
GenerationIssuesView( GenerationIssuesView(
text: "empty pages", text: "empty pages",
statusWhenNonEmpty: .warning, statusWhenNonEmpty: .warning,
@ -96,14 +96,14 @@ struct GenerationContentView: View {
statusWhenNonEmpty: .warning, statusWhenNonEmpty: .warning,
items: draftPages, items: draftPages,
buttonText: "Show", buttonText: "Show",
itemText: { $0.id }, itemText: { $0.identifier },
action: { show($0) }) action: { show($0) })
GenerationIssuesActionView( GenerationIssuesActionView(
title: "draft posts", title: "draft posts",
statusWhenNonEmpty: .warning, statusWhenNonEmpty: .warning,
items: draftPosts, items: draftPosts,
buttonText: "Show", buttonText: "Show",
itemText: { $0.id }, itemText: { $0.identifier },
action: { show($0) }) action: { show($0) })
GenerationIssuesView( GenerationIssuesView(
text: "additional output files", text: "additional output files",
@ -117,10 +117,10 @@ struct GenerationContentView: View {
} }
GenerationStringIssuesView( GenerationStringIssuesView(
text: "inaccessible files", text: "inaccessible files",
items: content.results.inaccessibleFiles) { $0.id } items: content.results.inaccessibleFiles) { $0.identifier }
GenerationStringIssuesView( GenerationStringIssuesView(
text: "unparsable files", text: "unparsable files",
items: content.results.unparsableFiles) { $0.id } items: content.results.unparsableFiles) { $0.identifier }
GenerationStringIssuesView( GenerationStringIssuesView(
text: "unsaved output files", text: "unsaved output files",
items: content.results.unsavedOutputFiles) items: content.results.unsavedOutputFiles)

View File

@ -24,7 +24,7 @@ struct FilePropertyView: View {
var body: some View { var body: some View {
GenericPropertyView(title: title, footer: footer) { GenericPropertyView(title: title, footer: footer) {
HStack { HStack {
Text(selectedFile?.id ?? "No file selected") Text(selectedFile?.identifier ?? "No file selected")
Spacer() Spacer()
Button("Select") { Button("Select") {
showFileSelectionSheet = true showFileSelectionSheet = true

View File

@ -36,7 +36,7 @@ struct OptionalImagePropertyView: View {
} }
HStack { HStack {
Text(selectedImage?.id ?? "No file selected") Text(selectedImage?.identifier ?? "No file selected")
Spacer() Spacer()
Button("Select") { Button("Select") {
showSelectionSheet = true showSelectionSheet = true

View File

@ -15,7 +15,7 @@ struct PagePropertyView: View {
var body: some View { var body: some View {
GenericPropertyView(title: title, footer: footer) { GenericPropertyView(title: title, footer: footer) {
HStack { HStack {
Text(selectedPage?.id ?? "No page selected") Text(selectedPage?.identifier ?? "No page selected")
Spacer() Spacer()
Button("Select") { Button("Select") {
showPageSelectionSheet = true showPageSelectionSheet = true

View File

@ -16,7 +16,7 @@ struct TagDisplayView: View {
var body: some View { var body: some View {
FlowHStack { FlowHStack {
ForEach(tags, id: \.id) { tag in ForEach(tags, id: \.identifier) { tag in
TagView(text: tag.localized(in: language).name) TagView(text: tag.localized(in: language).name)
.foregroundStyle(.white) .foregroundStyle(.white)
} }

View File

@ -27,7 +27,7 @@ struct TagPickerView: View {
Text("Select a tag to link to") Text("Select a tag to link to")
List(content.tags, selection: $newSelection) { tag in List(content.tags, selection: $newSelection) { tag in
let loc = tag.localized(in: language) let loc = tag.localized(in: language)
Text("\(loc.title) (\(tag.id))") Text("\(loc.title) (\(tag.identifier))")
.tag(tag) .tag(tag)
} }
.frame(minHeight: 300) .frame(minHeight: 300)

View File

@ -15,7 +15,7 @@ struct TagPropertyView: View {
var body: some View { var body: some View {
GenericPropertyView(title: title, footer: footer) { GenericPropertyView(title: title, footer: footer) {
HStack { HStack {
Text(selectedTag?.id ?? "No tag selected") Text(selectedTag?.identifier ?? "No tag selected")
Spacer() Spacer()
Button("Select") { Button("Select") {
showTagSelectionSheet = true showTagSelectionSheet = true

View File

@ -20,7 +20,7 @@ final class InsertableFileButton: ObservableObject {
""" """
icon: \(label.icon.rawValue) icon: \(label.icon.rawValue)
text: \(label.value) text: \(label.value)
file: \(file.id) file: \(file.identifier)
""" """
guard let downloadedFileName else { guard let downloadedFileName else {
return result return result
@ -86,7 +86,7 @@ struct InsertableButtons: View, InsertableCommandView {
var id: String { var id: String {
switch self { switch self {
case .file(let file): case .file(let file):
return "file-\(file.file?.id ?? "none")" return "file-\(file.file?.identifier ?? "none")"
case .url(let url): case .url(let url):
return "url-\(url.url)" return "url-\(url.url)"
case .event(let event): case .event(let event):
@ -161,7 +161,7 @@ private struct FileButtonView: View {
var body: some View { var body: some View {
HStack { HStack {
LabelEditingView(label: $content.label) LabelEditingView(label: $content.label)
Button("\(content.file?.id ?? "Select file")", action: { showFileSelectionSheet = true }) Button("\(content.file?.identifier ?? "Select file")", action: { showFileSelectionSheet = true })
OptionalTextField("", text: $content.downloadedFileName, prompt: "Downloaded file name") OptionalTextField("", text: $content.downloadedFileName, prompt: "Downloaded file name")
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
} }

View File

@ -28,7 +28,7 @@ struct InsertableGallery: View, InsertableCommandView {
} }
return ( return (
["```\(GalleryBlock.blockId)"] + ["```\(GalleryBlock.blockId)"] +
images.map { $0.id } + images.map { $0.identifier } +
["```"] ["```"]
).joined(separator: "\n") ).joined(separator: "\n")
} }

View File

@ -24,9 +24,9 @@ struct InsertableImage: View, InsertableCommandView {
return nil return nil
} }
guard let caption else { guard let caption else {
return "![image](\(selectedImage.id))" return "![image](\(selectedImage.identifier))"
} }
return "![image](\(selectedImage.id);\(caption))" return "![image](\(selectedImage.identifier);\(caption))"
} }
} }

View File

@ -45,11 +45,11 @@ struct InsertableLink: View, InsertableCommandView {
case .post, .tagOverview: case .post, .tagOverview:
return nil return nil
case .page: case .page:
return selectedPage?.id return selectedPage?.identifier
case .tag: case .tag:
return selectedTag?.id return selectedTag?.identifier
case .file: case .file:
return selectedFile?.id return selectedFile?.identifier
} }
} }

View File

@ -40,8 +40,8 @@ struct InsertableRoute: View, InsertableCommandView {
return nil return nil
} }
var result = ["```route"] var result = ["```route"]
result.append("\(RouteBlock.Key.image.rawValue): \(selectedImage.id)") result.append("\(RouteBlock.Key.image.rawValue): \(selectedImage.identifier)")
result.append("\(RouteBlock.Key.file.rawValue): \(dataFile.id)") result.append("\(RouteBlock.Key.file.rawValue): \(dataFile.identifier)")
if components != Set(RouteStatisticType.allCases) { if components != Set(RouteStatisticType.allCases) {
let list = components let list = components
.map { $0.rawValue } .map { $0.rawValue }

View File

@ -50,16 +50,16 @@ struct InsertableVideo: View, InsertableCommandView {
var lines: [String] = [] var lines: [String] = []
lines.append("```video") lines.append("```video")
if let posterImage { if let posterImage {
lines.append("\(VideoBlock.Key.poster): \(posterImage.id)") lines.append("\(VideoBlock.Key.poster): \(posterImage.identifier)")
} }
if let videoH265 { if let videoH265 {
lines.append("\(VideoBlock.Key.h265): \(videoH265.id)") lines.append("\(VideoBlock.Key.h265): \(videoH265.identifier)")
} }
if let videoH264 { if let videoH264 {
lines.append("\(VideoBlock.Key.h264): \(videoH264.id)") lines.append("\(VideoBlock.Key.h264): \(videoH264.identifier)")
} }
if let videoWebm { if let videoWebm {
lines.append("\(VideoBlock.Key.webm): \(videoWebm.id)") lines.append("\(VideoBlock.Key.webm): \(videoWebm.identifier)")
} }
if controls { lines.append(VideoBlock.Key.controls.rawValue) } if controls { lines.append(VideoBlock.Key.controls.rawValue) }
if autoplay { lines.append(VideoBlock.Key.autoplay.rawValue) } if autoplay { lines.append(VideoBlock.Key.autoplay.rawValue) }

View File

@ -35,7 +35,7 @@ struct PageContentResultsView: View {
TextWithSymbol( TextWithSymbol(
symbol: $0.type.category.symbol, symbol: $0.type.category.symbol,
color: .blue, color: .blue,
text: $0.id) text: $0.identifier)
} }
+ results.missingFiles.keys.map { + results.missingFiles.keys.map {
TextWithSymbol( TextWithSymbol(

View File

@ -32,7 +32,7 @@ struct PageContentView: View {
if page.isExternalUrl { if page.isExternalUrl {
VStack { VStack {
PageTitleView(page: page.localized(in: language)) PageTitleView(page: page.localized(in: language))
.id(page.id + language.rawValue) .id(page.identifier + language.rawValue)
Spacer() Spacer()
Text("No content available for external page") Text("No content available for external page")
.font(.title) .font(.title)
@ -42,10 +42,10 @@ struct PageContentView: View {
} else { } else {
VStack(alignment: .leading) { VStack(alignment: .leading) {
PageTitleView(page: page.localized(in: language)) PageTitleView(page: page.localized(in: language))
.id(page.id + language.rawValue) .id(page.identifier + language.rawValue)
TagDisplayView(tags: $page.tags) TagDisplayView(tags: $page.tags)
LocalizedPageContentView(page: page, language: language) LocalizedPageContentView(page: page, language: language)
.id(page.id + language.rawValue) .id(page.identifier + language.rawValue)
} }
.padding() .padding()
} }

View File

@ -30,7 +30,7 @@ struct PageDetailView: View {
title: "Page", title: "Page",
text: "A page contains longer content") text: "A page contains longer content")
IdPropertyView( IdPropertyView(
id: $page.id, id: $page.identifier,
footer: "The page id is used to link to it internally.", footer: "The page id is used to link to it internally.",
validation: page.isValid, validation: page.isValid,
update: { page.update(id: $0) }) update: { page.update(id: $0) })
@ -75,7 +75,7 @@ struct PageDetailView: View {
isExternalPage: page.isExternalUrl, isExternalPage: page.isExternalUrl,
page: page.localized(in: language), page: page.localized(in: language),
transferImage: transferImage) transferImage: transferImage)
.id(page.id + language.rawValue) .id(page.identifier + language.rawValue)
ColoredButton(delete: deletePage) ColoredButton(delete: deletePage)
} }
.padding() .padding()
@ -83,8 +83,8 @@ struct PageDetailView: View {
} }
private func deletePage() { private func deletePage() {
guard content.storage.delete(page: page.id) else { guard content.storage.delete(page: page.identifier) else {
print("Page '\(page.id)': Failed to delete file in content folder") print("Page '\(page.identifier)': Failed to delete file in content folder")
return return
} }
content.remove(page) content.remove(page)

View File

@ -27,7 +27,7 @@ struct PagePickerView: View {
Text("Select a page to link to") Text("Select a page to link to")
List(content.pages, selection: $newSelection) { page in List(content.pages, selection: $newSelection) { page in
let loc = page.localized(in: language) let loc = page.localized(in: language)
Text("\(loc.title) (\(page.id))") Text("\(loc.title) (\(page.identifier))")
.tag(page) .tag(page)
} }
.frame(minHeight: 300) .frame(minHeight: 300)

View File

@ -43,7 +43,7 @@ struct PostDetailView: View {
} }
IdPropertyView( IdPropertyView(
id: $post.id, id: $post.identifier,
footer: "The id is used to link to post and store them", footer: "The id is used to link to post and store them",
validation: post.isValid, validation: post.isValid,
update: { post.update(id: $0) }) update: { post.update(id: $0) })
@ -99,8 +99,8 @@ struct PostDetailView: View {
} }
private func deletePost() { private func deletePost() {
guard content.storage.delete(post: post.id) else { guard content.storage.delete(post: post.identifier) else {
print("Post '\(post.id)': Failed to delete file in content folder") print("Post '\(post.identifier)': Failed to delete file in content folder")
return return
} }
content.remove(post) content.remove(post)

View File

@ -18,7 +18,7 @@ struct PostImageView: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(height: 100) .frame(height: 100)
Text(image.id) Text(image.identifier)
.font(.title) .font(.title)
Text("Failed to load image") Text("Failed to load image")
.font(.body) .font(.body)
@ -32,7 +32,7 @@ struct PostImageView: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(height: 100) .frame(height: 100)
Text(image.id) Text(image.identifier)
.font(.title) .font(.title)
Button("Generate preview") { Button("Generate preview") {
generateVideoPreview(image) generateVideoPreview(image)
@ -48,7 +48,7 @@ struct PostImageView: View {
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(height: 100) .frame(height: 100)
Text(image.id) Text(image.identifier)
.font(.title) .font(.title)
Text("Invalid media type") Text("Invalid media type")
.font(.body) .font(.body)

View File

@ -10,7 +10,7 @@ private struct PostListItem: View {
var body: some View { var body: some View {
HStack { HStack {
LocalizedPostListItem(id: post.id, post: post.localized(in: language)) LocalizedPostListItem(id: post.identifier, post: post.localized(in: language))
if post.isDraft { if post.isDraft {
TextIndicator(text: "Draft", background: .yellow) TextIndicator(text: "Draft", background: .yellow)
} else { } else {

View File

@ -32,7 +32,7 @@ struct TagDetailView: View {
footer: "Indicate if the tag should appear in the tag list of posts and pages. If the tag is not visible, then it can still be used as a filter.") footer: "Indicate if the tag should appear in the tag list of posts and pages. If the tag is not visible, then it can still be used as a filter.")
IdPropertyView( IdPropertyView(
id: $tag.id, id: $tag.identifier,
title: "Tag id", title: "Tag id",
footer: "The unique id of the tag for references", footer: "The unique id of the tag for references",
validation: tag.isValid) { validation: tag.isValid) {
@ -42,7 +42,7 @@ struct TagDetailView: View {
LocalizedTagDetailView( LocalizedTagDetailView(
tag: tag.localized(in: language), tag: tag.localized(in: language),
transferImage: transferImage) transferImage: transferImage)
.id(tag.id + language.rawValue) .id(tag.identifier + language.rawValue)
ColoredButton(delete: deleteTag) ColoredButton(delete: deleteTag)
} }
.padding() .padding()
@ -50,8 +50,8 @@ struct TagDetailView: View {
} }
private func deleteTag() { private func deleteTag() {
guard content.storage.delete(tag: tag.id) else { guard content.storage.delete(tag: tag.identifier) else {
print("Tag '\(tag.id)': Failed to delete file in content folder") print("Tag '\(tag.identifier)': Failed to delete file in content folder")
return return
} }
content.remove(tag) content.remove(tag)