Add images to posts, saving

This commit is contained in:
Christoph Hagen 2024-11-20 23:46:54 +01:00
parent cb22ae34f2
commit a35c2d669e
21 changed files with 415 additions and 149 deletions

View File

@ -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 */,

View File

@ -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()

View File

@ -8,4 +8,8 @@ extension String {
.replacingOccurrences(of: "<", with: "&lt;")
.replacingOccurrences(of: ">", with: "&gt;")
}
var nonEmpty: String? {
isEmpty ? nil : self
}
}

View File

@ -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? {

View File

@ -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")

View File

@ -1,6 +1,6 @@
import Foundation
final class FileResources: ObservableObject {
final class FileResource: ObservableObject {
/// Globally unique id
@Published

View File

@ -50,6 +50,13 @@ extension ImageResource: Equatable {
}
}
extension ImageResource: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension ImageResource {
var imageToDisplay: Image {

View File

@ -25,10 +25,6 @@ final class LocalizedPost: ObservableObject {
self.images = images
}
var displayImages: [Image] {
images.map { $0.imageToDisplay }
}
@MainActor
func editableTitle() -> Binding<String> {
Binding(

View 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)
}
}

View 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)
}
}

View File

@ -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)
}

View 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
}
}

View 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
}
}
}

View File

@ -0,0 +1,11 @@
import Foundation
struct PageOnDisk {
let page: PageFile
let deContentUrl: URL
let enContentUrl: URL
}

View File

@ -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
}
}
}

View File

@ -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
}

View 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")
}
}

View 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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}