diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 8b6b021..5420807 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -203,6 +203,7 @@ E2FD1D5E2D47EED200B48627 /* PostImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D5D2D47EED200B48627 /* PostImageView.swift */; }; E2FD1D602D47EEEF00B48627 /* LocalizedPostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D5F2D47EEEF00B48627 /* LocalizedPostContentView.swift */; }; E2FD1D642D47EF4200B48627 /* DetailListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D632D47EF4200B48627 /* DetailListItem.swift */; }; + E2FD1D682D483CCF00B48627 /* Insert+Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D672D483CCA00B48627 /* Insert+Buttons.swift */; }; E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; }; E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; }; E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; }; @@ -451,6 +452,7 @@ E2FD1D5D2D47EED200B48627 /* PostImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImageView.swift; sourceTree = ""; }; E2FD1D5F2D47EEEF00B48627 /* LocalizedPostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostContentView.swift; sourceTree = ""; }; E2FD1D632D47EF4200B48627 /* DetailListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailListItem.swift; sourceTree = ""; }; + E2FD1D672D483CCA00B48627 /* Insert+Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Buttons.swift"; sourceTree = ""; }; E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = ""; }; E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = ""; }; E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = ""; }; @@ -933,6 +935,7 @@ E2FD1D572D477A9400B48627 /* InsertableCommand.swift */, E2FD1D3A2D3BC40500B48627 /* InsertableCommandSheet.swift */, E2FD1D382D3BBECA00B48627 /* InsertableView.swift */, + E2FD1D672D483CCA00B48627 /* Insert+Buttons.swift */, E2FD1D362D3BBCB500B48627 /* Insert+Image.swift */, E2FD1D552D46CED500B48627 /* Insert+Labels.swift */, ); @@ -1254,6 +1257,7 @@ E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, + E2FD1D682D483CCF00B48627 /* Insert+Buttons.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, diff --git a/CHDataManagement/Generator/ImageGenerator.swift b/CHDataManagement/Generator/ImageGenerator.swift index e5dd4f0..2eb0a6f 100644 --- a/CHDataManagement/Generator/ImageGenerator.swift +++ b/CHDataManagement/Generator/ImageGenerator.swift @@ -66,23 +66,6 @@ final class ImageGenerator { return true } - guard let data = version.image.dataContent() else { - print("ImageGenerator: Failed to load data for image \(version.image.id)") - return false - } - - guard let originalImage = NSImage(data: data) else { - print("ImageGenerator: Failed to load image \(version.image.id)") - return false - } - - let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight)) - - guard let data = create(image: representation, type: version.type, quality: version.quality) else { - print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.id)") - return false - } - if version.type == .avif { if version.image.type == .gif { // Skip GIFs, since they can't be converted by avifenc @@ -100,10 +83,27 @@ final class ImageGenerator { let quality = Int(version.quality * 100) avifCommands.insert("avifenc -q \(quality) '\(originalImagePath)' '\(generatedImagePath)'") - // hasNowGenerated(version) + version.wasNowGenerated() return true } + guard let data = version.image.dataContent() else { + print("ImageGenerator: Failed to load data for image \(version.image.id)") + return false + } + + guard let originalImage = NSImage(data: data) else { + print("ImageGenerator: Failed to load image \(version.image.id)") + return false + } + + let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight)) + + guard let data = create(image: representation, type: version.type, quality: version.quality) else { + print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.id)") + return false + } + guard write(imageData: data, of: version) else { return false } diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index d1730bc..189f044 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -62,16 +62,11 @@ struct MainView: App { @ViewBuilder var sidebar: some View { switch selection.tab { - case .posts: - PostListView(selectedPost: $selection.post) - case .pages: - PageListView(selectedPage: $selection.page) - case .tags: - TagListView(selectedTag: $selection.tag) - case .files: - FileListView(selectedFile: $selection.file) - case .generation: - SettingsListView(selectedSection: $selection.section) + case .posts: PostListView() + case .pages: PageListView() + case .tags: TagListView() + case .files: FileListView(selectedFile: $selection.file) + case .generation: SettingsListView() } } diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index aaf4497..3264743 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -66,7 +66,6 @@ extension Content { } private func generateRequiredImages() { - let images = results.imagesToGenerate.sorted() let count = images.count var completed = 0 @@ -83,8 +82,6 @@ extension Content { } imageGenerator.writeAvifCommandScript() - //let images = Set(self.images.map { $0.id }) - //imageGenerator.recalculateGeneratedImages(by: images) } func generateAllPages() { diff --git a/CHDataManagement/Views/Files/FileListView.swift b/CHDataManagement/Views/Files/FileListView.swift index 114dbda..e5ea28a 100644 --- a/CHDataManagement/Views/Files/FileListView.swift +++ b/CHDataManagement/Views/Files/FileListView.swift @@ -51,8 +51,19 @@ struct FileListView: View { TextField("", text: $searchString, prompt: Text("Search")) .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) - List(filteredFiles, selection: $selectedFile) { file in - Text(file.id).tag(file) + List(filteredFiles) { file in + HStack { + Text(file.id) + Spacer() + } + .listRowBackground(RoundedRectangle(cornerRadius: 5) + .fill(selectedFile == file ? Color.blue : Color.clear) + .padding(.horizontal, 10) + ) + .contentShape(Rectangle()) + .onTapGesture { + selectedFile = file + } } .onChange(of: selectedFileType) { oldValue, newValue in guard oldValue != newValue else { diff --git a/CHDataManagement/Views/Pages/Commands/Insert+Buttons.swift b/CHDataManagement/Views/Pages/Commands/Insert+Buttons.swift new file mode 100644 index 0000000..005428a --- /dev/null +++ b/CHDataManagement/Views/Pages/Commands/Insert+Buttons.swift @@ -0,0 +1,200 @@ +import SwiftUI +import SFSafeSymbols + +final class InsertableFileButton: ObservableObject { + + @Published + var label: ContentLabel = .init(icon: .buttonDownload, value: "Text") + + @Published + var file: FileResource? + + @Published + var downloadedFileName: String? = nil + + var command: String? { + guard let file else { + return nil + } + let result = + """ + icon: \(label.icon.rawValue) + text: \(label.value) + file: \(file.id) + """ + guard let downloadedFileName else { + return result + } + return result + "\n" + "name: \(downloadedFileName)" + } +} + +final class InsertableUrlButton: ObservableObject { + + @Published + var label: ContentLabel = .init(icon: .buttonDownload, value: "Text") + + @Published + var url: String = "Url" + + var command: String? { + """ + icon: \(label.icon.rawValue) + text: \(label.value) + url: \(url) + """ + } +} + +final class InsertableEventButton: ObservableObject { + + @Published + var label: ContentLabel = .init(icon: .buttonDownload, value: "Text") + + @Published + var event: String = "Javascript" + + var command: String? { + """ + icon: \(label.icon.rawValue) + text: \(label.value) + event: \(event) + """ + } +} + +struct InsertableButtons: View, InsertableCommandView { + + final class Model: ObservableObject, InsertableCommandModel { + + enum AnyButton: Identifiable { + case file(InsertableFileButton) + case url(InsertableUrlButton) + case event(InsertableEventButton) + + var command: String? { + switch self { + case .file(let file): + return file.command + case .url(let url): + return url.command + case .event(let event): + return event.command + } + } + + var id: String { + switch self { + case .file(let file): + return "file-\(file.file?.id ?? "none")" + case .url(let url): + return "url-\(url.url)" + case .event(let event): + return "event-\(event.event)" + } + } + } + + @Published + var buttons: [AnyButton] = [] + + var isReady: Bool { + !buttons.isEmpty + } + + var command: String? { + let result = buttons.compactMap { $0.command } + guard result.count == buttons.count else { + return nil + } + return result.map { + "```buttons\n\($0)\n```" + }.joined(separator: "\n\n") + } + } + + static let title = "Buttons" + + static let sheetTitle = "Insert buttons" + + static let icon: SFSymbol = .rectangleAndHandPointUpLeft + + @ObservedObject + private var model: Model + + init(model: Model) { + self.model = model + } + + var body: some View { + VStack(alignment: .leading) { + List(model.buttons) { button in + switch button { + case .file(let file): + FileButtonView(content: file) + case .url(let url): + UrlButtonView(content: url) + case .event(let event): + EventButtonView(content: event) + } + } + .frame(minHeight: 300) + HStack { + Spacer() + Button("File", action: { model.buttons.append(.file(.init())) }) + Button("Url", action: { model.buttons.append(.url(.init())) }) + Button("Event", action: { model.buttons.append(.event(.init())) }) + Spacer() + } + } + } +} + +private struct FileButtonView: View { + + @ObservedObject + var content: InsertableFileButton + + @State + private var showFileSelectionSheet = false + + var body: some View { + HStack { + LabelEditingView(label: content.label) + Button("\(content.file?.id ?? "Select file")", action: { showFileSelectionSheet = true }) + OptionalTextField("", text: $content.downloadedFileName, prompt: "Downloaded file name") + .textFieldStyle(.roundedBorder) + } + .sheet(isPresented: $showFileSelectionSheet) { + FileSelectionView(selectedFile: $content.file) + } + } +} + +private struct UrlButtonView: View { + + @ObservedObject + var content: InsertableUrlButton + + var body: some View { + HStack { + LabelEditingView(label: content.label) + TextField("", text: $content.url, prompt: Text("URL")) + .textFieldStyle(.roundedBorder) + } + } +} + +private struct EventButtonView: View { + + @ObservedObject + var content: InsertableEventButton + + var body: some View { + HStack { + LabelEditingView(label: content.label) + TextField("", text: $content.event, prompt: Text("Javascript")) + .textFieldStyle(.roundedBorder) + } + } +} diff --git a/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift b/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift index a603d82..441bf3a 100644 --- a/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift +++ b/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift @@ -8,6 +8,7 @@ struct InsertableItemsView: View { .font(.headline) InsertableView() InsertableView() + InsertableView() } } } diff --git a/CHDataManagement/Views/Pages/PageListView.swift b/CHDataManagement/Views/Pages/PageListView.swift index 8b2602f..0014604 100644 --- a/CHDataManagement/Views/Pages/PageListView.swift +++ b/CHDataManagement/Views/Pages/PageListView.swift @@ -41,17 +41,15 @@ struct PageListView: View { private var language @EnvironmentObject - private var content: Content + private var selection: SelectedContent - @Binding - private var selectedPage: Page? + @EnvironmentObject + private var content: Content @State private var searchString = "" - init(selectedPage: Binding) { - self._selectedPage = selectedPage - } + init() { } private var filteredPages: [Page] { guard !searchString.isEmpty else { @@ -65,20 +63,30 @@ struct PageListView: View { TextField("", text: $searchString, prompt: Text("Search")) .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) - List(filteredPages, selection: $selectedPage) { page in + List(filteredPages) { page in PageListItem(page: page) - .tag(page) + .listRowBackground(RoundedRectangle(cornerRadius: 5) + .fill(selection.page == page ? Color.blue : Color.clear) + .padding(.horizontal, 10) + ) + .contentShape(Rectangle()) + .onTapGesture { + selection.page = page + } } } .onAppear { - if selectedPage == nil { - selectedPage = content.pages.first + if selection.page == nil, let first = content.pages.first { + selection.page = first } } } } #Preview { - PageListView(selectedPage: .constant(nil)) - .environmentObject(Content.mock) + let content = Content.mock + let selection = SelectedContent() + PageListView() + .environmentObject(content) + .environmentObject(selection) } diff --git a/CHDataManagement/Views/Posts/PostListView.swift b/CHDataManagement/Views/Posts/PostListView.swift index 2116e64..dc2bc90 100644 --- a/CHDataManagement/Views/Posts/PostListView.swift +++ b/CHDataManagement/Views/Posts/PostListView.swift @@ -48,15 +48,13 @@ struct PostListView: View { @EnvironmentObject private var content: Content - @Binding - private var selectedPost: Post? + @EnvironmentObject + private var selection: SelectedContent @State private var searchString = "" - init(selectedPost: Binding) { - self._selectedPost = selectedPost - } + init() { } private var filteredPosts: [Post] { guard !searchString.isEmpty else { @@ -74,13 +72,20 @@ struct PostListView: View { TextField("", text: $searchString, prompt: Text("Search")) .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) - List(filteredAndSortedPosts, selection: $selectedPost) { post in + List(filteredAndSortedPosts) { post in PostListItem(post: post) - .tag(post) + .listRowBackground(RoundedRectangle(cornerRadius: 5) + .fill(selection.post == post ? Color.blue : Color.clear) + .padding(.horizontal, 10) + ) + .contentShape(Rectangle()) + .onTapGesture { + selection.post = post + } } }.onAppear { - if selectedPost == nil { - selectedPost = content.posts.first + if selection.post == nil, let first = content.posts.first { + selection.post = first } } } diff --git a/CHDataManagement/Views/Settings/SettingsListView.swift b/CHDataManagement/Views/Settings/SettingsListView.swift index 416a123..8956b31 100644 --- a/CHDataManagement/Views/Settings/SettingsListView.swift +++ b/CHDataManagement/Views/Settings/SettingsListView.swift @@ -2,12 +2,20 @@ import SwiftUI struct SettingsListView: View { - @Binding - var selectedSection: SettingsSection + @EnvironmentObject + private var selection: SelectedContent var body: some View { - List(SettingsSection.allCases, selection: $selectedSection) { item in - Label(item.rawValue, systemSymbol: item.icon).tag(item) + List(SettingsSection.allCases) { section in + Label(section.rawValue, systemSymbol: section.icon) + .listRowBackground(RoundedRectangle(cornerRadius: 5) + .fill(selection.section == section ? Color.blue : Color.clear) + .padding(.horizontal, 10) + ) + .contentShape(Rectangle()) + .onTapGesture { + selection.section = section + } } } } diff --git a/CHDataManagement/Views/Tags/TagListView.swift b/CHDataManagement/Views/Tags/TagListView.swift index 032e23e..bc9209f 100644 --- a/CHDataManagement/Views/Tags/TagListView.swift +++ b/CHDataManagement/Views/Tags/TagListView.swift @@ -8,15 +8,13 @@ struct TagListView: View { @EnvironmentObject private var content: Content - @Binding - var selectedTag: Tag? + @EnvironmentObject + private var selection: SelectedContent @State private var searchString = "" - init(selectedTag: Binding) { - _selectedTag = selectedTag - } + init() { } private var filteredTags: [Tag] { guard !searchString.isEmpty else { @@ -34,13 +32,20 @@ struct TagListView: View { TextField("", text: $searchString, prompt: Text("Search")) .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) - List(filteredAndSortedTags, selection: $selectedTag) { tag in + List(filteredAndSortedTags) { tag in TagListItem(tag: tag.localized(in: language)) - .tag(tag) + .listRowBackground(RoundedRectangle(cornerRadius: 5) + .fill(selection.tag == tag ? Color.blue : Color.clear) + .padding(.horizontal, 10) + ) + .contentShape(Rectangle()) + .onTapGesture { + selection.tag = tag + } } }.onAppear { - if selectedTag == nil { - selectedTag = content.tags.first + if selection.tag == nil, let first = content.tags.first { + selection.tag = first } } } @@ -52,6 +57,9 @@ private struct TagListItem: View { var tag: LocalizedTag var body: some View { - Text(tag.title) + HStack { + Text(tag.title) + Spacer() + } } }