diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index b59a0fb..07998c0 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 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 */; }; + E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.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 */; }; @@ -89,6 +90,7 @@ E218501A2CEE59E80090B18B /* Tag+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Storage.swift"; sourceTree = ""; }; E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = ""; }; E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = ""; }; + E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = ""; }; E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = ""; }; E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = ""; }; E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = ""; }; @@ -300,6 +302,7 @@ E2B85F4B2C4B8B7F0047CD0C /* Posts */ = { isa = PBXGroup; children = ( + E21850222CF10C840090B18B /* TagSelectionView.swift */, E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */, E21850082CEE01BF0090B18B /* PagePickerView.swift */, E2A21C112CB18D520060935B /* DatePickerView.swift */, @@ -481,6 +484,7 @@ E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */, E24252032C5163CF0029FF16 /* Importer.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, + E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */, E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, diff --git a/CHDataManagement/Views/Posts/PostImageGalleryView.swift b/CHDataManagement/Views/Posts/PostImageGalleryView.swift index 3f20a2e..e9359f0 100644 --- a/CHDataManagement/Views/Posts/PostImageGalleryView.swift +++ b/CHDataManagement/Views/Posts/PostImageGalleryView.swift @@ -30,14 +30,25 @@ struct PostImageGalleryView: View { @State private var showImagePicker = false + private var imageAtCurrentIndex: Image? { + guard !post.images.isEmpty else { + return nil + } + guard currentIndex < post.images.count else { + return post.images.last?.imageToDisplay + } + return post.images[currentIndex].imageToDisplay + } + var body: some View { ZStack(alignment: .center) { ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottom) { - post.images[currentIndex] - .imageToDisplay - .resizable() - .scaledToFit() + if let imageAtCurrentIndex { + imageAtCurrentIndex + .resizable() + .scaledToFit() + } if post.images.count > 1 { HStack(spacing: 8) { ForEach(0..= post.images.count { + currentIndex = post.images.count - 1 + } + } } #Preview(traits: .fixedLayout(width: 300, height: 250)) { diff --git a/CHDataManagement/Views/Posts/PostList.swift b/CHDataManagement/Views/Posts/PostList.swift index edb672c..c6762f0 100644 --- a/CHDataManagement/Views/Posts/PostList.swift +++ b/CHDataManagement/Views/Posts/PostList.swift @@ -1,22 +1,5 @@ import SwiftUI - -private struct CenteredPost: View where Content: View { - - let content: Content - - init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - var body: some View { - HorizontalCenter { - content - } - .listRowBackground(ColorPalette.listBackground) - } -} - struct PostList: View { @EnvironmentObject @@ -25,13 +8,13 @@ struct PostList: View { var body: some View { List { if content.posts.isEmpty { - CenteredPost { + HorizontalCenter { Text("No posts yet.") .padding() } .listRowSeparator(.hidden) } - CenteredPost { + HorizontalCenter { Button(action: addNewPost) { Text("Add post") } @@ -39,7 +22,7 @@ struct PostList: View { .listRowSeparator(.hidden) } ForEach(content.posts) { post in - CenteredPost { + HorizontalCenter { PostView(post: post) .frame(maxWidth: 600) } @@ -48,8 +31,7 @@ struct PostList: View { } } .listStyle(.plain) - .background(ColorPalette.listBackground) - .scrollContentBackground(.hidden) + //.scrollContentBackground(.hidden) } private func addNewPost() { diff --git a/CHDataManagement/Views/Posts/PostView.swift b/CHDataManagement/Views/Posts/PostView.swift index f0df198..3976688 100644 --- a/CHDataManagement/Views/Posts/PostView.swift +++ b/CHDataManagement/Views/Posts/PostView.swift @@ -1,13 +1,28 @@ import SwiftUI +import SFSafeSymbols struct PostView: View { + @ObservedObject + var post: Post + @Environment(\.language) - var language + private var language + + var body: some View { + LocalizedPostView(post: post, localized: post.localized(in: language)) + } +} + + +struct LocalizedPostView: View { @ObservedObject var post: Post + @ObservedObject + var localized: LocalizedPost + @State private var showDatePicker = false @@ -20,6 +35,12 @@ struct PostView: View { @State private var showTagPicker = false + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + private var linkedPageText: String { if let page = post.linkedPage { return page.localized(in: language).title @@ -29,7 +50,7 @@ struct PostView: View { var body: some View { VStack(alignment: .center) { - if post.localized(in: language).images.isEmpty { + if localized.images.isEmpty { Button(action: { showImagePicker = true }) { Text("Add image") } @@ -37,7 +58,7 @@ struct PostView: View { .foregroundStyle(.blue) .padding(.top) } else { - PostImageGalleryView(post: post.localized(in: language)) + PostImageGalleryView(post: localized) .aspectRatio(1.33, contentMode: .fill) } VStack(alignment: .leading) { @@ -49,7 +70,7 @@ struct PostView: View { Toggle("Draft", isOn: $post.isDraft) } .foregroundStyle(.secondary) - TextField("", text: post.localized(in: language).editableTitle()) + TextField("", text: $localized.title) .font(.system(size: 24, weight: .bold)) .foregroundStyle(Color.primary) .textFieldStyle(.plain) @@ -60,22 +81,21 @@ struct PostView: View { en: tag.english.name, de: tag.german.name) ) - .onTapGesture { - remove(tag: tag) - } + .foregroundStyle(.white) } Button(action: { showTagPicker = true }) { - SwiftUI.Image(systemSymbol: .plusCircleFill) + Image(systemSymbol: .squareAndPencilCircleFill) .resizable() .aspectRatio(1, contentMode: .fit) - .frame(height: 18) + .frame(height: 22) .foregroundColor(Color.blue) - //.opacity(0.7) - .padding(.top, 3) + .background(Circle() + .fill(Color.white) + .padding(1)) } .buttonStyle(.plain) } - TextEditor(text: post.localized(in: language).editableContent()) + TextEditor(text: $localized.content) .font(.body) .foregroundStyle(.secondary) .textEditorStyle(.plain) @@ -91,7 +111,7 @@ struct PostView: View { } .padding() } - .background(Color.secondary.colorInvert()) + .background(Color(NSColor.windowBackgroundColor)) .cornerRadius(8) .sheet(isPresented: $showDatePicker) { DatePickerView( @@ -106,8 +126,13 @@ struct PostView: View { .sheet(isPresented: $showImagePicker) { ImagePickerView( showImagePicker: $showImagePicker, - post: post.localized(in: language) - ) + post: localized) + } + .sheet(isPresented: $showTagPicker) { + TagSelectionView( + presented: $showTagPicker, + selected: $post.tags, + tags: $content.tags) } } @@ -120,11 +145,10 @@ struct PostView: View { List { PostView(post: .fullMock) .listRowSeparator(.hidden) - //.listRowBackground(ColorPalette.listBackground) .environment(\.language, ContentLanguage.german) PostView(post: .mock) .listRowSeparator(.hidden) - //.listRowBackground(ColorPalette.listBackground) } + .environmentObject(Content.mock) //.listStyle(.plain) } diff --git a/CHDataManagement/Views/Posts/TagSelectionView.swift b/CHDataManagement/Views/Posts/TagSelectionView.swift new file mode 100644 index 0000000..cf5228f --- /dev/null +++ b/CHDataManagement/Views/Posts/TagSelectionView.swift @@ -0,0 +1,95 @@ +import SwiftUI +import SFSafeSymbols + +struct TagSelectionView: View { + + @Binding + var presented: Bool + + @Binding + private var selected: [Tag] + + @Binding + private var tags: [Tag] + + @Environment(\.language) + private var language + + init(presented: Binding, selected: Binding<[Tag]>, tags: Binding<[Tag]>) { + self._presented = presented + self._selected = selected + self._tags = tags + } + + var body: some View { + VStack(spacing: 0) { + List { + Section("Selected tags") { + ForEach(selected) { tag in + HStack { + Image(systemSymbol: .minusCircleFill) + .foregroundStyle(.red) + .onTapGesture { + deselect(tag: tag) + } + Text(tag.localized(in: language).name) + } + } + .onMove(perform: moveTag) + } + Section("Available tags") { + ForEach(tags.filter({ !selected.contains($0) })) { tag in + HStack { + Image(systemSymbol: .plusCircleFill) + .foregroundStyle(.green) + Text(tag.localized(in: language).name) + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + select(tag: tag) + } + } + } + } + .frame(minHeight: 300) + .padding() + Button(action: dismiss) { + Text("Save") + }.padding() + } + } + + private func moveTag(from source: IndexSet, to destination: Int) { + selected.move(fromOffsets: source, toOffset: destination) + } + + private func deselect(tag: Tag) { + guard let index = selected.firstIndex(of: tag) else { + return + } + selected.remove(at: index) + + let insertIndex = tags.firstIndex(where: { $0 > tag }) ?? tags.endIndex + tags.insert(tag, at: insertIndex) + } + + private func select(tag: Tag) { + guard let index = tags.firstIndex(of: tag) else { + return + } + tags.remove(at: index) + selected.append(tag) + } + + private func dismiss() { + presented = false + } +} + +#Preview { + TagSelectionView( + presented: .constant(true), + selected: .constant([.hiking, .nature]), + tags: .constant([.sports, .mock])) +}