Add images to posts, saving
This commit is contained in:
parent
cb22ae34f2
commit
a35c2d669e
@ -10,6 +10,14 @@
|
||||
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; };
|
||||
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; };
|
||||
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500C2CEE07140090B18B /* ColorPalette.swift */; };
|
||||
E21850112CEE17070090B18B /* Page+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850102CEE17010090B18B /* Page+Storage.swift */; };
|
||||
E21850132CEE541D0090B18B /* Post+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850122CEE541A0090B18B /* Post+Storage.swift */; };
|
||||
E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850142CEE55D40090B18B /* FileOnDisk.swift */; };
|
||||
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
|
||||
E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; };
|
||||
E218501B2CEE59EC0090B18B /* Tag+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501A2CEE59E80090B18B /* Tag+Storage.swift */; };
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.swift */; };
|
||||
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */; };
|
||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
|
||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
|
||||
@ -73,6 +81,14 @@
|
||||
E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = "<group>"; };
|
||||
E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = "<group>"; };
|
||||
E218500C2CEE07140090B18B /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = "<group>"; };
|
||||
E21850102CEE17010090B18B /* Page+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Storage.swift"; sourceTree = "<group>"; };
|
||||
E21850122CEE541A0090B18B /* Post+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Storage.swift"; sourceTree = "<group>"; };
|
||||
E21850142CEE55D40090B18B /* FileOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOnDisk.swift; sourceTree = "<group>"; };
|
||||
E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
|
||||
E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = "<group>"; };
|
||||
E218501A2CEE59E80090B18B /* Tag+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Storage.swift"; sourceTree = "<group>"; };
|
||||
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = "<group>"; };
|
||||
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
|
||||
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
|
||||
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
|
||||
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = "<group>"; };
|
||||
@ -176,6 +192,7 @@
|
||||
E2A21C372CB9A4F10060935B /* Generic */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */,
|
||||
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
|
||||
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
|
||||
E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
|
||||
@ -204,10 +221,13 @@
|
||||
E2A37D0F2CE5375E0000979F /* Storage */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2A37D162CE73F170000979F /* TagFile.swift */,
|
||||
E21850142CEE55D40090B18B /* FileOnDisk.swift */,
|
||||
E21850162CEE55FB0090B18B /* FileType.swift */,
|
||||
E2A37D102CE537670000979F /* PageFile.swift */,
|
||||
E21850182CEE561B0090B18B /* PageOnDisk.swift */,
|
||||
E2A37D142CE68BEA0000979F /* PostFile.swift */,
|
||||
E2A37D0D2CE527040000979F /* Storage.swift */,
|
||||
E2A37D162CE73F170000979F /* TagFile.swift */,
|
||||
);
|
||||
path = Storage;
|
||||
sourceTree = "<group>";
|
||||
@ -231,9 +251,12 @@
|
||||
E25A0B882CE4021400F33674 /* LocalizedPage.swift */,
|
||||
E2A21C042CB176670060935B /* LocalizedText.swift */,
|
||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */,
|
||||
E21850102CEE17010090B18B /* Page+Storage.swift */,
|
||||
E2B85F3A2C428F0D0047CD0C /* Post.swift */,
|
||||
E21850122CEE541A0090B18B /* Post+Storage.swift */,
|
||||
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */,
|
||||
E2581DEC2C75202400F1F079 /* Tag.swift */,
|
||||
E218501A2CEE59E80090B18B /* Tag+Storage.swift */,
|
||||
E2A37D182CEA36A40000979F /* LocalizedTag.swift */,
|
||||
);
|
||||
path = Model;
|
||||
@ -277,6 +300,7 @@
|
||||
E2B85F4B2C4B8B7F0047CD0C /* Posts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */,
|
||||
E21850082CEE01BF0090B18B /* PagePickerView.swift */,
|
||||
E2A21C112CB18D520060935B /* DatePickerView.swift */,
|
||||
E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */,
|
||||
@ -432,6 +456,7 @@
|
||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */,
|
||||
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
|
||||
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */,
|
||||
E21850172CEE55FC0090B18B /* FileType.swift in Sources */,
|
||||
E2A37D112CE537800000979F /* PageFile.swift in Sources */,
|
||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */,
|
||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
||||
@ -450,24 +475,31 @@
|
||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */,
|
||||
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */,
|
||||
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
||||
E218501B2CEE59EC0090B18B /* Tag+Storage.swift in Sources */,
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
|
||||
E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */,
|
||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */,
|
||||
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
||||
E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */,
|
||||
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */,
|
||||
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */,
|
||||
E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */,
|
||||
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
||||
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
||||
E21850112CEE17070090B18B /* Page+Storage.swift in Sources */,
|
||||
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */,
|
||||
E21850132CEE541D0090B18B /* Post+Storage.swift in Sources */,
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
||||
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */,
|
||||
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
||||
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
||||
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
||||
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */,
|
||||
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
||||
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
|
||||
|
@ -59,9 +59,17 @@ struct CHDataManagementApp: App {
|
||||
}
|
||||
}
|
||||
.onAppear(perform: importOldContent)
|
||||
.onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func save() {
|
||||
// Save all changed files
|
||||
content.saveToDisk()
|
||||
}
|
||||
|
||||
private func importOldContent() {
|
||||
do {
|
||||
try content.loadFromDisk()
|
||||
|
@ -8,4 +8,8 @@ extension String {
|
||||
.replacingOccurrences(of: "<", with: "<")
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
}
|
||||
|
||||
var nonEmpty: String? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
}
|
||||
|
@ -1,69 +1,14 @@
|
||||
import Foundation
|
||||
|
||||
enum FileType {
|
||||
case image
|
||||
case file
|
||||
case video
|
||||
case resource
|
||||
|
||||
|
||||
init(fileExtension: String) {
|
||||
switch fileExtension.lowercased() {
|
||||
case "jpg", "jpeg", "png", "gif":
|
||||
self = .image
|
||||
case "html", "stl", "f3d", "step", "f3z", "zip", "json", "conf", "css", "js", "cpp", "cddx", "svg", "glb", "mp3", "pdf", "swift":
|
||||
self = .file
|
||||
case "mp4":
|
||||
self = .video
|
||||
case "key", "psd":
|
||||
self = .resource
|
||||
default:
|
||||
print("Unhandled file type: \(fileExtension)")
|
||||
self = .resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImportedPage {
|
||||
|
||||
let page: PageFile
|
||||
|
||||
let deContentUrl: URL
|
||||
|
||||
let enContentUrl: URL
|
||||
}
|
||||
|
||||
|
||||
struct FileResource {
|
||||
|
||||
let type: FileType
|
||||
|
||||
let url: URL
|
||||
|
||||
let name: String
|
||||
|
||||
init(image: String, url: URL) {
|
||||
self.type = .image
|
||||
self.url = url
|
||||
self.name = image
|
||||
}
|
||||
|
||||
init(type: FileType, url: URL, name: String) {
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
final class Importer {
|
||||
|
||||
var posts: [String : PostFile] = [:]
|
||||
|
||||
var pages: [String : ImportedPage] = [:]
|
||||
var pages: [String : PageOnDisk] = [:]
|
||||
|
||||
var tags: [String : TagFile] = [:]
|
||||
|
||||
var files: [String : FileResource] = [:]
|
||||
var files: [String : FileOnDisk] = [:]
|
||||
|
||||
var ignoredFiles: [URL] = []
|
||||
|
||||
@ -99,9 +44,9 @@ final class Importer {
|
||||
let meta = try JSONDecoder().decode(ImportableTag.self, from: data)
|
||||
|
||||
let thumbnailUrl = folder.appending(path: "thumbnail.jpg", directoryHint: .notDirectory)
|
||||
var thumbnail: FileResource? = nil
|
||||
var thumbnail: FileOnDisk? = nil
|
||||
if FileManager.default.fileExists(atPath: thumbnailUrl.path()) {
|
||||
thumbnail = FileResource(type: .image, url: thumbnailUrl, name: "\(name)-thumbnail.jpg")
|
||||
thumbnail = FileOnDisk(type: .image, url: thumbnailUrl, name: "\(name)-thumbnail.jpg")
|
||||
add(resource: thumbnail!)
|
||||
}
|
||||
|
||||
@ -143,7 +88,7 @@ final class Importer {
|
||||
.filter { $0.hasDirectoryPath }
|
||||
}
|
||||
|
||||
private func findResources(in folder: URL, pageId: String) throws -> [FileResource] {
|
||||
private func findResources(in folder: URL, pageId: String) throws -> [FileOnDisk] {
|
||||
try FileManager.default
|
||||
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
||||
.filter { !$0.hasDirectoryPath }
|
||||
@ -165,7 +110,7 @@ final class Importer {
|
||||
|
||||
let name = pageId + "-" + fileName
|
||||
|
||||
return FileResource(type: type, url: url, name: name)
|
||||
return FileOnDisk(type: type, url: url, name: name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,7 +217,7 @@ final class Importer {
|
||||
posts[pageId] = post
|
||||
}
|
||||
|
||||
private func add(resource: FileResource) {
|
||||
private func add(resource: FileOnDisk) {
|
||||
guard let existingFile = files[resource.name] else {
|
||||
files[resource.name] = resource
|
||||
return
|
||||
@ -284,19 +229,19 @@ final class Importer {
|
||||
print("Conflicting name for file \(resource.name)")
|
||||
}
|
||||
|
||||
private func determineThumbnail(in resources: [FileResource], folder: URL, customPath: String?, pageId: String, language: String) throws -> FileResource? {
|
||||
private func determineThumbnail(in resources: [FileOnDisk], folder: URL, customPath: String?, pageId: String, language: String) throws -> FileOnDisk? {
|
||||
guard let thumbnailUrl = findThumbnailUrl(in: folder, customPath: customPath, language: language) else {
|
||||
return nil
|
||||
}
|
||||
return resources.first { $0.url == thumbnailUrl }
|
||||
}
|
||||
|
||||
private func determineThumbnail(in folder: URL, customPath: String?, pageId: String, language: String) throws -> FileResource? {
|
||||
private func determineThumbnail(in folder: URL, customPath: String?, pageId: String, language: String) throws -> FileOnDisk? {
|
||||
guard let thumbnailUrl = findThumbnailUrl(in: folder, customPath: customPath, language: language) else {
|
||||
return nil
|
||||
}
|
||||
let id = pageId + "-" + thumbnailUrl.lastPathComponent
|
||||
return FileResource(image: id, url: thumbnailUrl)
|
||||
return FileOnDisk(image: id, url: thumbnailUrl)
|
||||
}
|
||||
|
||||
private func findThumbnailUrl(in folder: URL, customPath: String?, language: String) -> URL? {
|
||||
|
@ -17,7 +17,7 @@ final class Content: ObservableObject {
|
||||
var images: [ImageResource] = []
|
||||
|
||||
@Published
|
||||
var files: [FileResources] = []
|
||||
var files: [FileResource] = []
|
||||
|
||||
@AppStorage("contentPath")
|
||||
private var storedContentPath: String = ""
|
||||
@ -37,7 +37,7 @@ final class Content: ObservableObject {
|
||||
pages: [Page] = [],
|
||||
tags: [Tag] = [],
|
||||
images: [ImageResource] = [],
|
||||
files: [FileResources] = [],
|
||||
files: [FileResource] = [],
|
||||
storedContentPath: String) {
|
||||
self.posts = posts
|
||||
self.pages = pages
|
||||
@ -209,13 +209,13 @@ final class Content: ObservableObject {
|
||||
dict[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url)
|
||||
}
|
||||
|
||||
let files: [FileResources] = filesData.compactMap { file, url in
|
||||
let files: [FileResource] = filesData.compactMap { file, url in
|
||||
let ext = file.components(separatedBy: ".").last!.lowercased()
|
||||
let type = FileType(fileExtension: ext)
|
||||
guard type == .file else {
|
||||
return nil
|
||||
}
|
||||
return FileResources(uniqueId: file, description: "")
|
||||
return FileResource(uniqueId: file, description: "")
|
||||
}
|
||||
|
||||
let posts = postsData.map { postId, post in
|
||||
@ -293,6 +293,27 @@ final class Content: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Saving
|
||||
|
||||
func saveToDisk() {
|
||||
print("Starting save")
|
||||
for page in pages {
|
||||
storage.save(pageMetadata: page.pageFile, for: page.id)
|
||||
}
|
||||
|
||||
for post in posts {
|
||||
storage.save(post: post.postFile, for: post.id)
|
||||
}
|
||||
|
||||
for tag in tags {
|
||||
storage.save(tagMetadata: tag.tagFile, for: tag.id)
|
||||
}
|
||||
// TODO: Remove all files that are no longer in use (they belong to deleted items)
|
||||
print("Finished save")
|
||||
}
|
||||
|
||||
// MARK: Folder access
|
||||
|
||||
static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) {
|
||||
guard let bookmarkData = UserDefaults.standard.data(forKey: key) else {
|
||||
print("No bookmark data to access folder")
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class FileResources: ObservableObject {
|
||||
final class FileResource: ObservableObject {
|
||||
|
||||
/// Globally unique id
|
||||
@Published
|
||||
|
@ -50,6 +50,13 @@ extension ImageResource: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageResource: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageResource {
|
||||
|
||||
var imageToDisplay: Image {
|
||||
|
@ -25,10 +25,6 @@ final class LocalizedPost: ObservableObject {
|
||||
self.images = images
|
||||
}
|
||||
|
||||
var displayImages: [Image] {
|
||||
images.map { $0.imageToDisplay }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func editableTitle() -> Binding<String> {
|
||||
Binding(
|
||||
|
26
CHDataManagement/Model/Post+Storage.swift
Normal file
26
CHDataManagement/Model/Post+Storage.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
extension Post {
|
||||
|
||||
var postFile: PostFile {
|
||||
.init(
|
||||
isDraft: isDraft,
|
||||
createdDate: createdDate,
|
||||
startDate: startDate,
|
||||
endDate: hasEndDate ? endDate : nil,
|
||||
tags: tags.map { $0.id },
|
||||
german: german.postFile,
|
||||
english: english.postFile,
|
||||
linkedPageId: linkedPage?.id)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedPost {
|
||||
|
||||
var postFile: LocalizedPostFile {
|
||||
.init(images: images.map { $0.id },
|
||||
title: title.nonEmpty,
|
||||
content: content,
|
||||
lastModifiedDate: lastModified)
|
||||
}
|
||||
}
|
22
CHDataManagement/Model/Tag+Storage.swift
Normal file
22
CHDataManagement/Model/Tag+Storage.swift
Normal file
@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
|
||||
extension Tag {
|
||||
|
||||
var tagFile: TagFile {
|
||||
.init(id: id,
|
||||
german: german.tagFile,
|
||||
english: english.tagFile)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedTag {
|
||||
|
||||
var tagFile: LocalizedTagFile {
|
||||
.init(urlComponent: urlComponent,
|
||||
name: name,
|
||||
subtitle: subtitle,
|
||||
description: description,
|
||||
thumbnail: thumbnail,
|
||||
originalURL: originalUrl)
|
||||
}
|
||||
}
|
@ -33,15 +33,20 @@ extension Post {
|
||||
createdDate: .now,
|
||||
startDate: .now.addingTimeInterval(-86400), endDate: .now,
|
||||
tags: [.nature, .sports, .hiking, .mountains],
|
||||
german: .init(
|
||||
title: "Ein langer Titel",
|
||||
content: "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: MockImage.images),
|
||||
english: .init(
|
||||
title: "A longer title",
|
||||
content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||
images: MockImage.images)
|
||||
|
||||
)
|
||||
german: .german,
|
||||
english: .english)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedPost {
|
||||
|
||||
static let german = LocalizedPost(
|
||||
title: "Ein langer Titel",
|
||||
content: "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: MockImage.images)
|
||||
|
||||
static let english = LocalizedPost(
|
||||
title: "A longer title",
|
||||
content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||
images: MockImage.images)
|
||||
}
|
||||
|
23
CHDataManagement/Storage/FileOnDisk.swift
Normal file
23
CHDataManagement/Storage/FileOnDisk.swift
Normal file
@ -0,0 +1,23 @@
|
||||
import Foundation
|
||||
|
||||
struct FileOnDisk {
|
||||
|
||||
let type: FileType
|
||||
|
||||
let url: URL
|
||||
|
||||
let name: String
|
||||
|
||||
init(image: String, url: URL) {
|
||||
self.type = .image
|
||||
self.url = url
|
||||
self.name = image
|
||||
}
|
||||
|
||||
init(type: FileType, url: URL, name: String) {
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
25
CHDataManagement/Storage/FileType.swift
Normal file
25
CHDataManagement/Storage/FileType.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
enum FileType {
|
||||
case image
|
||||
case file
|
||||
case video
|
||||
case resource
|
||||
|
||||
|
||||
init(fileExtension: String) {
|
||||
switch fileExtension.lowercased() {
|
||||
case "jpg", "jpeg", "png", "gif":
|
||||
self = .image
|
||||
case "html", "stl", "f3d", "step", "f3z", "zip", "json", "conf", "css", "js", "cpp", "cddx", "svg", "glb", "mp3", "pdf", "swift":
|
||||
self = .file
|
||||
case "mp4":
|
||||
self = .video
|
||||
case "key", "psd":
|
||||
self = .resource
|
||||
default:
|
||||
print("Unhandled file type: \(fileExtension)")
|
||||
self = .resource
|
||||
}
|
||||
}
|
||||
}
|
11
CHDataManagement/Storage/PageOnDisk.swift
Normal file
11
CHDataManagement/Storage/PageOnDisk.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
struct PageOnDisk {
|
||||
|
||||
let page: PageFile
|
||||
|
||||
let deContentUrl: URL
|
||||
|
||||
let enContentUrl: URL
|
||||
}
|
||||
|
@ -252,8 +252,28 @@ final class Storage {
|
||||
print("Failed to encode content of \(type) '\(id)': \(error)")
|
||||
return false
|
||||
}
|
||||
return write(data: content, type: type, id: id, to: file)
|
||||
}
|
||||
|
||||
private func write(data: Data, type: String, id: String, to file: URL) -> Bool {
|
||||
if fm.fileExists(atPath: file.path()) {
|
||||
// Check if content is the same, to prevent unnecessary writes
|
||||
do {
|
||||
let oldData = try Data(contentsOf: file)
|
||||
if data == oldData {
|
||||
// File is the same, don't write
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
print("Failed to read file \(file.path()) for equality check: \(error)")
|
||||
// No check possible, write file
|
||||
}
|
||||
} else {
|
||||
print("Writing new file \(file.path())")
|
||||
}
|
||||
do {
|
||||
try content.write(to: file, options: .atomic)
|
||||
try data.write(to: file, options: .atomic)
|
||||
print("Saved file \(file.path())")
|
||||
return true
|
||||
} catch {
|
||||
print("Failed to save content for \(type) '\(id)': \(error)")
|
||||
@ -272,13 +292,11 @@ final class Storage {
|
||||
}
|
||||
|
||||
private func write(content: String, to file: URL, type: String, id: String) -> Bool {
|
||||
do {
|
||||
try content.write(to: file, atomically: true, encoding: .utf8)
|
||||
return true
|
||||
} catch {
|
||||
print("Failed to save content for \(type) '\(id)': \(error)")
|
||||
guard let data = content.data(using: .utf8) else {
|
||||
print("Failed to convert string to data for \(type) '\(id)'")
|
||||
return false
|
||||
}
|
||||
return write(data: data, type: type, id: id, to: file)
|
||||
}
|
||||
|
||||
private func read<T>(at url: URL) throws -> T where T: Decodable {
|
||||
@ -293,5 +311,4 @@ final class Storage {
|
||||
items[id] = item
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -2,16 +2,19 @@ import SwiftUI
|
||||
|
||||
enum ColorPalette {
|
||||
|
||||
static let tagBackground = Color(r: 9, g: 62, b: 103)
|
||||
static let tagBackground = Color(r: 188, g: 188, b: 188) // Color(r: 9, g: 62, b: 103)
|
||||
|
||||
static let tagForeground = Color(r: 96, g: 186, b: 255)
|
||||
static let tagForeground = Color.primary // Color(r: 96, g: 186, b: 255)
|
||||
|
||||
static let listBackground = Color(r: 2, g: 15, b: 26)
|
||||
|
||||
static let postBackground = Color(r: 4, g: 31, b: 52)
|
||||
static let postBackground = Color(r: 222, g: 222, b: 222) // Color(r: 4, g: 31, b: 52)
|
||||
|
||||
static let postText = Color(r: 221, g: 221, b: 221)
|
||||
|
||||
static let postDate = tagForeground
|
||||
|
||||
static let link = Color.blue
|
||||
|
||||
}
|
||||
|
||||
|
33
CHDataManagement/Views/Generic/VerticalCenter.swift
Normal file
33
CHDataManagement/Views/Generic/VerticalCenter.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import SwiftUI
|
||||
|
||||
/**
|
||||
A view that centers the content vertically using a `VStack`
|
||||
*/
|
||||
struct VerticalCenter<Content> : View where Content : View {
|
||||
|
||||
let alignment: HorizontalAlignment
|
||||
|
||||
let spacing: CGFloat?
|
||||
|
||||
let content: Content
|
||||
|
||||
public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
|
||||
self.alignment = alignment
|
||||
self.spacing = spacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: alignment, spacing: spacing) {
|
||||
Spacer()
|
||||
content
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VerticalCenter {
|
||||
Text("Test")
|
||||
}
|
||||
}
|
60
CHDataManagement/Views/Posts/ImagePickerView.swift
Normal file
60
CHDataManagement/Views/Posts/ImagePickerView.swift
Normal file
@ -0,0 +1,60 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePickerView: View {
|
||||
|
||||
@Binding
|
||||
var showImagePicker: Bool
|
||||
|
||||
@ObservedObject
|
||||
var post: LocalizedPost
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
init(showImagePicker: Binding<Bool>, post: LocalizedPost) {
|
||||
self._showImagePicker = showImagePicker
|
||||
self.post = post
|
||||
}
|
||||
|
||||
@State
|
||||
private var selectedImage: ImageResource?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Select the image to add")
|
||||
List(content.images, selection: $selectedImage) { image in
|
||||
Text("\(image.id)")
|
||||
.tag(image)
|
||||
}
|
||||
.frame(minHeight: 300)
|
||||
HStack {
|
||||
Button("Add") {
|
||||
DispatchQueue.main.async {
|
||||
if let selectedImage {
|
||||
print("Added image")
|
||||
post.images.append(selectedImage)
|
||||
} else {
|
||||
print("No image to add")
|
||||
}
|
||||
}
|
||||
showImagePicker = false
|
||||
}
|
||||
.disabled(selectedImage == nil)
|
||||
Button("Cancel", role: .cancel) {
|
||||
showImagePicker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Pick a page")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ImagePickerView(showImagePicker: .constant(true),
|
||||
post: LocalizedPost.english)
|
||||
.environmentObject(Content.mock)
|
||||
}
|
@ -22,16 +22,45 @@ private struct NavigationIcon: View {
|
||||
|
||||
struct PostImageGalleryView: View {
|
||||
|
||||
let images: [Image]
|
||||
@ObservedObject
|
||||
var post: LocalizedPost
|
||||
|
||||
@State private var currentIndex = 0
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
images[currentIndex]
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
if images.count > 1 {
|
||||
ZStack(alignment: .center) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ZStack(alignment: .bottom) {
|
||||
post.images[currentIndex]
|
||||
.imageToDisplay
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
if post.images.count > 1 {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<post.images.count, id: \.self) { index in
|
||||
Circle()
|
||||
.fill(index == currentIndex ? Color.white : Color.gray)
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
Button(action: { showImagePicker = true }) {
|
||||
Image(systemSymbol: .plusCircleFill)
|
||||
.resizable()
|
||||
.renderingMode(.template)
|
||||
.foregroundStyle(.blue.opacity(0.7))
|
||||
.frame(width: 30, height: 30)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.blue)
|
||||
if post.images.count > 1 {
|
||||
HStack {
|
||||
Button(action: previous) {
|
||||
NavigationIcon(symbol: .chevronLeft, edge: .trailing)
|
||||
@ -46,38 +75,33 @@ struct PostImageGalleryView: View {
|
||||
.buttonStyle(.plain)
|
||||
.padding()
|
||||
}
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<images.count, id: \.self) { index in
|
||||
Circle()
|
||||
.fill(index == currentIndex ? Color.white : Color.gray) // Change color based on current index
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(
|
||||
showImagePicker: $showImagePicker,
|
||||
post: post
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func previous() {
|
||||
if currentIndex > 0 {
|
||||
currentIndex -= 1
|
||||
} else {
|
||||
currentIndex = images.count - 1
|
||||
currentIndex = post.images.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
private func next() {
|
||||
if currentIndex < images.count - 1 {
|
||||
if currentIndex < post.images.count - 1 {
|
||||
currentIndex += 1
|
||||
} else {
|
||||
currentIndex = 0 // Wrap to first image
|
||||
currentIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(traits: .fixedLayout(width: 300, height: 300)) {
|
||||
PostImageGalleryView(images: MockImage.images.map { $0.imageToDisplay })
|
||||
#Preview(traits: .fixedLayout(width: 300, height: 250)) {
|
||||
PostImageGalleryView(post: .german)
|
||||
}
|
||||
|
@ -14,6 +14,12 @@ struct PostView: View {
|
||||
@State
|
||||
private var showPagePicker = false
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
|
||||
@State
|
||||
private var showTagPicker = false
|
||||
|
||||
private var linkedPageText: String {
|
||||
if let page = post.linkedPage {
|
||||
return page.localized(in: language).title
|
||||
@ -23,8 +29,15 @@ struct PostView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
if !post.localized(in: language).images.isEmpty {
|
||||
PostImageGalleryView(images: post.localized(in: language).displayImages)
|
||||
if post.localized(in: language).images.isEmpty {
|
||||
Button(action: { showImagePicker = true }) {
|
||||
Text("Add image")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.blue)
|
||||
.padding(.top)
|
||||
} else {
|
||||
PostImageGalleryView(post: post.localized(in: language))
|
||||
.aspectRatio(1.33, contentMode: .fill)
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
@ -35,10 +48,10 @@ struct PostView: View {
|
||||
Spacer()
|
||||
Toggle("Draft", isOn: $post.isDraft)
|
||||
}
|
||||
.foregroundStyle(ColorPalette.postDate)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("", text: post.localized(in: language).editableTitle())
|
||||
.font(.system(size: 24, weight: .bold))
|
||||
.foregroundStyle(Color.white)
|
||||
.foregroundStyle(Color.primary)
|
||||
.textFieldStyle(.plain)
|
||||
.lineLimit(2)
|
||||
FlowHStack {
|
||||
@ -51,20 +64,20 @@ struct PostView: View {
|
||||
remove(tag: tag)
|
||||
}
|
||||
}
|
||||
Button(action: showTagList) {
|
||||
Button(action: { showTagPicker = true }) {
|
||||
SwiftUI.Image(systemSymbol: .plusCircleFill)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(height: 18)
|
||||
.foregroundColor(ColorPalette.tagForeground)
|
||||
.opacity(0.7)
|
||||
.foregroundColor(Color.blue)
|
||||
//.opacity(0.7)
|
||||
.padding(.top, 3)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
TextEditor(text: post.localized(in: language).editableContent())
|
||||
.font(.body)
|
||||
.foregroundStyle(ColorPalette.postText)
|
||||
.foregroundStyle(.secondary)
|
||||
.textEditorStyle(.plain)
|
||||
.padding(.leading, -5)
|
||||
.scrollDisabled(true)
|
||||
@ -73,12 +86,12 @@ struct PostView: View {
|
||||
Text(linkedPageText)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(ColorPalette.postDate)
|
||||
.foregroundStyle(ColorPalette.link)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(ColorPalette.postBackground)
|
||||
.background(Color.secondary.colorInvert())
|
||||
.cornerRadius(8)
|
||||
.sheet(isPresented: $showDatePicker) {
|
||||
DatePickerView(
|
||||
@ -90,26 +103,28 @@ struct PostView: View {
|
||||
showPagePicker: $showPagePicker,
|
||||
selectedPage: $post.linkedPage)
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(
|
||||
showImagePicker: $showImagePicker,
|
||||
post: post.localized(in: language)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func remove(tag: Tag) {
|
||||
post.tags = post.tags.filter {$0.id != tag.id }
|
||||
}
|
||||
|
||||
private func showTagList() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(traits: .fixedLayout(width: 450, height: 600)) {
|
||||
List {
|
||||
PostView(post: .fullMock)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(ColorPalette.listBackground)
|
||||
//.listRowBackground(ColorPalette.listBackground)
|
||||
.environment(\.language, ContentLanguage.german)
|
||||
PostView(post: .mock)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(ColorPalette.listBackground)
|
||||
//.listRowBackground(ColorPalette.listBackground)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
//.listStyle(.plain)
|
||||
}
|
||||
|
@ -9,18 +9,12 @@ struct TagView: View {
|
||||
|
||||
let tag: LocalizedText
|
||||
|
||||
let icon: SFSymbol
|
||||
|
||||
let iconSize: CGFloat
|
||||
|
||||
init(tag: LocalizedText, icon: SFSymbol = .xCircleFill, iconSize: CGFloat = 12.0) {
|
||||
init(tag: LocalizedText) {
|
||||
self.tag = tag
|
||||
self.icon = icon
|
||||
self.iconSize = iconSize
|
||||
}
|
||||
|
||||
static var add: TagView {
|
||||
.init(tag: LocalizedText(en: "Add", de: "Mehr"), icon: .plusCircleFill)
|
||||
.init(tag: LocalizedText(en: "Add", de: "Mehr"))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -28,16 +22,11 @@ struct TagView: View {
|
||||
Text(tag.getText(for: language))
|
||||
.font(.subheadline)
|
||||
.padding(.leading, 2)
|
||||
SwiftUI.Image(systemSymbol: icon)
|
||||
.font(.system(size: iconSize, weight: .black, design: .rounded))
|
||||
.opacity(0.7)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
.foregroundColor(ColorPalette.tagForeground)
|
||||
.font(.caption2)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(ColorPalette.tagBackground)
|
||||
.background(Color.accentColor)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
@ -49,5 +38,5 @@ struct TagView: View {
|
||||
TagView(tag: LocalizedText(en: "Some", de: "Etwas"))
|
||||
.environment(\.language, ContentLanguage.english)
|
||||
TagView.add
|
||||
}
|
||||
}.background(Color.secondary)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user