Skip to content

Commit

Permalink
feat: Select file types on iOS (#449)
Browse files Browse the repository at this point in the history
This change also includes a drive-by refactor to use the `defaultFocus`
modifier.
  • Loading branch information
jbmorley authored Nov 22, 2022
1 parent 33a8b7c commit 0effde9
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 45 deletions.
2 changes: 1 addition & 1 deletion core/Sources/FileawayCore/Model/DirectoryModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public class DirectoryModel: ObservableObject, Identifiable, Hashable {
directoryMonitor
.$files
.compactMap { $0 } // nil is a marker that the data is loading
.combineLatest(settings.$types)
.combineLatest(settings.$fileTypes)
.receive(on: syncQueue)
.map { files, types in
let files = files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@ import UniformTypeIdentifiers

import Interact

public class FileTypePickerModel: ObservableObject, Runnable {
public class FileTypesViewModel: ObservableObject, Runnable {

@Published public var types: [UTType] = []
@Published public var fileTypes: [UTType] = []
@Published public var selection: Set<UTType.ID> = []
@Published public var input: String = ""
@Published public var newFilenameExtension: String = ""
@Published public var proposedFileType: UTType? = nil

private var settings: Settings
private var cancellables: Set<AnyCancellable> = []

@MainActor public var canSubmit: Bool {
return !self.input.isEmpty
return !self.newFilenameExtension.isEmpty
}

public init(settings: Settings) {
Expand All @@ -44,7 +45,7 @@ public class FileTypePickerModel: ObservableObject, Runnable {
@MainActor public func start() {

settings
.$types
.$fileTypes
.map { types in
return Array(types)
.sorted { lhs, rhs in
Expand All @@ -53,31 +54,42 @@ public class FileTypePickerModel: ObservableObject, Runnable {
}
.receive(on: DispatchQueue.main)
.sink { types in
self.types = types
self.fileTypes = types
}
.store(in: &cancellables)

$newFilenameExtension
.receive(on: DispatchQueue.main)
.sink { newFilenameExtension in
self.proposedFileType = UTType(filenameExtension: newFilenameExtension)
}
.store(in: &cancellables)
}

@MainActor public func stop() {
cancellables.removeAll()
}

@MainActor public func submit() {
guard let type = UTType(filenameExtension: input) else {
// TODO: Print unknown type??
print("Unknown type '\(input)'.")
guard let proposedFileType = proposedFileType else {
return
}
self.settings.types.insert(type)
input = ""
self.settings.fileTypes.insert(proposedFileType)
newFilenameExtension = ""
}

@MainActor public func remove(_ ids: Set<UTType.ID>) {
self.settings.types = self.settings.types.filter { !ids.contains($0.id) }
self.settings.fileTypes = self.settings.fileTypes.filter { !ids.contains($0.id) }
for id in ids {
selection.remove(id)
}
}

@MainActor public func remove(_ indexSet: IndexSet) {
let ids = indexSet.map { index in
return self.fileTypes[index].id
}
self.remove(Set(ids))
}

}
6 changes: 3 additions & 3 deletions core/Sources/FileawayCore/Model/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ public class Settings: ObservableObject {
@Published public var inboxUrls: [URL] = []
@Published public var archiveUrls: [URL] = []

@Published public var types: Set<UTType> = [] {
@Published public var fileTypes: Set<UTType> = [] {
didSet {
try? defaults.setCodable(types, for: .fileTypes)
try? defaults.setCodable(fileTypes, for: .fileTypes)
}
}

Expand All @@ -56,7 +56,7 @@ public class Settings: ObservableObject {
public init() {
inboxUrls = (try? defaults.securityScopeURLs(for: .inboxUrls)) ?? []
archiveUrls = (try? defaults.securityScopeURLs(for: .archiveUrls)) ?? []
types = (try? defaults.codable(Set<UTType>.self, for: .fileTypes)) ?? Self.defaultFileTypes
fileTypes = (try? defaults.codable(Set<UTType>.self, for: .fileTypes)) ?? Self.defaultFileTypes
}

public func setInboxUrls(_ urls: [URL]) throws {
Expand Down
2 changes: 1 addition & 1 deletion interact
8 changes: 8 additions & 0 deletions ios/Fileaway-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
/* Begin PBXBuildFile section */
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 */; };
D85532A0291E5FB400C21E12 /* WizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D855329F291E5FB400C21E12 /* WizardView.swift */; };
D864870D292CB99C007CD688 /* AddFileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D864870C292CB99C007CD688 /* AddFileTypesView.swift */; };
D86AA556291DB7E00078B2DA /* FilePicker in Frameworks */ = {isa = PBXBuildFile; productRef = D86AA555291DB7E00078B2DA /* FilePicker */; };
D88B353D28B61247000C8910 /* FileawayCore in Frameworks */ = {isa = PBXBuildFile; productRef = D88B353C28B61247000C8910 /* FileawayCore */; };
D8A871D22916D56400B2932A /* ComponentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8A871D12916D56400B2932A /* ComponentItem.swift */; };
Expand Down Expand Up @@ -55,7 +57,9 @@
/* Begin PBXFileReference section */
D81C5D92249BE3710083BD6F /* RulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RulesView.swift; sourceTree = "<group>"; };
D81C5D94249BE6F40083BD6F /* RuleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleView.swift; sourceTree = "<group>"; };
D85067F9292B8DE80017865A /* FileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypesView.swift; sourceTree = "<group>"; };
D855329F291E5FB400C21E12 /* WizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardView.swift; sourceTree = "<group>"; };
D864870C292CB99C007CD688 /* AddFileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFileTypesView.swift; sourceTree = "<group>"; };
D866D66326197AC80025329E /* FileawayCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FileawayCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D86AA554291DB7BF0078B2DA /* FilePicker */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FilePicker; path = ../FilePicker; sourceTree = "<group>"; };
D8A871D12916D56400B2932A /* ComponentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentItem.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -232,6 +236,8 @@
D8F763D6292255A30051D740 /* Settings */ = {
isa = PBXGroup;
children = (
D864870C292CB99C007CD688 /* AddFileTypesView.swift */,
D85067F9292B8DE80017865A /* FileTypesView.swift */,
D8CCD292246EDC6A007EF2BB /* SettingsView.swift */,
);
path = Settings;
Expand Down Expand Up @@ -411,9 +417,11 @@
D8CCD293246EDC6A007EF2BB /* SettingsView.swift in Sources */,
D8E4CA57249CF76500F5BC8E /* EditSafeButton.swift in Sources */,
D8F763DA292259960051D740 /* RulePicker.swift in Sources */,
D85067FA292B8DE80017865A /* FileTypesView.swift in Sources */,
D8CAE3EE249D3AD70047CA36 /* ErrorText.swift in Sources */,
D81C5D93249BE3710083BD6F /* RulesView.swift in Sources */,
D8E4CA5D249D061D00F5BC8E /* EditableText.swift in Sources */,
D864870D292CB99C007CD688 /* AddFileTypesView.swift in Sources */,
D8D81EB9291DD23800870E6E /* VariableRow.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
77 changes: 77 additions & 0 deletions ios/Fileaway/Views/Settings/AddFileTypesView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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 MobileCoreServices
import SwiftUI
import UniformTypeIdentifiers

import Interact

import FileawayCore

struct AddFileTypeView: View {

enum Focus: Hashable {
case add
}

@Environment(\.dismiss) var dismiss

@ObservedObject var model: FileTypesViewModel
@FocusState private var focus: Focus?

var body: some View {
NavigationView {
Form {
Section {
TextField("File Extension", text: $model.newFilenameExtension)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.focused($focus, equals: .add)
}
Section {
if let proposedFileType = model.proposedFileType {
Button {
model.submit()
dismiss()
} label: {
LabeledContent(proposedFileType.localizedDisplayName,
value: proposedFileType.preferredFilenameExtension ?? "")
}
.tint(.primary)
}
}
}
.defaultFocus($focus, .add)
.navigationTitle("Add File Type")
.navigationBarTitleDisplayMode(.inline)
.toolbar {

ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}

}
}
}

}
73 changes: 73 additions & 0 deletions ios/Fileaway/Views/Settings/FileTypesView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// 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 MobileCoreServices
import SwiftUI
import UniformTypeIdentifiers

import FileawayCore

struct FileTypesView: View {

enum SheetType: Identifiable {

var id: Self { self }

case add
}

@StateObject var model: FileTypesViewModel
@State var sheet: SheetType?

init(settings: Settings) {
_model = StateObject(wrappedValue: FileTypesViewModel(settings: settings))
}

var body: some View {
List {
ForEach(model.fileTypes) { fileType in
LabeledContent(fileType.localizedDisplayName, value: fileType.preferredFilenameExtension ?? "")
}
.onDelete { indexSet in
model.remove(indexSet)
}
}
.navigationTitle("File Types")
.toolbar {

ToolbarItem(placement: .navigationBarTrailing) {
Button {
sheet = .add
} label: {
Label("Add File Type", systemImage: "plus")
}
}

}
.sheet(item: $sheet) { sheet in
switch sheet {
case .add:
AddFileTypeView(model: model)
}
}
.runs(model)
}

}
24 changes: 19 additions & 5 deletions ios/Fileaway/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,33 @@ struct SettingsView: View {
}

@ObservedObject var applicationModel: ApplicationModel
@ObservedObject var settings: Settings
@Environment(\.dismiss) private var dismiss

@State private var sheet: SheetType? = nil

var body: some View {
NavigationStack {
Form {
Section("General") {
NavigationLink {
FileTypesView(settings: settings)
} label: {
Text("File Types")
.badge(settings.fileTypes.count)
}
}
Section("Rules") {
ForEach(applicationModel.directories(type: .archive)) { directory in
NavigationLink {
RulesView(rulesModel: directory.ruleSet)
} label: {
Text(directory.name)
if applicationModel.directories(type: .archive).isEmpty {
Text("None")
.foregroundColor(.secondary)
} else {
ForEach(applicationModel.directories(type: .archive)) { directory in
NavigationLink {
RulesView(rulesModel: directory.ruleSet)
} label: {
Text(directory.name)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion ios/Fileaway/Views/Viewer/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ struct ContentView: View {
.sheet(item: $sceneModel.sheet) { sheet in
switch sheet {
case .settings:
SettingsView(applicationModel: applicationModel)
SettingsView(applicationModel: applicationModel, settings: applicationModel.settings)
case .addLocation(let type):
FilePickerUIRepresentable(types: [.folder], allowMultiple: false, asCopy: false) { urls in
guard let url = urls.first else {
Expand Down
8 changes: 4 additions & 4 deletions macos/Fileaway-macOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
D8037B122617F8CD00F41971 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D8037B112617F8CD00F41971 /* Introspect */; };
D82D73D728B4C659006F49F1 /* Wizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82D73D628B4C659006F49F1 /* Wizard.swift */; };
D8384906292AE7EB00EA2595 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8384905292AE7EB00EA2595 /* GeneralSettingsView.swift */; };
D8384908292AE96800EA2595 /* FileTypePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8384907292AE96800EA2595 /* FileTypePickerView.swift */; };
D8384908292AE96800EA2595 /* FileTypesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8384907292AE96800EA2595 /* FileTypesView.swift */; };
D83CDCEA2620FF210063C91C /* RuleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D83CDCE92620FF210063C91C /* RuleSheet.swift */; };
D85C95812579CED30007AE1E /* SelectionToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85C95802579CED30007AE1E /* SelectionToolbar.swift */; };
D87DEF7F25774F6C006CFE85 /* FileawayApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87DEF7E25774F6C006CFE85 /* FileawayApp.swift */; };
Expand Down Expand Up @@ -68,7 +68,7 @@
/* Begin PBXFileReference section */
D82D73D628B4C659006F49F1 /* Wizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wizard.swift; sourceTree = "<group>"; };
D8384905292AE7EB00EA2595 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = "<group>"; };
D8384907292AE96800EA2595 /* FileTypePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypePickerView.swift; sourceTree = "<group>"; };
D8384907292AE96800EA2595 /* FileTypesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileTypesView.swift; sourceTree = "<group>"; };
D83CDCE92620FF210063C91C /* RuleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleSheet.swift; sourceTree = "<group>"; };
D85C95802579CED30007AE1E /* SelectionToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionToolbar.swift; sourceTree = "<group>"; };
D876A3B32654873E006D19FB /* FileawayCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = FileawayCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -278,7 +278,7 @@
D8F2524E2923A7DE00CC30AF /* ComponentValueTextField.swift */,
D8F2523F2922E5A600CC30AF /* ComponentView.swift */,
D8E7F9E22599B00A00C7409B /* DestinationTable.swift */,
D8384907292AE96800EA2595 /* FileTypePickerView.swift */,
D8384907292AE96800EA2595 /* FileTypesView.swift */,
D8384905292AE7EB00EA2595 /* GeneralSettingsView.swift */,
D8B657ED2642D40F009DE837 /* LocationsEditor.swift */,
D8FC5349257BB7A4008FB609 /* LocationsSettingsView.swift */,
Expand Down Expand Up @@ -480,7 +480,7 @@
D8F252522923BF5B00CC30AF /* NSItemProvider.swift in Sources */,
D8F252402922E5A600CC30AF /* ComponentView.swift in Sources */,
D8F8BE3725881C1A0010432F /* RulePicker.swift in Sources */,
D8384908292AE96800EA2595 /* FileTypePickerView.swift in Sources */,
D8384908292AE96800EA2595 /* FileTypesView.swift in Sources */,
D8F252442922E62400CC30AF /* VariableNameTextField.swift in Sources */,
D85C95812579CED30007AE1E /* SelectionToolbar.swift in Sources */,
D8C4CC4D292101680038B06E /* StackNavigationBar.swift in Sources */,
Expand Down
Loading

0 comments on commit 0effde9

Please sign in to comment.