Skip to content

Commit

Permalink
feat: Pick which file types to display on macOS (#440)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbmorley authored Nov 20, 2022
1 parent ab8af5a commit 0fb837d
Show file tree
Hide file tree
Showing 13 changed files with 405 additions and 84 deletions.
6 changes: 1 addition & 5 deletions core/Sources/FileawayCore/Extensions/FileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,12 @@ extension FileManager {
return urls(for: .libraryDirectory, in: .userDomainMask)[0]
}

public func files(at url: URL, extensions: [String]) -> [URL] {
public func files(at url: URL) -> [URL] {
var files: [URL] = []
if let enumerator = enumerator(at: url,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
for case let fileURL as URL in enumerator {
guard fileURL.matches(extensions: extensions) else {
continue
}

do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey])
if fileAttributes.isRegularFile! {
Expand Down
39 changes: 37 additions & 2 deletions core/Sources/FileawayCore/Extensions/UTType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,44 @@

import UniformTypeIdentifiers

extension UTType {

extension UTType: Identifiable {
public static let rules = UTType(exportedAs: "uk.co.inseven.fileaway.rules")
public static var component = UTType(exportedAs: "uk.co.inseven.fileaway.component")

public static var doc = UTType(filenameExtension: "doc")!
public static var docx = UTType(filenameExtension: "docx")!
public static var numbers = UTType(filenameExtension: "numbers")!
public static var pages = UTType(filenameExtension: "pages")!
public static var xls = UTType(filenameExtension: "xls")!
public static var xlsx = UTType(filenameExtension: "xlsx")!


public var id: String { self.identifier }

public var localizedDisplayName: String {
if let localizedDescription = localizedDescription {
return localizedDescription
}
if let preferredFilenameExtension = preferredFilenameExtension {
return preferredFilenameExtension
}
if let preferedMIMEType = preferredMIMEType {
return preferedMIMEType
}
return "Unknown Type"
}

public func conforms(to types: Set<UTType>) -> Bool {
if types.contains(self) {
return true
}
for type in supertypes {
if types.contains(type) {
return true
}
}
return false
}

}
15 changes: 15 additions & 0 deletions core/Sources/FileawayCore/Extensions/UserDefaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,19 @@ extension UserDefaults {
#endif
}

public func setCodable(_ codable: Codable, forKey defaultName: String) throws {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(codable) {
set(encoded, forKey: defaultName)
}
}

public func codable<T: Codable>(_ type: T.Type, forKey defaultName: String) throws -> T? {
guard let object = object(forKey: defaultName) as? Data else {
return nil
}
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: object)
}

}
4 changes: 2 additions & 2 deletions core/Sources/FileawayCore/Model/ApplicationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ extension EnvironmentValues {

public class ApplicationModel: ObservableObject {

private var settings = Settings()
public let settings = Settings()

@Published public var directories: [DirectoryModel] = []
@Published public var allRules: [RuleModel] = []
Expand Down Expand Up @@ -125,7 +125,7 @@ public class ApplicationModel: ObservableObject {

@MainActor private func addDirectoryObserver(type: DirectoryModel.DirectoryType, url: URL) {
dispatchPrecondition(condition: .onQueue(.main))
let directoryObserver = DirectoryModel(type: type, url: url)
let directoryObserver = DirectoryModel(settings: settings, type: type, url: url)
directories.append(directoryObserver)
directoryObserver.start()
updateCountSubscription()
Expand Down
59 changes: 38 additions & 21 deletions core/Sources/FileawayCore/Model/DirectoryModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import Combine
import SwiftUI
import UniformTypeIdentifiers

import Interact

Expand All @@ -45,52 +46,68 @@ public class DirectoryModel: ObservableObject, Identifiable, Hashable {
@Published public var files: [FileInfo] = []
@Published public var isLoading: Bool = true

private let extensions = ["pdf"]
private var directoryMonitor: DirectoryMonitor?
private let settings: Settings
private let syncQueue = DispatchQueue.init(label: "DirectoryModel.syncQueue")
private var directoryMonitor: DirectoryMonitor
private var cache: NSCache<NSURL, FileInfo> = NSCache()
private var cancelables: Set<AnyCancellable> = []

public init(type: DirectoryType, url: URL) {
public init(settings: Settings, type: DirectoryType, url: URL) {
self.settings = settings
self.type = type
self.url = url
self.ruleSet = RulesModel(url: url)
self.directoryMonitor = DirectoryMonitor(locations: [url])
}

public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}

@MainActor public func start() {
self.directoryMonitor = try! DirectoryMonitor(locations: [url],
extensions: extensions,
targetQueue: syncQueue) { urls in
let files = urls
.map { url in
if let fileInfo = self.cache.object(forKey: url as NSURL) {

directoryMonitor.start()

directoryMonitor
.$files
.compactMap { $0 } // nil is a marker that the data is loading
.combineLatest(settings.$types)
.receive(on: syncQueue)
.map { files, types in
let files = files
.compactMap { url -> FileInfo? in

// Filter by file type.
guard let type = UTType(filenameExtension: url.pathExtension), type.conforms(to: types) else {
return nil
}

// Load file info from the cache if we have it, updating if not.
if let fileInfo = self.cache.object(forKey: url as NSURL) {
return fileInfo
}
let fileInfo = FileInfo(url: url)
self.cache.setObject(fileInfo, forKey: url as NSURL)

return fileInfo
}
let fileInfo = FileInfo(url: url)
self.cache.setObject(fileInfo, forKey: url as NSURL)
return fileInfo
}
DispatchQueue.main.sync {
return files
}
.receive(on: DispatchQueue.main)
.sink { files in
self.files = files
self.isLoading = false
}
}
self.directoryMonitor?.start()
.store(in: &cancelables)
}

@MainActor public func stop() {
guard let directoryMonitor = directoryMonitor else {
return
}
directoryMonitor.stop()
self.directoryMonitor = nil
cancelables.removeAll()
}

@MainActor public func refresh() {
directoryMonitor?.refresh()
directoryMonitor.refresh()
}

}
83 changes: 83 additions & 0 deletions core/Sources/FileawayCore/Model/FileTypePickerModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// 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 Combine
import SwiftUI
import UniformTypeIdentifiers

import Interact

public class FileTypePickerModel: ObservableObject, Runnable {

@Published public var types: [UTType] = []
@Published public var selection: Set<UTType.ID> = []
@Published public var input: String = ""

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

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

public init(settings: Settings) {
self.settings = settings
}

@MainActor public func start() {

settings
.$types
.map { types in
return Array(types)
.sorted { lhs, rhs in
lhs.localizedDisplayName.localizedStandardCompare(rhs.localizedDisplayName) == .orderedAscending
}
}
.receive(on: DispatchQueue.main)
.sink { types in
self.types = types
}
.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)'.")
return
}
self.settings.types.insert(type)
input = ""
}

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

}
37 changes: 31 additions & 6 deletions core/Sources/FileawayCore/Model/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,52 @@
// SOFTWARE.

import SwiftUI
import UniformTypeIdentifiers

public class Settings: ObservableObject {

private static let inboxUrls = "inbox-urls"
private static let archiveUrls = "archive-urls"
enum Key: String {
case inboxUrls = "inbox-urls"
case archiveUrls = "archive-urls"
case fileTypes = "file-types"
}

private static let defaultFileTypes: Set<UTType> = [
.commaSeparatedText,
.doc,
.docx,
.numbers,
.pages,
.pdf,
.rtf,
.xls,
.xlsx,
]

// TODO: These are never updated during the run of the app.
@Published public var inboxUrls: [URL] = []
@Published public var archiveUrls: [URL] = []

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

private let defaults: SafeUserDefaults<Key> = SafeUserDefaults(defaults: UserDefaults.standard)

public init() {
inboxUrls = (try? UserDefaults.standard.securityScopeURLs(forKey: Self.inboxUrls)) ?? []
archiveUrls = (try? UserDefaults.standard.securityScopeURLs(forKey: Self.archiveUrls)) ?? []
inboxUrls = (try? defaults.securityScopeURLs(for: .inboxUrls)) ?? []
archiveUrls = (try? defaults.securityScopeURLs(for: .archiveUrls)) ?? []
types = (try? defaults.codable(Set<UTType>.self, for: .fileTypes)) ?? Self.defaultFileTypes
}

public func setInboxUrls(_ urls: [URL]) throws {
try UserDefaults.standard.setSecurityScopeURLs(urls, forKey: Self.inboxUrls)
try defaults.setSecurityScopeURLs(urls, for: .inboxUrls)
}

public func setArchiveUrls(_ urls: [URL]) throws {
try UserDefaults.standard.setSecurityScopeURLs(urls, forKey: Self.archiveUrls)
try defaults.setSecurityScopeURLs(urls, for: .archiveUrls)
}

}
Loading

0 comments on commit 0fb837d

Please sign in to comment.