From 5f8ec17995db8abe8ab35f0051868d0d000ebf2f Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Sun, 27 Nov 2022 08:52:43 +0000 Subject: [PATCH] feat: Show a document preview in the rule wizard on iOS (#484) --- .../FileawayCore/Views/Wizard/RuleForm.swift | 21 +----- .../Views/Wizard/RuleFormSection.swift | 64 +++++++++++++++++++ interact | 2 +- ios/Fileaway-iOS.xcodeproj/project.pbxproj | 26 ++++++-- ios/Fileaway/Extensions/CGSize.swift | 27 ++++++++ ...ader.swift => DocumentPreviewButton.swift} | 21 +++--- .../Views/Wizard/DocumentPreviewHeader.swift | 48 ++++++++++++++ ios/Fileaway/Views/Wizard/RuleFormView.swift | 38 ++++++----- ios/Fileaway/Views/Wizard/RulePicker.swift | 12 +++- 9 files changed, 205 insertions(+), 54 deletions(-) create mode 100644 core/Sources/FileawayCore/Views/Wizard/RuleFormSection.swift create mode 100644 ios/Fileaway/Extensions/CGSize.swift rename ios/Fileaway/Views/Wizard/{FilePreviewHeader.swift => DocumentPreviewButton.swift} (79%) create mode 100644 ios/Fileaway/Views/Wizard/DocumentPreviewHeader.swift diff --git a/core/Sources/FileawayCore/Views/Wizard/RuleForm.swift b/core/Sources/FileawayCore/Views/Wizard/RuleForm.swift index c712e6a1..f82fad4b 100644 --- a/core/Sources/FileawayCore/Views/Wizard/RuleForm.swift +++ b/core/Sources/FileawayCore/Views/Wizard/RuleForm.swift @@ -24,36 +24,17 @@ public struct RuleForm: View { @ObservedObject private var ruleFormModel: RuleFormModel private let url: URL - @StateObject private var dateExtractor: DateExtractor public init(_ ruleFormModel: RuleFormModel, url: URL) { self.ruleFormModel = ruleFormModel self.url = url - _dateExtractor = StateObject(wrappedValue: DateExtractor(url: url)) } public var body: some View { Form { - Section { - ForEach(ruleFormModel.variableFieldModels) { variable in - if let variable = variable as? DateFieldModel { - DateField(variable: variable, - creationDate: FileInfo.creationDate(url: url)?.date, - options: dateExtractor.dates) - } else if let variable = variable as? StringFieldModel { - StringField(variable: variable) - } else { - Text("Unknown Variable Type") - } - } - } footer: { - Text(ruleFormModel.attributedRelativeDestinationPath(font: .standardizedFooter)) - .horizontalSpace(.trailing) - } + RuleFormSection(ruleFormModel, url: url) } .formStyle(.grouped) - .runs(ruleFormModel) - .runs(dateExtractor) } } diff --git a/core/Sources/FileawayCore/Views/Wizard/RuleFormSection.swift b/core/Sources/FileawayCore/Views/Wizard/RuleFormSection.swift new file mode 100644 index 00000000..e73bafef --- /dev/null +++ b/core/Sources/FileawayCore/Views/Wizard/RuleFormSection.swift @@ -0,0 +1,64 @@ +// Copyright (c) 2018-2022 InSeven Limited +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +import Interact + +extension String: Identifiable { + + public var id: Self { self } + +} + +public struct RuleFormSection: View { + + @ObservedObject private var ruleFormModel: RuleFormModel + private let url: URL + @StateObject private var dateExtractor: DateExtractor + + public init(_ ruleFormModel: RuleFormModel, url: URL) { + self.ruleFormModel = ruleFormModel + self.url = url + _dateExtractor = StateObject(wrappedValue: DateExtractor(url: url)) + } + + public var body: some View { + Section { + ForEach(ruleFormModel.variableFieldModels) { variable in + if let variable = variable as? DateFieldModel { + DateField(variable: variable, + creationDate: FileInfo.creationDate(url: url)?.date, + options: dateExtractor.dates) + } else if let variable = variable as? StringFieldModel { + StringField(variable: variable) + } else { + Text("Unknown Variable Type") + } + } + } footer: { + Text(ruleFormModel.attributedRelativeDestinationPath(font: .standardizedFooter)) + .horizontalSpace(.trailing) + } + .runs(ruleFormModel) + .runs(dateExtractor) + } + +} diff --git a/interact b/interact index 4d19f4f5..671bb25f 160000 --- a/interact +++ b/interact @@ -1 +1 @@ -Subproject commit 4d19f4f5e8a4a842e17685f45ce5829a134a2360 +Subproject commit 671bb25f2b4e8a9c4606b400264908cd7e8fe2fc diff --git a/ios/Fileaway-iOS.xcodeproj/project.pbxproj b/ios/Fileaway-iOS.xcodeproj/project.pbxproj index 1c426616..f448a490 100644 --- a/ios/Fileaway-iOS.xcodeproj/project.pbxproj +++ b/ios/Fileaway-iOS.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ D81C5D93249BE3710083BD6F /* RulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81C5D92249BE3710083BD6F /* RulesView.swift */; }; D81C5D95249BE6F40083BD6F /* RuleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81C5D94249BE6F40083BD6F /* RuleView.swift */; }; D85067FA292B8DE80017865A /* FileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85067F9292B8DE80017865A /* FileTypesView.swift */; }; + D850BD3329333E3800D81AAA /* DocumentPreviewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850BD3229333E3800D81AAA /* DocumentPreviewHeader.swift */; }; + D850BD3629333FF300D81AAA /* CGSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D850BD3529333FF300D81AAA /* CGSize.swift */; }; D85532A0291E5FB400C21E12 /* WizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D855329F291E5FB400C21E12 /* WizardView.swift */; }; D864870D292CB99C007CD688 /* AddFileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D864870C292CB99C007CD688 /* AddFileTypesView.swift */; }; D8648710292CD08F007CD688 /* WizardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D864870F292CD08F007CD688 /* WizardModel.swift */; }; @@ -33,7 +35,7 @@ D8E4CA5D249D061D00F5BC8E /* EditableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E4CA5C249D061D00F5BC8E /* EditableText.swift */; }; D8F763DA292259960051D740 /* RulePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F763D9292259960051D740 /* RulePicker.swift */; }; D8F763DC29225F720051D740 /* RuleFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F763DB29225F720051D740 /* RuleFormView.swift */; }; - D8F763DE29226FD30051D740 /* FilePreviewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F763DD29226FD30051D740 /* FilePreviewHeader.swift */; }; + D8F763DE29226FD30051D740 /* DocumentPreviewButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F763DD29226FD30051D740 /* DocumentPreviewButton.swift */; }; D8FB857F2916D2430024F885 /* Diligence in Frameworks */ = {isa = PBXBuildFile; productRef = D8FB857E2916D2430024F885 /* Diligence */; }; D8FB85822916D31D0024F885 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FB85812916D31D0024F885 /* AboutView.swift */; }; /* End PBXBuildFile section */ @@ -59,6 +61,8 @@ D81C5D92249BE3710083BD6F /* RulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesView.swift; sourceTree = ""; }; D81C5D94249BE6F40083BD6F /* RuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleView.swift; sourceTree = ""; }; D85067F9292B8DE80017865A /* FileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypesView.swift; sourceTree = ""; }; + D850BD3229333E3800D81AAA /* DocumentPreviewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPreviewHeader.swift; sourceTree = ""; }; + D850BD3529333FF300D81AAA /* CGSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSize.swift; sourceTree = ""; }; D855329F291E5FB400C21E12 /* WizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardView.swift; sourceTree = ""; }; D864870C292CB99C007CD688 /* AddFileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFileTypesView.swift; sourceTree = ""; }; D864870F292CD08F007CD688 /* WizardModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardModel.swift; sourceTree = ""; }; @@ -88,7 +92,7 @@ D8E4CA5C249D061D00F5BC8E /* EditableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableText.swift; sourceTree = ""; }; D8F763D9292259960051D740 /* RulePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulePicker.swift; sourceTree = ""; }; D8F763DB29225F720051D740 /* RuleFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleFormView.swift; sourceTree = ""; }; - D8F763DD29226FD30051D740 /* FilePreviewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewHeader.swift; sourceTree = ""; }; + D8F763DD29226FD30051D740 /* DocumentPreviewButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentPreviewButton.swift; sourceTree = ""; }; D8FB85812916D31D0024F885 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -134,6 +138,14 @@ path = Views; sourceTree = ""; }; + D850BD3429333FE600D81AAA /* Extensions */ = { + isa = PBXGroup; + children = ( + D850BD3529333FF300D81AAA /* CGSize.swift */, + ); + path = Extensions; + sourceTree = ""; + }; D864870E292CCF2D007CD688 /* Model */ = { isa = PBXGroup; children = ( @@ -187,6 +199,7 @@ D8CCB8112143672700AC445F /* Info.plist */, D8C15DB9291A498F002DB14D /* FileawayApp.swift */, D8CCB80C2143672700AC445F /* Assets.xcassets */, + D850BD3429333FE600D81AAA /* Extensions */, D8CCB80E2143672700AC445F /* LaunchScreen.storyboard */, D864870E292CCF2D007CD688 /* Model */, D81C5D8F249BDD3C0083BD6F /* Views */, @@ -236,11 +249,12 @@ D8F763D5292255780051D740 /* Wizard */ = { isa = PBXGroup; children = ( - D8F763DD29226FD30051D740 /* FilePreviewHeader.swift */, + D8F763DD29226FD30051D740 /* DocumentPreviewButton.swift */, + D850BD3229333E3800D81AAA /* DocumentPreviewHeader.swift */, D8F763DB29225F720051D740 /* RuleFormView.swift */, D8F763D9292259960051D740 /* RulePicker.swift */, - D855329F291E5FB400C21E12 /* WizardView.swift */, D8648711292D323A007CD688 /* RulePickerRow.swift */, + D855329F291E5FB400C21E12 /* WizardView.swift */, ); path = Wizard; sourceTree = ""; @@ -415,10 +429,12 @@ buildActionMask = 2147483647; files = ( D8D81EB2291DD07800870E6E /* VariableView.swift in Sources */, + D850BD3329333E3800D81AAA /* DocumentPreviewHeader.swift in Sources */, + D850BD3629333FF300D81AAA /* CGSize.swift in Sources */, D8FB85822916D31D0024F885 /* AboutView.swift in Sources */, D85532A0291E5FB400C21E12 /* WizardView.swift in Sources */, D8D81EB4291DD1BE00870E6E /* DestinationFooter.swift in Sources */, - D8F763DE29226FD30051D740 /* FilePreviewHeader.swift in Sources */, + D8F763DE29226FD30051D740 /* DocumentPreviewButton.swift in Sources */, D8A871D22916D56400B2932A /* ComponentItem.swift in Sources */, D8F763DC29225F720051D740 /* RuleFormView.swift in Sources */, D8D81EB0291DC70200870E6E /* ContentView.swift in Sources */, diff --git a/ios/Fileaway/Extensions/CGSize.swift b/ios/Fileaway/Extensions/CGSize.swift new file mode 100644 index 00000000..c0c72e5d --- /dev/null +++ b/ios/Fileaway/Extensions/CGSize.swift @@ -0,0 +1,27 @@ +// Copyright (c) 2018-2022 InSeven Limited +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import CoreGraphics + +extension CGSize { + + static let navigationBarIcon = CGSize(width: 32.0, height: 32.0) + +} diff --git a/ios/Fileaway/Views/Wizard/FilePreviewHeader.swift b/ios/Fileaway/Views/Wizard/DocumentPreviewButton.swift similarity index 79% rename from ios/Fileaway/Views/Wizard/FilePreviewHeader.swift rename to ios/Fileaway/Views/Wizard/DocumentPreviewButton.swift index 78cbd773..978a7931 100644 --- a/ios/Fileaway/Views/Wizard/FilePreviewHeader.swift +++ b/ios/Fileaway/Views/Wizard/DocumentPreviewButton.swift @@ -20,26 +20,29 @@ import SwiftUI +import Interact + import FileawayCore -struct FilePreviewHeader: ToolbarContent { +public struct DocumentPreviewButton: View { @State private var previewURL: URL? private let url: URL + private let size: CGSize - init(url: URL) { + init(url: URL, size: CGSize) { self.url = url + self.size = size } - var body: some ToolbarContent { - ToolbarItem(placement: .principal) { - Button("Preview") { - previewURL = url - } - .buttonStyle(.borderedProminent) - .quickLookPreview($previewURL) + public var body: some View { + Button() { + previewURL = url + } label: { + IconView(url: url, size: size) } + .quickLookPreview($previewURL) } } diff --git a/ios/Fileaway/Views/Wizard/DocumentPreviewHeader.swift b/ios/Fileaway/Views/Wizard/DocumentPreviewHeader.swift new file mode 100644 index 00000000..ae358562 --- /dev/null +++ b/ios/Fileaway/Views/Wizard/DocumentPreviewHeader.swift @@ -0,0 +1,48 @@ +// Copyright (c) 2018-2022 InSeven Limited +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +import Interact + +import FileawayCore + +struct DocumentPreviewHeader: View { + + @Binding private var isVisible: Bool + private let url: URL + + init(_ isVisible: Binding, url: URL) { + _isVisible = isVisible + self.url = url + } + + var body: some View { + ConditionalHeader($isVisible) { + DocumentPreviewButton(url: url, size: .init(width: 240, height: 240)) + .horizontalSpace(.both) + Text(url.displayName) + .font(.body) + .fontWeight(.bold) + .multilineTextAlignment(.center) + } + } + +} diff --git a/ios/Fileaway/Views/Wizard/RuleFormView.swift b/ios/Fileaway/Views/Wizard/RuleFormView.swift index 5da741fc..18325879 100644 --- a/ios/Fileaway/Views/Wizard/RuleFormView.swift +++ b/ios/Fileaway/Views/Wizard/RuleFormView.swift @@ -24,9 +24,12 @@ import FileawayCore struct RuleFormView: View { + @EnvironmentObject var wizardModel: WizardModel + @ObservedObject var ruleModel: RuleModel + @StateObject var ruleFormModel: RuleFormModel - @EnvironmentObject var wizardModel: WizardModel + @State private var isHeaderVisible: Bool = true var url: URL @@ -39,24 +42,27 @@ struct RuleFormView: View { } var body: some View { - RuleForm(ruleFormModel, url: url) - .navigationTitle(ruleFormModel.name) - .toolbar { - - FilePreviewHeader(url: url) - - ToolbarItem(placement: .navigationBarTrailing) { - Button("Move") { - do { - try ruleFormModel.move() - wizardModel.complete() - } catch { - // TODO: Present this error to the user. - print("Failed to move file with error \(error).") - } + Form { + DocumentPreviewHeader($isHeaderVisible, url: url) + RuleFormSection(ruleFormModel, url: url) + } + .navigationTitle(ruleFormModel.name) + .conditionalTitle(!isHeaderVisible) { + DocumentPreviewButton(url: url, size: .navigationBarIcon) + } + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Move") { + do { + try ruleFormModel.move() + wizardModel.complete() + } catch { + // TODO: Present this error to the user. + print("Failed to move file with error \(error).") } } } + } } } diff --git a/ios/Fileaway/Views/Wizard/RulePicker.swift b/ios/Fileaway/Views/Wizard/RulePicker.swift index ef711dba..5f685914 100644 --- a/ios/Fileaway/Views/Wizard/RulePicker.swift +++ b/ios/Fileaway/Views/Wizard/RulePicker.swift @@ -20,6 +20,8 @@ import SwiftUI +import Interact + import FileawayCore struct RulePicker: View { @@ -27,7 +29,9 @@ struct RulePicker: View { @Environment(\.dismiss) var dismiss @EnvironmentObject private var applicationModel: ApplicationModel + @StateObject var rulePickerModel: RulePickerModel + @State private var isHeaderVisible: Bool = true private let url: URL @@ -38,12 +42,13 @@ struct RulePicker: View { var body: some View { List { + DocumentPreviewHeader($isHeaderVisible, url: url) + if !rulePickerModel.recentRules.isEmpty { Section("Recent") { ForEach(rulePickerModel.recentRules) { ruleModel in RulePickerRow(url: url, ruleModel: ruleModel) } - } } Section("All Rules") { @@ -57,14 +62,15 @@ struct RulePicker: View { .navigationBarTitleDisplayMode(.inline) .toolbar { - FilePreviewHeader(url: url) - ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } } + .conditionalTitle(!isHeaderVisible) { + DocumentPreviewButton(url: url, size: .navigationBarIcon) + } .runs(rulePickerModel) }