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

@ -69,7 +69,7 @@ extension Content {
continue
}
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)
}
}
@ -147,23 +147,23 @@ extension Content {
// MARK: Find items by id
func page(_ pageId: String) -> Page? {
pages.first { $0.id == pageId }
pages.first { $0.identifier == pageId }
}
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? {
files.first { $0.id == videoId && $0.type.isVideo }
files.first { $0.identifier == videoId && $0.type.isVideo }
}
func file(_ fileId: String) -> FileResource? {
files.first { $0.id == fileId }
files.first { $0.identifier == fileId }
}
func tag(_ tagId: String) -> Tag? {
tags.first { $0.id == tagId }
tags.first { $0.identifier == tagId }
}
// MARK: Generation input
@ -322,12 +322,12 @@ extension Content {
let pageUrl = settings.general.url + relativePageUrl
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
}
guard storage.write(content, to: filePath) else {
print("Failed to save page \(page.id)")
print("Failed to save page \(page.identifier)")
return
}

View File

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

View File

@ -7,19 +7,19 @@ extension Content {
private static let disallowedCharactersInFileIds = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted
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 {
!pages.contains { $0.id == id }
!pages.contains { $0.identifier == id }
}
func isNewIdForPost(_ id: String) -> Bool {
!posts.contains { $0.id == id }
!posts.contains { $0.identifier == id }
}
func isNewIdForFile(_ id: String) -> Bool {
!files.contains { $0.id == id }
!files.contains { $0.identifier == id }
}
func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {

View File

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

View File

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

View File

@ -49,18 +49,18 @@ final class FileResource: Item, LocalizedItem {
/// The dimensions of the image
var imageDimensions: CGSize? {
get { content.dimensions(of: id) }
get { content.dimensions(of: identifier) }
set {
content.cache(dimensions: newValue, of: id)
content.cache(dimensions: newValue, of: identifier)
didChange(save: false)
}
}
/// The size of the file in bytes
var fileSize: Int? {
get { content.size(of: id) }
get { content.size(of: identifier) }
set {
content.cache(size: newValue, of: id)
content.cache(size: newValue, of: identifier)
didChange(save: false)
}
}
@ -114,11 +114,11 @@ final class FileResource: Item, LocalizedItem {
// MARK: Text
func textContent() -> String {
content.storage.fileContent(for: id) ?? ""
content.storage.fileContent(for: identifier) ?? ""
}
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
}
modifiedDate = .now
@ -126,7 +126,7 @@ final class FileResource: Item, LocalizedItem {
}
func dataContent() -> Foundation.Data? {
content.storage.fileData(for: id)
content.storage.fileData(for: identifier)
}
// MARK: Images
@ -165,7 +165,7 @@ final class FileResource: Item, LocalizedItem {
}
update(fileSize: displayImageData.count)
guard let loadedImage = NSImage(data: displayImageData) else {
print("Failed to create image \(id)")
print("Failed to create image \(identifier)")
return nil
}
update(imageDimensions: loadedImage.size)
@ -191,14 +191,14 @@ final class FileResource: Item, LocalizedItem {
private var displayImageData: Foundation.Data? {
if type.isImage {
guard let data = content.storage.fileData(for: id) else {
print("Failed to load data for image \(id)")
guard let data = content.storage.fileData(for: identifier) else {
print("Failed to load data for image \(identifier)")
return nil
}
return data
}
if type.isVideo {
return content.storage.getVideoThumbnail(for: id)
return content.storage.getVideoThumbnail(for: identifier)
}
return nil
}
@ -234,7 +234,7 @@ final class FileResource: Item, LocalizedItem {
func determineFileSize() {
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)
}
}
@ -249,7 +249,7 @@ final class FileResource: Item, LocalizedItem {
/// The path to the output folder where image versions are stored (no leading slash)
var outputImageFolder: String {
"\(content.settings.paths.imagesOutputFolderPath)/\(id.fileNameWithoutExtension)"
"\(content.settings.paths.imagesOutputFolderPath)/\(identifier.fileNameWithoutExtension)"
}
func outputPath(width: Int, height: Int, type: FileType?) -> String {
@ -293,9 +293,9 @@ final class FileResource: Item, LocalizedItem {
func createVideoThumbnail() {
guard type.isVideo else { return }
guard !content.storage.hasVideoThumbnail(for: id) else { return }
guard !content.storage.hasVideoThumbnail(for: identifier) else { return }
Task {
if await content.imageGenerator.createVideoThumbnail(for: id) {
if await content.imageGenerator.createVideoThumbnail(for: identifier) {
didChange()
}
}
@ -322,7 +322,7 @@ final class FileResource: Item, LocalizedItem {
return "/" + customOutputPath
}
}
let path = pathPrefix + "/" + id
let path = pathPrefix + "/" + identifier
return makeCleanAbsolutePath(path)
}
@ -353,14 +353,14 @@ final class FileResource: Item, LocalizedItem {
@discardableResult
func update(id newId: String) -> Bool {
guard !isExternallyStored else {
id = newId
identifier = newId
return true
}
guard content.storage.move(file: id, to: newId) else {
print("Failed to move file \(id) to \(newId)")
guard content.storage.move(file: identifier, to: newId) else {
print("Failed to move file \(identifier) to \(newId)")
return false
}
id = newId
identifier = newId
return true
}
}
@ -368,7 +368,7 @@ final class FileResource: Item, LocalizedItem {
extension FileResource: CustomStringConvertible {
var description: String {
id
identifier
}
}
@ -418,6 +418,6 @@ extension FileResource: StorageItem {
}
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
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
var id: String
var identifier: String
init(content: Content, id: String) {
self.id = id
self.identifier = id
super.init(content: content)
observeChanges()
@ -44,14 +50,14 @@ class Item: ChangeObservingItem, Identifiable {
}
var itemId: ItemId {
.init(type: itemType, id: id)
.init(type: itemType, id: identifier)
}
}
extension Item: Equatable {
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) {
hasher.combine(id)
hasher.combine(itemType)
}
}
extension Item: Comparable {
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:
return "1-feed"
case .post(let post):
return "2-post-\(post.id)"
return "2-post-\(post.identifier)"
case .page(let page):
return "3-page-\(page.id)"
return "3-page-\(page.identifier)"
case .tagPage(let tag):
return "5-tag-\(tag.id)"
return "5-tag-\(tag.identifier)"
case .tagOverview:
return "4-tag-overview"
}
@ -76,11 +76,11 @@ extension ItemReference: CustomStringConvertible {
case .feed:
return "Feed"
case .post(let post):
return "Post \(post.id)"
return "Post \(post.identifier)"
case .page(let page):
return "Page \(page.id)"
return "Page \(page.identifier)"
case .tagPage(let tag):
return "Tag \(tag.id)"
return "Tag \(tag.identifier)"
case .tagOverview:
return "Tag Overview"
}

View File

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

View File

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

View File

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

View File

@ -75,11 +75,11 @@ final class Page: Item, DateItem, LocalizedItem {
@discardableResult
func update(id newId: String) -> Bool {
guard content.storage.move(page: id, to: newId) else {
print("Failed to move files of page \(id)")
guard content.storage.move(page: identifier, to: newId) else {
print("Failed to move files of page \(identifier)")
return false
}
id = newId
identifier = newId
return true
}
@ -146,11 +146,11 @@ final class Page: Item, DateItem, LocalizedItem {
}
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 {
guard content.storage.remove(pageContent: id, in: language) else {
guard content.storage.remove(pageContent: identifier, in: language) else {
return 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 {
guard content.storage.save(pageContent: pageContent, for: id, in: language) else {
guard content.storage.save(pageContent: pageContent, for: identifier, in: language) else {
return false
}
if localized(in: language).update(hasContent: true) {
@ -175,7 +175,7 @@ final class Page: Item, DateItem, LocalizedItem {
func updateContentExistence() {
var didUpdate = false
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) {
didUpdate = true
}
@ -234,7 +234,7 @@ extension Page: StorageItem {
.init(
isDraft: isDraft,
externalLink: externalLink,
tags: tags.map { $0.id },
tags: tags.map { $0.identifier },
hideDate: hideDate ? true : nil,
createdDate: createdDate,
startDate: startDate,
@ -244,6 +244,6 @@ extension Page: StorageItem {
}
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.
*/
override func title(in language: ContentLanguage) -> String {
localized(in: language).title ?? id
localized(in: language).title ?? identifier
}
func contains(_ string: String) -> Bool {
id.contains(string) ||
identifier.contains(string) ||
german.contains(string) ||
english.contains(string)
}
@ -135,11 +135,11 @@ final class Post: Item, DateItem, LocalizedItem {
@discardableResult
func update(id newId: String) -> Bool {
guard content.storage.move(post: id, to: newId) else {
print("Failed to move file of post \(id)")
guard content.storage.move(post: identifier, to: newId) else {
print("Failed to move file of post \(identifier)")
return false
}
id = newId
identifier = newId
return true
}
@ -149,10 +149,10 @@ final class Post: Item, DateItem, LocalizedItem {
}
func makePage() -> Page {
var id = self.id
var id = self.identifier
var number = 2
while !content.isNewIdForPage(id) {
id += "\(self.id)-\(number)"
id += "\(self.identifier)-\(number)"
number += 1
}
// Move tags to page
@ -210,13 +210,13 @@ extension Post: StorageItem {
createdDate: createdDate,
startDate: startDate,
endDate: endDate,
tags: tags.map { $0.id },
tags: tags.map { $0.identifier },
german: german.data,
english: english.data,
linkedPageId: linkedPage?.id)
linkedPageId: linkedPage?.identifier)
}
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 {
.init(playlistCoverImageSize: playlistCoverImageSize,
smallCoverImageSize: smallCoverImageSize,
audioPlayerJsFile: audioPlayerJsFile?.id,
audioPlayerCssFile: audioPlayerCssFile?.id,
audioPlayerJsFile: audioPlayerJsFile?.identifier,
audioPlayerCssFile: audioPlayerCssFile?.identifier,
german: german.data,
english: english.data)
}

View File

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

View File

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

View File

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

View File

@ -37,11 +37,11 @@ class Tag: Item, LocalizedItem {
@discardableResult
func update(id newId: String) -> Bool {
guard content.storage.move(tag: id, to: newId) else {
print("Failed to move files of tag \(id)")
guard content.storage.move(tag: identifier, to: newId) else {
print("Failed to move files of tag \(identifier)")
return false
}
id = newId
identifier = newId
return true
}
@ -106,6 +106,6 @@ extension Tag: StorageItem {
}
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 {
override var itemId: ItemId {
.init(type: .tagOverview, id: id)
.init(type: .tagOverview, id: identifier)
}
}