From 05bf21565f0f8f9943f354ab1cb7e483fdbd5d6b Mon Sep 17 00:00:00 2001 From: slyboots Date: Thu, 7 Dec 2023 16:13:42 -0600 Subject: [PATCH 01/19] got search working --- Transcopied.xcodeproj/project.pbxproj | 4 +- transcopied/CopiedItem.swift | 61 +++++++++---------------- transcopied/CopiedItemsList.swift | 65 +++++++++------------------ transcopied/Transcopied.swift | 5 +-- 4 files changed, 44 insertions(+), 91 deletions(-) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 35f0376..d01f03d 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -380,7 +380,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "\"-Xfrontend -debug-time-function-bodies\""; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.slyboots.transcopied; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -420,7 +420,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "\"-Xfrontend -debug-time-function-bodies\""; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = com.slyboots.transcopied; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index a40b441..62955fa 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -16,39 +16,25 @@ enum CopiedItemType: String, Codable { case file = "FILE" } - -enum CopiedItemKindScope: CaseIterable { - case txt - case url - case img - case file - case all -} - struct CopiedItemSearchToken { enum Kind: String, Identifiable, Hashable, CaseIterable { - case txt - case url - case img - case file - case all - var id: Self {self} + case txt = "TXT" + case url = "URL" + case img = "IMG" + case file = "FILE" + case all = "" + var id: Self { self } } - enum Scope: String, Identifiable, Hashable, CaseIterable { - case kind - var id: Self {self} - } var kind: Kind = .all - var scope: Scope = .kind } -//let copiedItemSearchTokens = [ +// let copiedItemSearchTokens = [ // CopiedItemTypeToken("txt"), // CopiedItemTypeToken("url"), // CopiedItemTypeToken("url"), // CopiedItemTypeToken("file"), -//] +// ] @Model final class CopiedItem { @@ -64,21 +50,13 @@ final class CopiedItem { self.title = title } - - static func predicate(searchText: String) -> Predicate { - return #Predicate { - if searchText.isEmpty { - return true - } - else if $0.title?.localizedStandardContains(searchText) == true { - return true - } - else if $0.content?.localizedStandardContains(searchText) == true { - return true - } - else { - return false - } + static func predicate(searchText: String, searchScope: String) -> Predicate { + return #Predicate { + searchText.isEmpty + ? true + : searchScope.localizedStandardContains($0.type) + ? ($0.title ?? ($0.content ?? "")).localizedStandardContains(searchText) + : false } } } @@ -96,8 +74,10 @@ public extension Binding { } init(isNotNil source: Binding, defaultValue: T) where Value == Bool { - self.init(get: { source.wrappedValue != nil }, - set: { source.wrappedValue = $0 ? defaultValue : nil }) + self.init( + get: { source.wrappedValue != nil }, + set: { source.wrappedValue = $0 ? defaultValue : nil } + ) } } @@ -112,6 +92,7 @@ public extension Binding where Value: Equatable { else { source.wrappedValue = newValue } - }) + } + ) } } diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index b4d729e..1cfc394 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -118,21 +118,16 @@ struct CopiedItemsList: View { } } - init(searchText: String) { + init(searchText: String, searchScope: String) { _items = Query( filter: #Predicate { - if searchText.isEmpty { - return true - } - else if $0.title?.localizedStandardContains(searchText) == true { - return true - } - else if $0.content?.localizedStandardContains(searchText) == true { - return true - } - else { - return false - } + return searchText.isEmpty ? ($0.type.localizedStandardContains(searchScope) || searchScope == "") : ( + ($0.type.localizedStandardContains(searchScope) || searchScope == "") + && ( + ($0.title?.localizedStandardContains(searchText) ?? false) + || ($0.content?.localizedStandardContains(searchText) ?? false) + ) + ) }, sort: \CopiedItem.timestamp, order: .reverse @@ -156,47 +151,27 @@ struct CopiedItemsList: View { } } -class CopiedItemSearchModel: ObservableObject { - @Published var searchText: String = "" - @Published var searchScope: CopiedItemKindScope = .all - @Published var searchTokens: [CopiedItemSearchToken.Kind] = [] -} -struct CopiedItemListContainer: View { - @EnvironmentObject private var model: CopiedItemSearchModel +struct CopiedItemsListContainer: View { @State private var searchText: String = "" - @State private var searchTokens: [CopiedItemSearchToken.Kind] = [] - @State private var searchScope: CopiedItemSearchToken.Scope = .kind - - var suggestedTokens: [CopiedItemSearchToken.Kind] { - if searchText.starts(with: "#") { - return CopiedItemSearchToken.Kind.allCases - } - else { - return [] - } - } + @State private var searchTokens = [CopiedItemSearchToken.Kind]() + @State private var searchScope: CopiedItemSearchToken.Kind = .all var body: some View { - CopiedItemsList(searchText: model.searchText) - .searchable(text: $model.searchText, tokens: $model.searchTokens) { token in - switch token { - case CopiedItemSearchToken.Kind.txt: Text("Text") - case CopiedItemSearchToken.Kind.url: Text("Url") - case CopiedItemSearchToken.Kind.img: Text("Img") - case CopiedItemSearchToken.Kind.file: Text("File") - case CopiedItemSearchToken.Kind.all: Text("All") - } + CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) + .searchable(text: $searchText) + .searchScopes($searchScope) { + Text("Text").tag(CopiedItemSearchToken.Kind.txt) + Text("URL").tag(CopiedItemSearchToken.Kind.url) + Text("Image").tag(CopiedItemSearchToken.Kind.img) + Text("File").tag(CopiedItemSearchToken.Kind.file) + Text("All").tag(CopiedItemSearchToken.Kind.all) } - .searchScopes($searchScope, scopes: { - Text("Kind").tag(CopiedItemSearchToken.Scope.kind) - }) } } #Preview { NavigationStack { - CopiedItemListContainer() -// CopiedItemsList(searchText: "") + CopiedItemsListContainer() } .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index 6394493..ccd934b 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -10,8 +10,6 @@ import SwiftUI @main struct Transcopied: App { - @State var copiedItemSearch: String = "" - var sharedModelContainer: ModelContainer = { let schema = Schema([ CopiedItem.self, @@ -33,8 +31,7 @@ struct Transcopied: App { var body: some Scene { WindowGroup { NavigationStack { - CopiedItemsList(searchText: copiedItemSearch) - .searchable(text: $copiedItemSearch) + CopiedItemsListContainer() } } .modelContainer(sharedModelContainer) From fe19012f3a36b37c4b42c009d5d53afcb70cb804 Mon Sep 17 00:00:00 2001 From: slyboots Date: Thu, 7 Dec 2023 16:51:21 -0600 Subject: [PATCH 02/19] search is done --- transcopied/CopiedItem.swift | 17 ----------------- transcopied/CopiedItemsList.swift | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index 62955fa..2c269af 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -29,13 +29,6 @@ struct CopiedItemSearchToken { var kind: Kind = .all } -// let copiedItemSearchTokens = [ -// CopiedItemTypeToken("txt"), -// CopiedItemTypeToken("url"), -// CopiedItemTypeToken("url"), -// CopiedItemTypeToken("file"), -// ] - @Model final class CopiedItem { var title: String? @@ -49,16 +42,6 @@ final class CopiedItem { self.type = type.rawValue self.title = title } - - static func predicate(searchText: String, searchScope: String) -> Predicate { - return #Predicate { - searchText.isEmpty - ? true - : searchScope.localizedStandardContains($0.type) - ? ($0.title ?? ($0.content ?? "")).localizedStandardContains(searchText) - : false - } - } } public extension Binding { diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index 1cfc394..31c9a46 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -119,16 +119,16 @@ struct CopiedItemsList: View { } init(searchText: String, searchScope: String) { - _items = Query( - filter: #Predicate { - return searchText.isEmpty ? ($0.type.localizedStandardContains(searchScope) || searchScope == "") : ( - ($0.type.localizedStandardContains(searchScope) || searchScope == "") - && ( - ($0.title?.localizedStandardContains(searchText) ?? false) - || ($0.content?.localizedStandardContains(searchText) ?? false) - ) + let filter = #Predicate { item in + return searchText.isEmpty ? + (item.type.localizedStandardContains(searchScope) || searchScope == "") : + ((item.type.localizedStandardContains(searchScope) || searchScope == "") && + ((item.title?.localizedStandardContains(searchText) ?? false) || + (item.content?.localizedStandardContains(searchText) ?? false)) ) - }, + } + _items = Query( + filter: filter, sort: \CopiedItem.timestamp, order: .reverse ) @@ -159,7 +159,7 @@ struct CopiedItemsListContainer: View { var body: some View { CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) - .searchScopes($searchScope) { + .searchScopes($searchScope, activation: .onSearchPresentation) { Text("Text").tag(CopiedItemSearchToken.Kind.txt) Text("URL").tag(CopiedItemSearchToken.Kind.url) Text("Image").tag(CopiedItemSearchToken.Kind.img) From b478f583d3476cbf654594d4422f3e9bef1f347e Mon Sep 17 00:00:00 2001 From: slyboots Date: Thu, 7 Dec 2023 17:51:29 -0600 Subject: [PATCH 03/19] editor now autofocuses --- .../xcschemes/Transcopied.xcscheme | 4 +-- transcopied/CopiedEditorView.swift | 29 +++++++++++++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme index cdae682..7c092e2 100644 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme @@ -47,11 +47,9 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" - showGraphicsOverview = "Yes" - logGraphicsOverview = "Yes" allowLocationSimulation = "YES" consoleMode = "0" - structuredConsoleMode = "1"> + structuredConsoleMode = "2"> @Environment(\.modelContext) private var modelContext @Bindable var item: CopiedItem @State var title: String? - @FocusState private var editorFocused: Bool + @FocusState private var editorFocused: EditorFocused? @State private var bottomBarPlacement: ToolbarItemPlacement = .bottomBar @State private var copiedHapticTriggered: Bool = false @@ -23,6 +26,7 @@ struct CopiedEditorView: View { VStack { TextField(text: Binding($item.title, nilAs: ""), label: { EmptyView() }) .font(.title2) + .focused($editorFocused, equals: .title) Divider().padding(.vertical, 5).foregroundStyle(.primary) HStack { Text(item.type) + @@ -32,19 +36,32 @@ struct CopiedEditorView: View { .frame(maxWidth: .infinity, alignment: .leading) .foregroundStyle(.secondary) .font(.caption2) - TextEditor(text: Binding($item.content, nilAs: "")) .frame( - // maxWidth: .infinity, maxHeight: .infinity ) -// .padding(.top) .foregroundStyle(.primary) - .focused($editorFocused) + .focused($editorFocused, equals: .content) .onChange(of: editorFocused) { - bottomBarPlacement = editorFocused ? .keyboard : .bottomBar + bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar } } + .defaultFocus($editorFocused, EditorFocused.title) + .onAppear(perform: { + let _t = (item.title ?? "") + let _c = (item.content ?? "") + + if (!_c.isEmpty) { + editorFocused = .content + } + else if (!_t.isEmpty && _c.isEmpty) { + editorFocused = .title + } + else { + editorFocused = nil + } + + }) .accessibilityAction(.magicTap) { setClipboard() } .navigationTitle("Edit") .padding(.horizontal) From e59c91f5cfebc08e2f76da000491e92f71b8b764 Mon Sep 17 00:00:00 2001 From: slyboots Date: Fri, 9 Feb 2024 06:39:11 -0600 Subject: [PATCH 04/19] stuff --- Transcopied.xcodeproj/project.pbxproj | 8 +-- transcopied/CopiedItemsList.swift | 10 ++- transcopied/PBManager.swift | 18 ------ transcopied/PBoardManager.swift | 89 +++++++++++++++++++++++++++ transcopied/Transcopied.swift | 6 +- 5 files changed, 105 insertions(+), 26 deletions(-) delete mode 100644 transcopied/PBManager.swift create mode 100644 transcopied/PBoardManager.swift diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index d01f03d..7ab2efa 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; 1DBC765B2B1F5FA6004B1261 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67922B13168E000E36DA /* Assets.xcassets */; }; - 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC765E2B20BAB2004B1261 /* PBManager.swift */; }; + 1DBC765F2B20BAB2004B1261 /* PBoardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC765E2B20BAB2004B1261 /* PBoardManager.swift */; }; 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD331C82B19846400708F46 /* ModalMessage.swift */; }; 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */; }; 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678C2B13168D000E36DA /* Transcopied.swift */; }; @@ -22,7 +22,7 @@ 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetails.swift; sourceTree = ""; }; 1D8346852B1415AB004ACF46 /* CopiedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItem.swift; sourceTree = ""; }; 1DBC765D2B20040B004B1261 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 1DBC765E2B20BAB2004B1261 /* PBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBManager.swift; sourceTree = ""; }; + 1DBC765E2B20BAB2004B1261 /* PBoardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBoardManager.swift; sourceTree = ""; }; 1DD331BB2B1828CE00708F46 /* SwiftData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftData.framework; path = System/Library/Frameworks/SwiftData.framework; sourceTree = SDKROOT; }; 1DD331BC2B1828CE00708F46 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 1DD331BD2B1828CE00708F46 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; @@ -90,7 +90,7 @@ children = ( 1DFE678C2B13168D000E36DA /* Transcopied.swift */, 1DFE678E2B13168D000E36DA /* CopiedItemsList.swift */, - 1DBC765E2B20BAB2004B1261 /* PBManager.swift */, + 1DBC765E2B20BAB2004B1261 /* PBoardManager.swift */, 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */, 1DFE67922B13168E000E36DA /* Assets.xcassets */, 1DFE67942B13168E000E36DA /* Preview Content */, @@ -205,7 +205,7 @@ buildActionMask = 2147483647; files = ( 1DFE678F2B13168D000E36DA /* CopiedItemsList.swift in Sources */, - 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, + 1DBC765F2B20BAB2004B1261 /* PBoardManager.swift in Sources */, 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */, diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index 31c9a46..95f839b 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -76,6 +76,7 @@ struct CopiedItemRow: View { struct CopiedItemsList: View { @Environment(\.modelContext) private var modelContext + @Environment(PBoardManager.self) private var pbm @Query private var items: [CopiedItem] var body: some View { @@ -136,9 +137,12 @@ struct CopiedItemsList: View { private func addItem() { withAnimation { - let content = PBManager.getClipboard() - let newItem = CopiedItem(content: content, title: nil, timestamp: Date(), type: .text) - modelContext.insert(newItem) + let content = pbm.get() + if (content == nil) { return } + else { + let newItem = CopiedItem(content: content as! String, title: nil, timestamp: Date(), type: .text) + modelContext.insert(newItem) + } } } diff --git a/transcopied/PBManager.swift b/transcopied/PBManager.swift deleted file mode 100644 index 03baee3..0000000 --- a/transcopied/PBManager.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// PBManager.swift -// Transcopied -// -// Created by Dakota Lorance on 12/6/23. -// - -import Foundation -import SwiftUI - - -final class PBManager { - static func getClipboard() -> String? { - let pasteboard = UIPasteboard.general - let data = pasteboard.string - return data - } -} diff --git a/transcopied/PBoardManager.swift b/transcopied/PBoardManager.swift new file mode 100644 index 0000000..128e1b6 --- /dev/null +++ b/transcopied/PBoardManager.swift @@ -0,0 +1,89 @@ +// +// PBManager.swift +// Transcopied +// +// Created by Dakota Lorance on 12/6/23. +// + +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +typealias UIPB = UIPasteboard + +private enum PasteType: String, CaseIterable { + case text = "public.text" + case image = "public.image" + case url = "public.url" + case file = "public.file" +} + +@Observable +final class PBoardManager { + var buffer: Any? + private var board: UIPasteboard = UIPasteboard.general + + private var _canCopy: Bool { + let inType = UIPB.general.types.first != nil ? UTType(UIPB.general.types.first!) : nil + + if inType == nil { + return false + } + else { + return inType!.isSubtype(of: UTType(PasteType.image.rawValue)!) || + inType!.isSubtype(of: UTType(PasteType.file.rawValue)!) || + inType!.isSubtype(of: UTType(PasteType.url.rawValue)!) || + inType!.isSubtype(of: UTType(PasteType.text.rawValue)!) + } + } + + func get() -> Any? { + if !_canCopy { + return nil + } + if self.board.hasImages { + return self.board.images + } + else if self.board.hasURLs { + return self.board.urls + } + else { + return self.board.strings + } + } + + func set(data: [String: Any]) { + UIPB.general.setItems([data]) + } +} + +private struct PasteboardContextModifier: ViewModifier { + func body(content: Content) -> some View { + @State var pbm = PBoardManager() + Group { + content + .environment(pbm) + } + } +} + +private struct SceneActivationActionModifier: ViewModifier { + let action: () -> Void + + func body(content: Content) -> some View { + content + .onReceive(NotificationCenter.default.publisher(for: UIScene.didActivateNotification)) { _ in + action() + } + } +} + +public extension View { + func onSceneActivate(perform action: @escaping () -> Void) -> some View { + modifier(SceneActivationActionModifier(action: action)) + } + + func pasteboardContext() -> some View { + modifier(PasteboardContextModifier()) + } +} diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index ccd934b..05b6240 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -10,6 +10,8 @@ import SwiftUI @main struct Transcopied: App { + @State private var pbm: PBoardManager = PBoardManager() + var sharedModelContainer: ModelContainer = { let schema = Schema([ CopiedItem.self, @@ -29,10 +31,12 @@ struct Transcopied: App { }() var body: some Scene { - WindowGroup { + WindowGroup { NavigationStack { CopiedItemsListContainer() + .onSceneActivate {pbm.get()} } + .environment(pbm) } .modelContainer(sharedModelContainer) } From 4d41d677d91993659483c7e860c1dbdd35b2645e Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Sat, 10 Feb 2024 01:42:40 -0600 Subject: [PATCH 05/19] more tweaks --- Transcopied.xcodeproj/project.pbxproj | 27 +++++++++++++++++++++++++++ transcopied/CopiedItem.swift | 5 +++-- transcopied/CopiedItemsList.swift | 9 ++++++++- transcopied/PBoardManager.swift | 22 +++++++++++----------- transcopied/Transcopied.swift | 3 ++- 5 files changed, 51 insertions(+), 15 deletions(-) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 7ab2efa..2486bef 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1D243AF82B7676AC002560F9 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 1D243AF72B7676AC002560F9 /* SwiftUIX */; }; 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; 1DBC765B2B1F5FA6004B1261 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67922B13168E000E36DA /* Assets.xcassets */; }; @@ -45,6 +46,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1D243AF82B7676AC002560F9 /* SwiftUIX in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -126,6 +128,9 @@ dependencies = ( ); name = Transcopied; + packageProductDependencies = ( + 1D243AF72B7676AC002560F9 /* SwiftUIX */, + ); productName = nipper; productReference = 1DFE67892B13168D000E36DA /* Transcopied.app */; productType = "com.apple.product-type.application"; @@ -155,6 +160,7 @@ ); mainGroup = 1DFE67802B13168D000E36DA; packageReferences = ( + 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */, ); productRefGroup = 1DFE678A2B13168D000E36DA /* Products */; projectDirPath = ""; @@ -276,6 +282,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -341,6 +348,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; @@ -454,6 +462,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftUIX/SwiftUIX"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.9; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1D243AF72B7676AC002560F9 /* SwiftUIX */ = { + isa = XCSwiftPackageProductDependency; + package = 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */; + productName = SwiftUIX; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 1DFE67812B13168D000E36DA /* Project object */; } diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index 2c269af..354da72 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -36,8 +36,9 @@ final class CopiedItem { var timestamp: Date = Date(timeIntervalSinceNow: TimeInterval(0)) var type: String = CopiedItemType.text.rawValue - init(content: String?, title: String?, timestamp: Date, type: CopiedItemType) { - self.content = content + init(content: Any?, title: String?, timestamp: Date, type: CopiedItemType) { + // Just forcing to string for now + self.content = content as? String self.timestamp = timestamp self.type = type.rawValue self.title = title diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index 95f839b..b5fa046 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -140,7 +140,7 @@ struct CopiedItemsList: View { let content = pbm.get() if (content == nil) { return } else { - let newItem = CopiedItem(content: content as! String, title: nil, timestamp: Date(), type: .text) + let newItem = CopiedItem(content: content?.first, title: nil, timestamp: Date(), type: .text) modelContext.insert(newItem) } } @@ -161,6 +161,13 @@ struct CopiedItemsListContainer: View { @State private var searchScope: CopiedItemSearchToken.Kind = .all var body: some View { + if #available(iOS 17.1, *) { + #if DEBUG + let _ = Self._logChanges() + #endif + } else { + // Fallback on earlier versions + } CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) .searchScopes($searchScope, activation: .onSearchPresentation) { diff --git a/transcopied/PBoardManager.swift b/transcopied/PBoardManager.swift index 128e1b6..2521c75 100644 --- a/transcopied/PBoardManager.swift +++ b/transcopied/PBoardManager.swift @@ -12,10 +12,10 @@ import UniformTypeIdentifiers typealias UIPB = UIPasteboard private enum PasteType: String, CaseIterable { - case text = "public.text" + case text = "public.plain-text" case image = "public.image" case url = "public.url" - case file = "public.file" + case file = "public.file-url" } @Observable @@ -24,22 +24,22 @@ final class PBoardManager { private var board: UIPasteboard = UIPasteboard.general private var _canCopy: Bool { - let inType = UIPB.general.types.first != nil ? UTType(UIPB.general.types.first!) : nil - - if inType == nil { + if UIPB.general.numberOfItems < 1 { return false } else { - return inType!.isSubtype(of: UTType(PasteType.image.rawValue)!) || - inType!.isSubtype(of: UTType(PasteType.file.rawValue)!) || - inType!.isSubtype(of: UTType(PasteType.url.rawValue)!) || - inType!.isSubtype(of: UTType(PasteType.text.rawValue)!) + return UIPB.general.contains(pasteboardTypes: [ + PasteType.text.rawValue, + PasteType.image.rawValue, + PasteType.url.rawValue, + PasteType.file.rawValue, + ]) } } - func get() -> Any? { + func get() -> [Any]? { if !_canCopy { - return nil + return [] } if self.board.hasImages { return self.board.images diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index 05b6240..8f6bfea 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -34,7 +34,8 @@ struct Transcopied: App { WindowGroup { NavigationStack { CopiedItemsListContainer() - .onSceneActivate {pbm.get()} + .onSceneActivate {let _ = pbm.get()} + .onAppear {let _ = pbm.get()} } .environment(pbm) } From 8a6f4d6115acf6c66d2c1dfd0e959423d6197365 Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Sun, 11 Feb 2024 16:58:53 -0600 Subject: [PATCH 06/19] shits busted --- Transcopied.xcodeproj/project.pbxproj | 6 +- .../xcschemes/Transcopied.xcscheme | 2 +- transcopied/Activitea.swift | 18 ++++++ transcopied/CopiedItem.swift | 12 +++- transcopied/CopiedItemsList.swift | 16 +++-- transcopied/PBoardManager.swift | 60 ++++++++++++------- transcopied/Transcopied.swift | 6 +- 7 files changed, 83 insertions(+), 37 deletions(-) create mode 100644 transcopied/Activitea.swift diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 2486bef..a905aea 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D17E8312B779DEC002FE8E7 /* Activitea.swift */; }; 1D243AF82B7676AC002560F9 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 1D243AF72B7676AC002560F9 /* SwiftUIX */; }; 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; @@ -20,6 +21,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1D17E8312B779DEC002FE8E7 /* Activitea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Activitea.swift; sourceTree = ""; }; 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetails.swift; sourceTree = ""; }; 1D8346852B1415AB004ACF46 /* CopiedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItem.swift; sourceTree = ""; }; 1DBC765D2B20040B004B1261 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -99,6 +101,7 @@ 1D8346852B1415AB004ACF46 /* CopiedItem.swift */, 1DD331C82B19846400708F46 /* ModalMessage.swift */, 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */, + 1D17E8312B779DEC002FE8E7 /* Activitea.swift */, ); path = transcopied; sourceTree = ""; @@ -200,7 +203,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\nif [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint >/dev/null; then\n #swiftlint;\n echo \"YAY\"\nelse\n echo \"Warning swiftlint not installed! See https://github.com/realm/SwiftLint\"\nfi\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n#iif [[ \"$(uname -m)\" == arm64 ]]; then\n# export PATH=\"/opt/homebrew/bin:$PATH\"\n#fi\n\n#if which swiftlint >/dev/null; then\n #swiftlint;\n# echo \"YAY\"\n#else\n# echo \"Warning swiftlint not installed! See https://github.com/realm/SwiftLint\"\n#fi\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -214,6 +217,7 @@ 1DBC765F2B20BAB2004B1261 /* PBoardManager.swift in Sources */, 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, + 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */, 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */, 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */, 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */, diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme index 7c092e2..d3b051a 100644 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme @@ -4,7 +4,7 @@ version = "2.2"> + buildImplicitDependencies = "NO"> 0 && board.contains( + pasteboardTypes: PasteType.allCases.map(\.rawValue) + ) + } + + func get() -> Any? { + if !_canCopy { + return nil + } + + // nothing has been copied since last time + if board.changeCount == changes { + return buffer } else { - return UIPB.general.contains(pasteboardTypes: [ - PasteType.text.rawValue, - PasteType.image.rawValue, - PasteType.url.rawValue, - PasteType.file.rawValue, - ]) + changes = board.changeCount } - } - func get() -> [Any]? { - if !_canCopy { - return [] + var PT = PasteType.file.rawValue + var PV: Any? + + if board.hasImages { + PT = PasteType.image.rawValue + PV = board.images?.first! } - if self.board.hasImages { - return self.board.images + else if board.hasURLs { + PT = PasteType.url.rawValue + PV = board.urls?.first! } - else if self.board.hasURLs { - return self.board.urls + else if board.hasStrings { + PT = PasteType.text.rawValue + PV = board.string } else { - return self.board.strings + PT = PasteType.file.rawValue + PV = board.value(forPasteboardType: PT) } + + if PV != nil { + buffer = PV ?? nil + } + + return buffer } func set(data: [String: Any]) { - UIPB.general.setItems([data]) + board.setItems([data]) } } diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index 8f6bfea..7419fc3 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -31,13 +31,11 @@ struct Transcopied: App { }() var body: some Scene { - WindowGroup { + WindowGroup { NavigationStack { CopiedItemsListContainer() - .onSceneActivate {let _ = pbm.get()} - .onAppear {let _ = pbm.get()} } - .environment(pbm) + .pasteboardContext() } .modelContainer(sharedModelContainer) } From f4b1812c9edec8ac4eeb15450e79fa6077fdb24b Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Sun, 18 Feb 2024 10:15:14 -0600 Subject: [PATCH 07/19] shits broken --- Transcopied.xcodeproj/project.pbxproj | 19 +---- transcopied/CopiedEditorView.swift | 12 +-- transcopied/CopiedItem.swift | 84 ++++++++++++++------ transcopied/CopiedItemsList.swift | 35 +++++---- transcopied/PBoardManager.swift | 107 +++++++++++++++++++++----- transcopied/Transcopied.swift | 13 +++- 6 files changed, 190 insertions(+), 80 deletions(-) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index a905aea..cb48ab3 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D17E8312B779DEC002FE8E7 /* Activitea.swift */; }; - 1D243AF82B7676AC002560F9 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 1D243AF72B7676AC002560F9 /* SwiftUIX */; }; 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; 1DBC765B2B1F5FA6004B1261 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67922B13168E000E36DA /* Assets.xcassets */; }; @@ -48,7 +47,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1D243AF82B7676AC002560F9 /* SwiftUIX in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -132,7 +130,6 @@ ); name = Transcopied; packageProductDependencies = ( - 1D243AF72B7676AC002560F9 /* SwiftUIX */, ); productName = nipper; productReference = 1DFE67892B13168D000E36DA /* Transcopied.app */; @@ -264,13 +261,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COMPILER_INDEX_STORE_ENABLE = NO; + COMPILER_INDEX_STORE_ENABLE = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = V6MX2ZRT2L; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -336,13 +333,13 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COMPILER_INDEX_STORE_ENABLE = NO; + COMPILER_INDEX_STORE_ENABLE = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = V6MX2ZRT2L; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -477,14 +474,6 @@ }; }; /* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 1D243AF72B7676AC002560F9 /* SwiftUIX */ = { - isa = XCSwiftPackageProductDependency; - package = 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */; - productName = SwiftUIX; - }; -/* End XCSwiftPackageProductDependency section */ }; rootObject = 1DFE67812B13168D000E36DA /* Project object */; } diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index 156d762..aede7f9 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -24,7 +24,7 @@ struct CopiedEditorView: View { var body: some View { VStack { - TextField(text: Binding($item.title, nilAs: ""), label: { EmptyView() }) + TextField(text: $item.title, label: { EmptyView() }) .font(.title2) .focused($editorFocused, equals: .title) Divider().padding(.vertical, 5).foregroundStyle(.primary) @@ -36,7 +36,7 @@ struct CopiedEditorView: View { .frame(maxWidth: .infinity, alignment: .leading) .foregroundStyle(.secondary) .font(.caption2) - TextEditor(text: Binding($item.content, nilAs: "")) + TextEditor(text: $item.content) .frame( maxHeight: .infinity ) @@ -49,7 +49,7 @@ struct CopiedEditorView: View { .defaultFocus($editorFocused, EditorFocused.title) .onAppear(perform: { let _t = (item.title ?? "") - let _c = (item.content ?? "") + let _c = (item.content ?? Data("".utf8)) if (!_c.isEmpty) { editorFocused = .content @@ -114,13 +114,13 @@ struct CopiedEditorView: View { } private func setClipboard() { - if item.content != nil { - UIPasteboard.general.setValue(item.content as Any, forPasteboardType: UTType.plainText.identifier) + if let _ = item.content { + UIPasteboard.general.setValue(item.content!, forPasteboardType: UTType.plainText.identifier) } } } #Preview { - CopiedEditorView(item: CopiedItem(content: "Testing 123", title: "", timestamp: Date(), type: CopiedItemType.text)) + CopiedEditorView(item: CopiedItem(content: "Testing 123", type: CopiedContentType.text, title: "", timestamp: Date())) .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index 17196eb..f4ae24f 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -5,51 +5,87 @@ // Created by Dakota Lorance on 11/26/23. // +import CryptoKit import Foundation import SwiftData import SwiftUI -enum CopiedItemType: String, Codable { - case text = "TXT" - case url = "URL" - case img = "IMG" - case file = "FILE" +extension CopiedContentType { + static subscript(index: String) -> CopiedContentType { + return CopiedContentType(rawValue: index)! + } } -struct CopiedItemSearchToken { - enum Kind: String, Identifiable, Hashable, CaseIterable { - case txt = "TXT" - case url = "URL" - case img = "IMG" - case file = "FILE" - case all = "" - var id: Self { self } +enum CopiedContentType: String, Codable, Identifiable, CaseIterable, Hashable { + case text + case url + case image + case file + case any + var id: String { return "\(self)" } +} + +class CopiedContentTypeTransformer: ValueTransformer { + override class func transformedValueClass() -> AnyClass { + return NSString.self } - var kind: Kind = .all + override class func allowsReverseTransformation() -> Bool { + return true + } + + override func transformedValue(_ value: Any?) -> Any? { + let enumValue = value as? CopiedContentType + return enumValue?.rawValue + } + + override func reverseTransformedValue(_ value: Any?) -> Any? { + guard let stringValue = value as? String else { + return nil + } + return CopiedContentType(rawValue: stringValue) + } +} +extension NSValueTransformerName { + static let copiedContentTypeTransformerName = NSValueTransformerName(rawValue: "CopiedContentTypeTransformer") +} +//ValueTransformer.setValueTransformer(CopiedContentTypeTransformer(), forName: .copiedContentTypeTransformerName) + +func clipboardHash(data: Data) -> String { + return SHA256.hash(data: data).compactMap { String(format: "%02x", $0) }.joined() +} + +struct CopiedItemSearchToken { + typealias Kind = CopiedContentType + var kind: CopiedContentType = .any } @Model final class CopiedItem { - var title: String? - var content: String? - var image: Data? + @Attribute(.unique, originalName: "hash") var id: String + var title: String + var content: Data? + @Attribute(.transformable(by: CopiedContentTypeTransformer)) var type: String = "\(CopiedContentType.text)" var timestamp: Date = Date(timeIntervalSinceNow: TimeInterval(0)) - var type: String = CopiedItemType.text.rawValue - init(content: Any?, title: String?, timestamp: Date, type: CopiedItemType) { + init(content: Any?, type: CopiedContentType, title: String, timestamp: Date) { + self.type = type.rawValue switch type { - case .img: - self.image = (content as! UIImage).pngData() + case .image: + self.content = (content as! UIImage).pngData()! + case .url: + self.content = Data((content as! NSURL).absoluteString!.utf8) + case .text: + self.content = Data((content as! String).utf8) case .file: - // come back to this - _ = {} + self.content = Data(content as! Data) default: - self.content = (content as! String) + self.content = Data(content as! Data) } self.timestamp = timestamp self.type = type.rawValue self.title = title + self.id = clipboardHash(data: self.content ?? Data()) } } diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index 1dfa89b..e61af62 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -91,6 +91,7 @@ struct CopiedItemsList: View { } .onDelete(perform: deleteItems) } +// .onAppear(perform: {self.addItem()}) .navigationTitle("Clippings") .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -124,8 +125,8 @@ struct CopiedItemsList: View { return searchText.isEmpty ? (item.type.localizedStandardContains(searchScope) || searchScope == "") : ((item.type.localizedStandardContains(searchScope) || searchScope == "") && - ((item.title?.localizedStandardContains(searchText) ?? false) || - (item.content?.localizedStandardContains(searchText) ?? false)) + ((item.title.localizedStandardContains(searchText) ?? false) || + (String(data:item.content, encoding: UTF8)?.localizedStandardContains(searchText) ?? false)) ) } _items = Query( @@ -137,12 +138,19 @@ struct CopiedItemsList: View { private func addItem() { withAnimation { - let content = pbm.buffer + let content = self.pbm.currentBoard if (content == nil) { return } - else { - let newItem = CopiedItem(content: content, title: nil, timestamp: Date(), type: .text) - modelContext.insert(newItem) - } + + let pbtype = pbm.currentUTI + let uid = pbm.hashed(data: content!, type: pbtype!) + + let newItem = CopiedItem( + content: content, + type: pbm.pt2ct(pt: pbtype!)!, + title: nil, + timestamp: Date() + ) + modelContext.insert(newItem) } } @@ -156,25 +164,20 @@ struct CopiedItemsList: View { } struct CopiedItemsListContainer: View { - @Environment(PBoardManager.self) private var pbm - @State private var searchText: String = "" @State private var searchTokens = [CopiedItemSearchToken.Kind]() - @State private var searchScope: CopiedItemSearchToken.Kind = .all + @State private var searchScope: CopiedItemSearchToken.Kind = .any var body: some View { CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) .searchScopes($searchScope, activation: .onSearchPresentation) { - Text("Text").tag(CopiedItemSearchToken.Kind.txt) + Text("Text").tag(CopiedItemSearchToken.Kind.text) Text("URL").tag(CopiedItemSearchToken.Kind.url) - Text("Image").tag(CopiedItemSearchToken.Kind.img) + Text("Image").tag(CopiedItemSearchToken.Kind.image) Text("File").tag(CopiedItemSearchToken.Kind.file) - Text("All").tag(CopiedItemSearchToken.Kind.all) + Text("All").tag(CopiedItemSearchToken.Kind.any) } - .onAppear(perform: { - let _ = pbm.get() - }) } } diff --git a/transcopied/PBoardManager.swift b/transcopied/PBoardManager.swift index dc98126..e713a6b 100644 --- a/transcopied/PBoardManager.swift +++ b/transcopied/PBoardManager.swift @@ -10,9 +10,8 @@ import Foundation import SwiftUI import UniformTypeIdentifiers -typealias UIPB = UIPasteboard -private enum PasteType: String, CaseIterable { +public enum PasteType: String, CaseIterable { case image = "public.image" case url = "public.url" case text = "public.plain-text" @@ -20,37 +19,76 @@ private enum PasteType: String, CaseIterable { } @Observable -final class PBoardManager { - var buffer: Any? +class PBoardManager { + var currentBoard: Any? + var currentUTI: PasteType? var changes: Int = 0 - private var board: UIPasteboard = UIPasteboard.general - private var _canCopy: Bool { - return board.numberOfItems > 0 && board.contains( + var canCopy: Bool { + return board.numberOfItems > 0 && board.changeCount > changes && board.contains( pasteboardTypes: PasteType.allCases.map(\.rawValue) ) } - func get() -> Any? { - if !_canCopy { + func uti() -> PasteType? { + if board.numberOfItems == 0 { return nil } + if board.hasImages { + return PasteType.image + } + else if board.hasURLs { + return PasteType.url + } + else if board.hasStrings { + return PasteType.text + } + else { + return PasteType.file + } + } - // nothing has been copied since last time - if board.changeCount == changes { - return buffer + func pt2ct(pt: PasteType) -> CopiedContentType? { + if pt == PasteType.image { + return CopiedContentType.image + } + else if (pt == PasteType.url) { + return CopiedContentType.url + } + else if pt == PasteType.text { + return CopiedContentType.text } else { - changes = board.changeCount + return CopiedContentType.file } + } + + func hashed(data: Any, type: PasteType) -> Int { + switch type { + case .image: + return ((data as? Data)?.base64EncodedString().hashValue)! + case .url: + return ((data as? URL)?.absoluteString.hashValue)! + case .text: + return (data as? String)!.hashValue + default: + return (data as? Data)!.hashValue + } + } + func get() -> Any? { + if !canCopy { + return nil + } + + changes = board.changeCount var PT = PasteType.file.rawValue var PV: Any? if board.hasImages { PT = PasteType.image.rawValue - PV = board.images?.first! + PV = board.images?.first!.pngData() } else if board.hasURLs { PT = PasteType.url.rawValue @@ -65,11 +103,11 @@ final class PBoardManager { PV = board.value(forPasteboardType: PT) } - if PV != nil { - buffer = PV ?? nil - } +// if PV != nil { +// buffer = PV ?? nil +// } - return buffer + return PV } func set(data: [String: Any]) { @@ -98,12 +136,45 @@ private struct SceneActivationActionModifier: ViewModifier { } } +private struct ClipboardHasContentModifier: ViewModifier { + let action: () -> Void + + func body(content: Content) -> some View { + content + .onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in + action() + } + } +} + public extension View { func onSceneActivate(perform action: @escaping () -> Void) -> some View { modifier(SceneActivationActionModifier(action: action)) } + func onPasteboardContent(perform action: @escaping () -> Void) -> some View { + modifier(ClipboardHasContentModifier(action: action)) + } + func pasteboardContext() -> some View { modifier(PasteboardContextModifier()) } } + +extension UIPasteboard { + var hasContent: Bool { + self.numberOfItems > 0 && self.contains(pasteboardTypes: PasteType.allCases.map(\.rawValue)) + } + var hasContentPublisher: AnyPublisher { + return Just(hasContent) + .merge( + with: NotificationCenter.default + .publisher(for: UIPasteboard.changedNotification, object: self) + .map { _ in self.hasContent }) +// .merge( +// with: NotificationCenter.default +// .publisher(for: UIApplication.didBecomeActiveNotification, object: nil) +// .map { _ in self.hasContent }) + .eraseToAnyPublisher() + } +} diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index 7419fc3..df9f94e 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -11,6 +11,8 @@ import SwiftUI @main struct Transcopied: App { @State private var pbm: PBoardManager = PBoardManager() + @State private var currentBoard: Any? = nil + @State private var currentUTI: String? = nil var sharedModelContainer: ModelContainer = { let schema = Schema([ @@ -35,7 +37,16 @@ struct Transcopied: App { NavigationStack { CopiedItemsListContainer() } - .pasteboardContext() + .environment(self.pbm) + .onSceneActivate { + // whenever the list view is shown + // if we have new stuff in clip + if self.pbm.canCopy { + // then save the data from the clipboard for use later + self.pbm.currentBoard = self.pbm.get() + self.pbm.currentUTI = self.pbm.uti() + } + } } .modelContainer(sharedModelContainer) } From e7f4ecce1d25e588c149b51a73d85088dfdbb57c Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Mon, 19 Feb 2024 01:03:39 -0600 Subject: [PATCH 08/19] got it working again --- Transcopied-Info.plist | 1 + .../xcschemes/Transcopied.xcscheme | 6 + transcopied/CopiedEditorView.swift | 14 +- transcopied/CopiedItem.swift | 146 +++++++++++------- transcopied/CopiedItemsList.swift | 39 +++-- transcopied/PBoardManager.swift | 10 +- 6 files changed, 126 insertions(+), 90 deletions(-) diff --git a/Transcopied-Info.plist b/Transcopied-Info.plist index ca9a074..26fa09f 100644 --- a/Transcopied-Info.plist +++ b/Transcopied-Info.plist @@ -4,6 +4,7 @@ UIBackgroundModes + fetch remote-notification diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme index d3b051a..e18053d 100644 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme @@ -60,6 +60,12 @@ ReferencedContainer = "container:Transcopied.xcodeproj"> + + + + CopiedContentType { - return CopiedContentType(rawValue: index)! - } -} -enum CopiedContentType: String, Codable, Identifiable, CaseIterable, Hashable { +enum PasteboardContentType: String, Codable, Identifiable, CaseIterable, Hashable { case text case url case image @@ -25,67 +20,85 @@ enum CopiedContentType: String, Codable, Identifiable, CaseIterable, Hashable { var id: String { return "\(self)" } } -class CopiedContentTypeTransformer: ValueTransformer { - override class func transformedValueClass() -> AnyClass { - return NSString.self - } +enum CopiedContent { + case string(String) + case image(UIImage) + case file(Data) + case url(URL) +} +func hashString(data: Data) -> String { + return SHA256.hash(data: data).compactMap { String(format: "%02x", $0) }.joined() +} +enum CopiedItemError: Error { + case alreadyExists +} - override class func allowsReverseTransformation() -> Bool { - return true +@Model +final class CopiedItem { + var uid: String? + var title: String = "" + @Attribute(.externalStorage) var content: Data = Data() + var text: String? + @Transient var url: URL? { + return self.type == PasteboardContentType.url.rawValue ? URL(string: String(data: self.content, encoding: .utf8)!) : nil } - - override func transformedValue(_ value: Any?) -> Any? { - let enumValue = value as? CopiedContentType - return enumValue?.rawValue + @Transient var file: Data? { + return self.type == PasteboardContentType.file.rawValue ? self.content : nil } + @Transient var image: UIImage? { + return self.type == PasteboardContentType.image.rawValue ? UIImage(data: self.content) : nil + } + + var type: String = PasteboardContentType.any.rawValue + var timestamp: Date? - override func reverseTransformedValue(_ value: Any?) -> Any? { - guard let stringValue = value as? String else { - return nil + init(content: CopiedContent, type: PasteboardContentType, title: String = "", timestamp: Date?) { + switch content { + case let .image(I): + self.uid = hashString(data: I.pngData()!) + self.type = PasteboardContentType.image.rawValue + self.content = I.pngData()! + case let .file(D): + self.uid = hashString(data: D) + self.type = PasteboardContentType.file.rawValue + self.content = Data(D) + case let .string(S): + self.uid = hashString(data: Data(S.utf8)) + self.type = PasteboardContentType.text.rawValue + self.content = Data(S.utf8) + case let .url(U): + self.uid = hashString(data: Data(U.absoluteString.utf8)) + self.type = PasteboardContentType.url.rawValue + self.content = Data(U.absoluteString.utf8) } - return CopiedContentType(rawValue: stringValue) + self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) + self.title = title } -} -extension NSValueTransformerName { - static let copiedContentTypeTransformerName = NSValueTransformerName(rawValue: "CopiedContentTypeTransformer") -} -//ValueTransformer.setValueTransformer(CopiedContentTypeTransformer(), forName: .copiedContentTypeTransformerName) -func clipboardHash(data: Data) -> String { - return SHA256.hash(data: data).compactMap { String(format: "%02x", $0) }.joined() -} + // exists function to check if title already exist or not + private func exists(context: ModelContext, uid: String) -> Bool { -struct CopiedItemSearchToken { - typealias Kind = CopiedContentType - var kind: CopiedContentType = .any -} + let predicate = #Predicate { $0.uid == uid } + let descriptor = FetchDescriptor(predicate: predicate) -@Model -final class CopiedItem { - @Attribute(.unique, originalName: "hash") var id: String - var title: String - var content: Data? - @Attribute(.transformable(by: CopiedContentTypeTransformer)) var type: String = "\(CopiedContentType.text)" - var timestamp: Date = Date(timeIntervalSinceNow: TimeInterval(0)) - - init(content: Any?, type: CopiedContentType, title: String, timestamp: Date) { - self.type = type.rawValue - switch type { - case .image: - self.content = (content as! UIImage).pngData()! - case .url: - self.content = Data((content as! NSURL).absoluteString!.utf8) - case .text: - self.content = Data((content as! String).utf8) - case .file: - self.content = Data(content as! Data) - default: - self.content = Data(content as! Data) + do { + let result = try context.fetch(descriptor) + return !result.isEmpty ? true: false + } catch { + return false + } + } + + func save(context: ModelContext) throws { + + // find if the budget category with the same name already exists + if !exists(context: context, uid: self.uid!) { + // save it + context.insert(self) + } else { + // do something else + throw CopiedItemError.alreadyExists } - self.timestamp = timestamp - self.type = type.rawValue - self.title = title - self.id = clipboardHash(data: self.content ?? Data()) } } @@ -100,6 +113,7 @@ public extension Binding { source.wrappedValue = $0 }) } + init(isNotNil source: Binding, defaultValue: T) where Value == Bool { self.init( @@ -107,6 +121,10 @@ public extension Binding { set: { source.wrappedValue = $0 ? defaultValue : nil } ) } + +// init(isNotNil source: Binding, defaultValue: T) where Value == Data { +// self.init +// } } public extension Binding where Value: Equatable { @@ -124,3 +142,17 @@ public extension Binding where Value: Equatable { ) } } + + +#Preview { + Group { + @Bindable var item: CopiedItem = CopiedItem(content: .string("Test Content"), type: .text, title: "Test Title", timestamp: Date.init(timeIntervalSinceNow: 0)) + VStack { + Text(item.title) + Text(item.type) + Text(String(data: item.content, encoding: .utf8)!) + Text(item.timestamp!.ISO8601Format()) + } + } + .modelContainer(for: CopiedItem.self, inMemory: true, isAutosaveEnabled: true) +} diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index e61af62..28adbe8 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -9,7 +9,7 @@ import SwiftUI struct ConditionalRowText: View { var main: String? - var alt: String? + var alt: String? = "" var def: String = "Tap to edit" var body: some View { @@ -44,7 +44,7 @@ struct CopiedItemRow: View { .frame(maxHeight: .infinity, alignment: .center) VStack { HStack { - ConditionalRowText(main: item.title, alt: item.content, def: "Empty Clipping! Tap to edit") + ConditionalRowText(main: item.title, alt: item.text, def: "Empty Clipping! Tap to edit") .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .fixedSize(horizontal: false, vertical: true) .lineLimit(8) @@ -52,10 +52,10 @@ struct CopiedItemRow: View { HStack { Image(systemName: "info.circle") .symbolRenderingMode(.monochrome) - Text("\(item.content?.count ?? 0) characters") + Text("\(item.text?.count ?? 0 ) characters") Image(systemName: "clock") .symbolRenderingMode(.monochrome) - Text(relativeDateFmt(item.timestamp)) + Text(relativeDateFmt(item.timestamp!)) } .frame(maxWidth: .infinity, alignment: .leading) .font(.footnote) @@ -122,12 +122,12 @@ struct CopiedItemsList: View { init(searchText: String, searchScope: String) { let filter = #Predicate { item in - return searchText.isEmpty ? - (item.type.localizedStandardContains(searchScope) || searchScope == "") : - ((item.type.localizedStandardContains(searchScope) || searchScope == "") && - ((item.title.localizedStandardContains(searchText) ?? false) || - (String(data:item.content, encoding: UTF8)?.localizedStandardContains(searchText) ?? false)) - ) + return searchText.isEmpty ? + (item.type.localizedStandardContains(searchScope) || searchScope == "any") : + ((item.type.localizedStandardContains(searchScope) || searchScope == "any") && + (item.title.localizedStandardContains(searchText) || + (item.text?.localizedStandardContains(searchText) ?? false)) + ) } _items = Query( filter: filter, @@ -142,12 +142,11 @@ struct CopiedItemsList: View { if (content == nil) { return } let pbtype = pbm.currentUTI - let uid = pbm.hashed(data: content!, type: pbtype!) let newItem = CopiedItem( - content: content, + content: .string(content as! String), type: pbm.pt2ct(pt: pbtype!)!, - title: nil, + title: "", timestamp: Date() ) modelContext.insert(newItem) @@ -165,18 +164,18 @@ struct CopiedItemsList: View { struct CopiedItemsListContainer: View { @State private var searchText: String = "" - @State private var searchTokens = [CopiedItemSearchToken.Kind]() - @State private var searchScope: CopiedItemSearchToken.Kind = .any + @State private var searchTokens = [PasteboardContentType]() + @State private var searchScope: PasteboardContentType = .any var body: some View { CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) .searchScopes($searchScope, activation: .onSearchPresentation) { - Text("Text").tag(CopiedItemSearchToken.Kind.text) - Text("URL").tag(CopiedItemSearchToken.Kind.url) - Text("Image").tag(CopiedItemSearchToken.Kind.image) - Text("File").tag(CopiedItemSearchToken.Kind.file) - Text("All").tag(CopiedItemSearchToken.Kind.any) + Text("Text").tag(PasteboardContentType.text) + Text("URL").tag(PasteboardContentType.url) + Text("Image").tag(PasteboardContentType.image) + Text("File").tag(PasteboardContentType.file) + Text("All").tag(PasteboardContentType.any) } } } diff --git a/transcopied/PBoardManager.swift b/transcopied/PBoardManager.swift index e713a6b..c4f210f 100644 --- a/transcopied/PBoardManager.swift +++ b/transcopied/PBoardManager.swift @@ -49,18 +49,18 @@ class PBoardManager { } } - func pt2ct(pt: PasteType) -> CopiedContentType? { + func pt2ct(pt: PasteType) -> PasteboardContentType? { if pt == PasteType.image { - return CopiedContentType.image + return PasteboardContentType.image } else if (pt == PasteType.url) { - return CopiedContentType.url + return PasteboardContentType.url } else if pt == PasteType.text { - return CopiedContentType.text + return PasteboardContentType.text } else { - return CopiedContentType.file + return PasteboardContentType.file } } From 108b4418abef1f68a82f3a395902d350bdc43126 Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Sun, 3 Mar 2024 07:03:54 -0600 Subject: [PATCH 09/19] Lots of refactoring cuz I learned more swift --- Transcopied.xcodeproj/project.pbxproj | 32 +++- transcopied/CopiedEditorView.swift | 2 +- transcopied/CopiedItem.swift | 96 +++++++---- .../CopiedItemList/CopiedItemListRow.swift | 131 +++++++++++++++ transcopied/CopiedItemsList.swift | 135 ++++++--------- .../Migrations/CopiedItemVersions.swift | 157 ++++++++++++++++++ .../{PBoardManager.swift => PBManager.swift} | 116 ++++++++----- transcopied/Transcopied.swift | 16 +- 8 files changed, 506 insertions(+), 179 deletions(-) create mode 100644 transcopied/CopiedItemList/CopiedItemListRow.swift create mode 100644 transcopied/Migrations/CopiedItemVersions.swift rename transcopied/{PBoardManager.swift => PBManager.swift} (57%) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index cb48ab3..a7a38e8 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -7,11 +7,13 @@ objects = { /* Begin PBXBuildFile section */ + 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D086B922B9461E8004939CD /* CopiedItemVersions.swift */; }; + 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D086B962B9483F0004939CD /* CopiedItemListRow.swift */; }; 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D17E8312B779DEC002FE8E7 /* Activitea.swift */; }; 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; 1DBC765B2B1F5FA6004B1261 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67922B13168E000E36DA /* Assets.xcassets */; }; - 1DBC765F2B20BAB2004B1261 /* PBoardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC765E2B20BAB2004B1261 /* PBoardManager.swift */; }; + 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC765E2B20BAB2004B1261 /* PBManager.swift */; }; 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD331C82B19846400708F46 /* ModalMessage.swift */; }; 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */; }; 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678C2B13168D000E36DA /* Transcopied.swift */; }; @@ -20,11 +22,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1D086B922B9461E8004939CD /* CopiedItemVersions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItemVersions.swift; sourceTree = ""; }; + 1D086B962B9483F0004939CD /* CopiedItemListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItemListRow.swift; sourceTree = ""; }; 1D17E8312B779DEC002FE8E7 /* Activitea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Activitea.swift; sourceTree = ""; }; 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetails.swift; sourceTree = ""; }; 1D8346852B1415AB004ACF46 /* CopiedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItem.swift; sourceTree = ""; }; 1DBC765D2B20040B004B1261 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 1DBC765E2B20BAB2004B1261 /* PBoardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBoardManager.swift; sourceTree = ""; }; + 1DBC765E2B20BAB2004B1261 /* PBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBManager.swift; sourceTree = ""; }; 1DD331BB2B1828CE00708F46 /* SwiftData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftData.framework; path = System/Library/Frameworks/SwiftData.framework; sourceTree = SDKROOT; }; 1DD331BC2B1828CE00708F46 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 1DD331BD2B1828CE00708F46 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; @@ -53,6 +57,22 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1D086B912B946169004939CD /* Migrations */ = { + isa = PBXGroup; + children = ( + 1D086B922B9461E8004939CD /* CopiedItemVersions.swift */, + ); + path = Migrations; + sourceTree = ""; + }; + 1D086B952B948394004939CD /* CopiedItemList */ = { + isa = PBXGroup; + children = ( + 1D086B962B9483F0004939CD /* CopiedItemListRow.swift */, + ); + path = CopiedItemList; + sourceTree = ""; + }; 1DD331BA2B1828CE00708F46 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -90,9 +110,11 @@ 1DFE678B2B13168D000E36DA /* transcopied */ = { isa = PBXGroup; children = ( + 1D086B952B948394004939CD /* CopiedItemList */, + 1D086B912B946169004939CD /* Migrations */, 1DFE678C2B13168D000E36DA /* Transcopied.swift */, 1DFE678E2B13168D000E36DA /* CopiedItemsList.swift */, - 1DBC765E2B20BAB2004B1261 /* PBoardManager.swift */, + 1DBC765E2B20BAB2004B1261 /* PBManager.swift */, 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */, 1DFE67922B13168E000E36DA /* Assets.xcassets */, 1DFE67942B13168E000E36DA /* Preview Content */, @@ -211,7 +233,9 @@ buildActionMask = 2147483647; files = ( 1DFE678F2B13168D000E36DA /* CopiedItemsList.swift in Sources */, - 1DBC765F2B20BAB2004B1261 /* PBoardManager.swift in Sources */, + 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */, + 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, + 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */, 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */, diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index 5c0df71..08357cc 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -119,6 +119,6 @@ struct CopiedEditorView: View { } #Preview { - CopiedEditorView(item: CopiedItem(content: .string("Testing 123"), type: PasteboardContentType.text, title: "Preview Content", timestamp: Date())) + CopiedEditorView(item: CopiedItem(content: "Testing 123", type: PasteboardContentType.text, title: "Preview Content", timestamp: Date())) .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index 6f730b9..2d0c1fe 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -10,14 +10,17 @@ import Foundation import SwiftData import SwiftUI - enum PasteboardContentType: String, Codable, Identifiable, CaseIterable, Hashable { - case text - case url - case image - case file - case any + case text = "public.plain-text" + case url = "public.url" + case image = "public.image" + case file = "public.content" var id: String { return "\(self)" } + + static subscript(index: String) -> PasteboardContentType? { + return PasteboardContentType(rawValue: index) ?? PasteboardContentType.allCases + .first(where: { index == "\($0)" })! + } } enum CopiedContent { @@ -26,76 +29,98 @@ enum CopiedContent { case file(Data) case url(URL) } + func hashString(data: Data) -> String { return SHA256.hash(data: data).compactMap { String(format: "%02x", $0) }.joined() } + enum CopiedItemError: Error { case alreadyExists } @Model final class CopiedItem { - var uid: String? + var uid: String = "00000000-0000-0000-0000-000000000000" var title: String = "" - @Attribute(.externalStorage) var content: Data = Data() - var text: String? - @Transient var url: URL? { - return self.type == PasteboardContentType.url.rawValue ? URL(string: String(data: self.content, encoding: .utf8)!) : nil + var type: String = "" + var timestamp: Date = Date(timeIntervalSince1970: .zero) + + @Attribute(.externalStorage) + var content: Data = Data() + + @Transient + var text: String? { + get { return type == PasteboardContentType.text.rawValue ? String(data: content, encoding: .utf8) : nil } + set { + content = Data(newValue!.utf8) + } } - @Transient var file: Data? { - return self.type == PasteboardContentType.file.rawValue ? self.content : nil + + @Transient + var url: URL? { + return type == PasteboardContentType.url.rawValue ? URL(string: String(data: content, encoding: .utf8)!) : nil } - @Transient var image: UIImage? { - return self.type == PasteboardContentType.image.rawValue ? UIImage(data: self.content) : nil + + @Transient + var file: Data? { + return type == PasteboardContentType.file.rawValue ? content : nil } - var type: String = PasteboardContentType.any.rawValue - var timestamp: Date? + @Transient + var image: UIImage? { + return type == PasteboardContentType.image.rawValue ? UIImage(data: content) : nil + } - init(content: CopiedContent, type: PasteboardContentType, title: String = "", timestamp: Date?) { - switch content { - case let .image(I): + init(content: Any, type: PasteboardContentType, title: String = "", timestamp: Date?) { + switch type { + case .image: + let I = (content as! UIImage) self.uid = hashString(data: I.pngData()!) self.type = PasteboardContentType.image.rawValue self.content = I.pngData()! - case let .file(D): + case .file: + let D = (content as! Data) self.uid = hashString(data: D) self.type = PasteboardContentType.file.rawValue self.content = Data(D) - case let .string(S): + case .text: + let S = (content as! String) self.uid = hashString(data: Data(S.utf8)) self.type = PasteboardContentType.text.rawValue self.content = Data(S.utf8) - case let .url(U): + case .url: + let U = (content as! URL) self.uid = hashString(data: Data(U.absoluteString.utf8)) self.type = PasteboardContentType.url.rawValue self.content = Data(U.absoluteString.utf8) } - self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) + if !self.content.isEmpty { + self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) + } self.title = title } // exists function to check if title already exist or not private func exists(context: ModelContext, uid: String) -> Bool { - let predicate = #Predicate { $0.uid == uid } let descriptor = FetchDescriptor(predicate: predicate) do { let result = try context.fetch(descriptor) - return !result.isEmpty ? true: false - } catch { + return !result.isEmpty ? true : false + } + catch { return false } } func save(context: ModelContext) throws { - // find if the budget category with the same name already exists - if !exists(context: context, uid: self.uid!) { + if !exists(context: context, uid: uid) { // save it context.insert(self) - } else { + } + else { // do something else throw CopiedItemError.alreadyExists } @@ -113,7 +138,6 @@ public extension Binding { source.wrappedValue = $0 }) } - init(isNotNil source: Binding, defaultValue: T) where Value == Bool { self.init( @@ -143,15 +167,19 @@ public extension Binding where Value: Equatable { } } - #Preview { Group { - @Bindable var item: CopiedItem = CopiedItem(content: .string("Test Content"), type: .text, title: "Test Title", timestamp: Date.init(timeIntervalSinceNow: 0)) + @Bindable var item: CopiedItem = CopiedItem( + content: "Test Content", + type: .text, + title: "Test Title", + timestamp: Date(timeIntervalSinceNow: 0) + ) VStack { Text(item.title) Text(item.type) Text(String(data: item.content, encoding: .utf8)!) - Text(item.timestamp!.ISO8601Format()) + Text(item.timestamp.ISO8601Format()) } } .modelContainer(for: CopiedItem.self, inMemory: true, isAutosaveEnabled: true) diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift new file mode 100644 index 0000000..8d9b9bf --- /dev/null +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -0,0 +1,131 @@ +// +// CopiedItemListRow.swift +// Transcopied +// +// Created by Dakota Lorance on 3/3/24. +// + +import SwiftUI + +struct ConditionalRowText: View { + var main: String? + var alt: String? = "" + var def: String = "Tap to edit" + + var body: some View { + Text(calc(main: main, alt: alt, fallback: def)) + } + + func calc(main: String?, alt: String?, fallback: String) -> String { + if main == nil, alt == nil { + return fallback + } + else if alt != nil { + return main != nil ? String(main!.prefix(100)) : String(alt!.prefix(100)) + } + else { + // main should be safe to use + return String(main!.prefix(100)) + } + } +} + +struct CopiedItemRow: View { + @Bindable var item: CopiedItem + + var body: some View { + HStack(alignment: .top) { + VStack { + switch item.type { + case "public.image": + Image(systemName: "text.alignleft") + .imageScale(.large) + .aspectRatio(contentMode: .fit) + .foregroundColor(.accentColor) + .font(.system(size: 24)) + case "public.url": + Image(systemName: "text.alignleft") + .imageScale(.large) + .aspectRatio(contentMode: .fit) + .foregroundColor(.blue) + .font(.system(size: 24)) + case "public.plain-text": + Image(systemName: "text.alignleft") + .imageScale(.large) + .aspectRatio(contentMode: .fit) + .foregroundColor(.secondary) + .font(.system(size: 24)) + default: + Image(systemName: "text.alignleft") + .imageScale(.large) + .aspectRatio(contentMode: .fit) + .foregroundColor(.secondary) + .font(.system(size: 24)) + } + } + .frame(maxHeight: .infinity, alignment: .topTrailing) + VStack { + HStack { + Text( + !item.title.isEmpty + ? item.title + : (item.text ?? "") + ) + .font(.callout) + .lineLimit(1) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() + HStack { + Image(systemName: "info.circle") + .symbolRenderingMode(.monochrome) + Text("\(item.text?.count ?? 0) characters") + Image(systemName: "clock") + .symbolRenderingMode(.monochrome) + Text(relativeDateFmt(item.timestamp)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + .frame(maxHeight: 40, alignment: .center) + } + + private func relativeDateFmt(_ date: Date) -> String { + let fmt = RelativeDateTimeFormatter() + fmt.unitsStyle = .abbreviated + return fmt.localizedString(fromTimeInterval: Date.now.distance(to: date)) + } +} + +#Preview("Text Row", traits: .sizeThatFitsLayout) { + Group { + @State var exampleData: [CopiedItem] = [ + CopiedItem( + content: "Test Just Text. Alot of text. Like a LOOOOOOOOOOOOOOOOOOOOOO00000000000000000000000000000T", + type: .text, + timestamp: nil + ), + CopiedItem(content: "Empty title falls back to content", type: .text, title: "TITLE", timestamp: nil), + CopiedItem( + content: "Test Text With Title And Timestamp", + type: .text, + title: "TITLE", + timestamp: Date(timeIntervalSince1970: .zero) + ), + ] + List(exampleData) { item in + @Bindable var item = item + + CopiedItemRow( + item: item + ) + } + .frame(height: 500, alignment: .center) + .listStyle(.plain) + .modelContainer(for: CopiedItem.self, inMemory: true) + } +} diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift index 28adbe8..b2efec3 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemsList.swift @@ -7,76 +7,10 @@ import SwiftData import SwiftUI -struct ConditionalRowText: View { - var main: String? - var alt: String? = "" - var def: String = "Tap to edit" - - var body: some View { - Text(calc(main: main, alt: alt, fallback: def)) - } - - func calc(main: String?, alt: String?, fallback: String) -> String { - if main == nil, alt == nil { - return fallback - } - else if alt != nil { - return main != nil ? String(main!.prefix(100)) : String(alt!.prefix(100)) - } - else { - // main should be safe to use - return String(main!.prefix(100)) - } - } -} - -struct CopiedItemRow: View { - @Bindable var item: CopiedItem - - var body: some View { - HStack(alignment: .top) { - VStack { - Image(systemName: "text.alignleft") - .imageScale(.large) - .symbolRenderingMode(.monochrome) - .foregroundColor(.secondary) - } - .frame(maxHeight: .infinity, alignment: .center) - VStack { - HStack { - ConditionalRowText(main: item.title, alt: item.text, def: "Empty Clipping! Tap to edit") - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .fixedSize(horizontal: false, vertical: true) - .lineLimit(8) - } - HStack { - Image(systemName: "info.circle") - .symbolRenderingMode(.monochrome) - Text("\(item.text?.count ?? 0 ) characters") - Image(systemName: "clock") - .symbolRenderingMode(.monochrome) - Text(relativeDateFmt(item.timestamp!)) - } - .frame(maxWidth: .infinity, alignment: .leading) - .font(.footnote) - .foregroundStyle(.secondary) - } - VStack {} - } - .padding(.leading) - .frame(maxHeight: .infinity, alignment: .center) - } - - private func relativeDateFmt(_ date: Date) -> String { - let fmt = RelativeDateTimeFormatter() - fmt.unitsStyle = .abbreviated - return fmt.localizedString(fromTimeInterval: Date.now.distance(to: date)) - } -} struct CopiedItemsList: View { @Environment(\.modelContext) private var modelContext - @Environment(PBoardManager.self) private var pbm + @Environment(PBManager.self) private var pbm @Query private var items: [CopiedItem] var body: some View { @@ -91,7 +25,7 @@ struct CopiedItemsList: View { } .onDelete(perform: deleteItems) } -// .onAppear(perform: {self.addItem()}) + .onAppear(perform: {self.addItem()}) .navigationTitle("Clippings") .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -121,14 +55,27 @@ struct CopiedItemsList: View { } init(searchText: String, searchScope: String) { + let emptySearch = searchText.isEmpty + let anyScope = searchScope == ContentTypeFilter.any.rawValue + let noFilter = emptySearch && anyScope let filter = #Predicate { item in - return searchText.isEmpty ? - (item.type.localizedStandardContains(searchScope) || searchScope == "any") : - ((item.type.localizedStandardContains(searchScope) || searchScope == "any") && - (item.title.localizedStandardContains(searchText) || - (item.text?.localizedStandardContains(searchText) ?? false)) - ) + return noFilter || + (anyScope || item.type.localizedStandardContains(searchScope)) && + (emptySearch || ( + item.title.contains(searchText) || + item.content.contains(searchText.utf8) + )) } + +// let filter = #Predicate { item in +// return searchText.isEmpty ? +// (item.type.localizedStandardContains(searchScope) || searchScope == "any") : +// ((item.type.localizedStandardContains(searchScope) || searchScope == "any") && +// (item.title.contains(searchText) || +// (item.content.contains(searchText.utf8) ?? false)) +// ) +// } + _items = Query( filter: filter, sort: \CopiedItem.timestamp, @@ -138,18 +85,23 @@ struct CopiedItemsList: View { private func addItem() { withAnimation { - let content = self.pbm.currentBoard - if (content == nil) { return } - - let pbtype = pbm.currentUTI + let content = self.pbm.get() + guard !(content == nil) else { return } + let pbtype = self.pbm.uti + let newItem = CopiedItem( - content: .string(content as! String), - type: pbm.pt2ct(pt: pbtype!)!, + content: content!, + type: PasteboardContentType[pbtype!]!, title: "", timestamp: Date() ) - modelContext.insert(newItem) + do { + + try newItem.save(context: modelContext) + } catch _ { + return + } } } @@ -161,21 +113,29 @@ struct CopiedItemsList: View { } } } +enum ContentTypeFilter: String { + case text = "public.text" + case url = "public.url" + case image = "public.image" + case file = "public.content" + case any = "" + var id: String { return "\(self)" } +} struct CopiedItemsListContainer: View { @State private var searchText: String = "" @State private var searchTokens = [PasteboardContentType]() - @State private var searchScope: PasteboardContentType = .any + @State private var searchScope: ContentTypeFilter = .any var body: some View { CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) .searchScopes($searchScope, activation: .onSearchPresentation) { - Text("Text").tag(PasteboardContentType.text) - Text("URL").tag(PasteboardContentType.url) - Text("Image").tag(PasteboardContentType.image) - Text("File").tag(PasteboardContentType.file) - Text("All").tag(PasteboardContentType.any) + Text("Text").tag(ContentTypeFilter.text) + Text("URL").tag(ContentTypeFilter.url) + Text("Image").tag(ContentTypeFilter.image) + Text("File").tag(ContentTypeFilter.file) + Text("All").tag(ContentTypeFilter.any) } } } @@ -184,5 +144,6 @@ struct CopiedItemsListContainer: View { NavigationStack { CopiedItemsListContainer() } + .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/Migrations/CopiedItemVersions.swift b/transcopied/Migrations/CopiedItemVersions.swift new file mode 100644 index 0000000..a673d50 --- /dev/null +++ b/transcopied/Migrations/CopiedItemVersions.swift @@ -0,0 +1,157 @@ +// +// CopiedItemVersions.swift +// Transcopied +// +// Created by Dakota Lorance on 3/3/24. +// + +import Foundation +import SwiftData +import CoreData +import UIKit + +enum CopiedItemSchemaV1: VersionedSchema { + // initial structure + static var versionIdentifier: Schema.Version = .init(1, 0, 0) + static var models: [any PersistentModel.Type] { + return [CopiedItem.self] + } + + enum CopiedItemType: String, Codable { + case text = "TXT" + case url = "URL" + case img = "IMG" + case file = "FILE" + } + + @Model + final class CopiedItem { + var title: String? + var content: String? + var timestamp: Date = Date(timeIntervalSinceNow: TimeInterval(0)) + var type: String = CopiedItemType.text.rawValue + + init(content: String?, title: String?, timestamp: Date, type: CopiedItemType) { + self.content = content + self.timestamp = timestamp + self.type = type.rawValue + self.title = title + } + } +} + +enum CopiedItemSchemaV1_5: VersionedSchema { + // use intermediary column to migrate from string content + // over to Data content columns + static var versionIdentifier: Schema.Version = .init(1, 5, 0) + static var models: [any PersistentModel.Type] { + return [CopiedItem.self] + } + + enum CopiedItemType: String, Codable { + case text = "TXT" + case url = "URL" + case img = "IMG" + case file = "FILE" + } + + @Model + final class CopiedItem { + var title: String? + var content: String? + var timestamp: Date = Date(timeIntervalSinceNow: TimeInterval(0)) + var type: String = CopiedItemType.text.rawValue + + var dummyColumn: Data = Data() + + init(content: String?, title: String?, timestamp: Date, type: CopiedItemType) { + self.content = content + self.timestamp = timestamp + self.type = type.rawValue + self.title = title + } + } +} + +enum CopiedItemSchemaV2: VersionedSchema { + static var versionIdentifier: Schema.Version = .init(2, 0, 0) + static var models: [any PersistentModel.Type] { + return [CopiedItem.self] + } + + @Model + final class CopiedItem { + var uid: String = "00000000-0000-0000-0000-000000000000" + var title: String = "" + var type: String = "" + var timestamp: Date = Date.init(timeIntervalSince1970: .zero) + + @Attribute(.externalStorage) + var content: Data = Data() + + @Transient var text: String? + @Transient var url: URL? + @Transient var file: Data? + @Transient var image: UIImage? + + init(content: Any, type: PasteboardContentType, title: String = "", timestamp: Date?) { + switch type { + case .image: + let I = (content as! UIImage) + self.uid = hashString(data: I.pngData()!) + self.type = PasteboardContentType.image.rawValue + self.content = I.pngData()! + case .file: + let D = (content as! Data) + self.uid = hashString(data: D) + self.type = PasteboardContentType.file.rawValue + self.content = Data(D) + case .text: + let S = (content as! String) + self.uid = hashString(data: Data(S.utf8)) + self.type = PasteboardContentType.text.rawValue + self.content = Data(S.utf8) + case .url: + let U = (content as! URL) + self.uid = hashString(data: Data(U.absoluteString.utf8)) + self.type = PasteboardContentType.url.rawValue + self.content = Data(U.absoluteString.utf8) + } + if !self.content.isEmpty { + self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) + } + self.title = title + } + } +} + + +enum CopiedItemsMigrationPlan: SchemaMigrationPlan { + static var schemas: [any VersionedSchema.Type] { + [CopiedItemSchemaV1.self, CopiedItemSchemaV2.self] + } + + static var stages: [MigrationStage] { + [ + V1__V2 + ] + } + + static let V1__V1_5 = MigrationStage.custom( + fromVersion: CopiedItemSchemaV1.self, + toVersion: CopiedItemSchemaV2.self, + willMigrate: { context in + + }, + didMigrate: { context in + let copieditems = try context.fetch(FetchDescriptor()) + for item in copieditems { + if item.content != nil { + item.dummyColumn = Data(item.content!.utf8) + } + } + } + ) + + static let V1__V2 = MigrationStage.lightweight(fromVersion: CopiedItemSchemaV1.self, toVersion: CopiedItemSchemaV2.self) +} diff --git a/transcopied/PBoardManager.swift b/transcopied/PBManager.swift similarity index 57% rename from transcopied/PBoardManager.swift rename to transcopied/PBManager.swift index c4f210f..d0b04bb 100644 --- a/transcopied/PBoardManager.swift +++ b/transcopied/PBManager.swift @@ -10,50 +10,50 @@ import Foundation import SwiftUI import UniformTypeIdentifiers - public enum PasteType: String, CaseIterable { case image = "public.image" case url = "public.url" case text = "public.plain-text" case file = "public.content" + static subscript(index: String) -> PasteType? { + get { + return PasteType(rawValue: index) ?? PasteType.allCases.first(where: {"\($0)" == index })! + } + } } @Observable -class PBoardManager { - var currentBoard: Any? - var currentUTI: PasteType? +class PBManager { + var incomingBuffer: Any? var changes: Int = 0 private var board: UIPasteboard = UIPasteboard.general var canCopy: Bool { - return board.numberOfItems > 0 && board.changeCount > changes && board.contains( - pasteboardTypes: PasteType.allCases.map(\.rawValue) - ) + let types = PasteType.allCases.map(\.rawValue) + return board.contains(pasteboardTypes: types) } - func uti() -> PasteType? { - if board.numberOfItems == 0 { - return nil - } + var uti: String? { if board.hasImages { - return PasteType.image + return "public.image" } - else if board.hasURLs { - return PasteType.url + if board.hasURLs { + return "public.url" } - else if board.hasStrings { - return PasteType.text + if board.hasStrings { + return "public.plain-text" } - else { - return PasteType.file + if board.value(forPasteboardType: "public.content") != nil { + return "public.content" } + return nil } func pt2ct(pt: PasteType) -> PasteboardContentType? { if pt == PasteType.image { return PasteboardContentType.image } - else if (pt == PasteType.url) { + else if pt == PasteType.url { return PasteboardContentType.url } else if pt == PasteType.text { @@ -76,38 +76,23 @@ class PBoardManager { return (data as? Data)!.hashValue } } + func get() -> Any? { if !canCopy { return nil } changes = board.changeCount - - var PT = PasteType.file.rawValue - var PV: Any? - - if board.hasImages { - PT = PasteType.image.rawValue - PV = board.images?.first!.pngData() - } - else if board.hasURLs { - PT = PasteType.url.rawValue - PV = board.urls?.first! + if let url = board.url { + return url } - else if board.hasStrings { - PT = PasteType.text.rawValue - PV = board.string + if let image = board.image { + return image.pngData() } - else { - PT = PasteType.file.rawValue - PV = board.value(forPasteboardType: PT) + if let string = board.string { + return string } - -// if PV != nil { -// buffer = PV ?? nil -// } - - return PV + return board.value(forPasteboardType: "public.content") } func set(data: [String: Any]) { @@ -115,9 +100,46 @@ class PBoardManager { } } +// class PasteboardManager { +// // Board references the system's general pasteboard +// private var board: UIPasteboard = UIPasteboard.general +// +// // Property to check if the board can copy data +// var canCopy: Bool { +// let types: [String] = ["public.image", "public.url", "public.plain-text", "public.content"] +// guard !board.types.isEmpty else { return false } +// for type in types { +// if board.types.contains(type) { return true } +// } +// return false +// } +// +// // Function to get the UTI of the pasteboard contents +// func uti() -> String? { +// if board.hasImages { return "public.image" } +// if board.hasURLs { return "public.url" } +// if board.hasStrings { return "public.plain-text" } +// if board.data(forPasteboardType: "public.content") != nil { return "public.content" } +// return nil +// } +// +// // Function to retrieve data from the board +// func get() -> Any? { +// if let url = board.url { return url } +// if let image = board.image { return image.pngData() } +// if let string = board.string { return string } +// return board.data(forPasteboardType: "public.content") +// } +// +// // Function to set data to the board +// func set(data: [String: Any]) { +// board.setItems([data], options: [:]) +// } +// } + private struct PasteboardContextModifier: ViewModifier { func body(content: Content) -> some View { - @State var pbm = PBoardManager() + @State var pbm = PBManager() Group { content .environment(pbm) @@ -141,7 +163,7 @@ private struct ClipboardHasContentModifier: ViewModifier { func body(content: Content) -> some View { content - .onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in + .onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in action() } } @@ -163,14 +185,16 @@ public extension View { extension UIPasteboard { var hasContent: Bool { - self.numberOfItems > 0 && self.contains(pasteboardTypes: PasteType.allCases.map(\.rawValue)) + numberOfItems > 0 && contains(pasteboardTypes: PasteType.allCases.map(\.rawValue)) } + var hasContentPublisher: AnyPublisher { return Just(hasContent) .merge( with: NotificationCenter.default .publisher(for: UIPasteboard.changedNotification, object: self) - .map { _ in self.hasContent }) + .map { _ in self.hasContent } + ) // .merge( // with: NotificationCenter.default // .publisher(for: UIApplication.didBecomeActiveNotification, object: nil) diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index df9f94e..ec17741 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -10,9 +10,7 @@ import SwiftUI @main struct Transcopied: App { - @State private var pbm: PBoardManager = PBoardManager() - @State private var currentBoard: Any? = nil - @State private var currentUTI: String? = nil + @State private var pbm: PBManager = PBManager() var sharedModelContainer: ModelContainer = { let schema = Schema([ @@ -25,7 +23,11 @@ struct Transcopied: App { cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("iCloud.Transcopied") ) do { - return try ModelContainer(for: schema, configurations: [modelConfiguration]) + return try ModelContainer( + for: schema, + migrationPlan: CopiedItemsMigrationPlan.self, + configurations: [modelConfiguration] + ) } catch { fatalError("Could not create ModelContainer: \(error)") @@ -37,14 +39,14 @@ struct Transcopied: App { NavigationStack { CopiedItemsListContainer() } - .environment(self.pbm) + .pasteboardContext() +// .environment(self.pbm) .onSceneActivate { // whenever the list view is shown // if we have new stuff in clip if self.pbm.canCopy { // then save the data from the clipboard for use later - self.pbm.currentBoard = self.pbm.get() - self.pbm.currentUTI = self.pbm.uti() + self.pbm.incomingBuffer = self.pbm.get() } } } From 0ff6a27c1e2af6feb8285efd4162eaabe138bb89 Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Mon, 4 Mar 2024 08:47:59 -0600 Subject: [PATCH 10/19] got url copies almost working --- Transcopied.xcodeproj/project.pbxproj | 19 ++- transcopied/CopiedEditorView.swift | 121 ++++++++++++--- transcopied/CopiedItem.swift | 62 +++++--- .../CopiedItemList.swift} | 20 +-- .../CopiedItemList/CopiedItemListRow.swift | 145 +++++++++++++----- transcopied/PBManager.swift | 96 +++++------- transcopied/Transcopied.swift | 3 +- 7 files changed, 308 insertions(+), 158 deletions(-) rename transcopied/{CopiedItemsList.swift => CopiedItemList/CopiedItemList.swift} (85%) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index a7a38e8..1cf87ab 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D086B922B9461E8004939CD /* CopiedItemVersions.swift */; }; 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D086B962B9483F0004939CD /* CopiedItemListRow.swift */; }; + 1D086B992B95340F004939CD /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 1D086B982B95340F004939CD /* SwiftUIX */; }; 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D17E8312B779DEC002FE8E7 /* Activitea.swift */; }; 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; @@ -17,7 +18,7 @@ 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD331C82B19846400708F46 /* ModalMessage.swift */; }; 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */; }; 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678C2B13168D000E36DA /* Transcopied.swift */; }; - 1DFE678F2B13168D000E36DA /* CopiedItemsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678E2B13168D000E36DA /* CopiedItemsList.swift */; }; + 1DFE678F2B13168D000E36DA /* CopiedItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */; }; 1DFE67962B13168E000E36DA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67952B13168E000E36DA /* Preview Assets.xcassets */; }; /* End PBXBuildFile section */ @@ -41,7 +42,7 @@ 1DF36DDA2B178A890037FA6A /* TranscopiedDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TranscopiedDebug.entitlements; sourceTree = ""; }; 1DFE67892B13168D000E36DA /* Transcopied.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Transcopied.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1DFE678C2B13168D000E36DA /* Transcopied.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transcopied.swift; sourceTree = ""; }; - 1DFE678E2B13168D000E36DA /* CopiedItemsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItemsList.swift; sourceTree = ""; }; + 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItemList.swift; sourceTree = ""; }; 1DFE67922B13168E000E36DA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 1DFE67952B13168E000E36DA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -51,6 +52,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1D086B992B95340F004939CD /* SwiftUIX in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -68,6 +70,7 @@ 1D086B952B948394004939CD /* CopiedItemList */ = { isa = PBXGroup; children = ( + 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */, 1D086B962B9483F0004939CD /* CopiedItemListRow.swift */, ); path = CopiedItemList; @@ -113,7 +116,6 @@ 1D086B952B948394004939CD /* CopiedItemList */, 1D086B912B946169004939CD /* Migrations */, 1DFE678C2B13168D000E36DA /* Transcopied.swift */, - 1DFE678E2B13168D000E36DA /* CopiedItemsList.swift */, 1DBC765E2B20BAB2004B1261 /* PBManager.swift */, 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */, 1DFE67922B13168E000E36DA /* Assets.xcassets */, @@ -152,6 +154,7 @@ ); name = Transcopied; packageProductDependencies = ( + 1D086B982B95340F004939CD /* SwiftUIX */, ); productName = nipper; productReference = 1DFE67892B13168D000E36DA /* Transcopied.app */; @@ -232,7 +235,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1DFE678F2B13168D000E36DA /* CopiedItemsList.swift in Sources */, + 1DFE678F2B13168D000E36DA /* CopiedItemList.swift in Sources */, 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */, 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */, @@ -498,6 +501,14 @@ }; }; /* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1D086B982B95340F004939CD /* SwiftUIX */ = { + isa = XCSwiftPackageProductDependency; + package = 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */; + productName = SwiftUIX; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 1DFE67812B13168D000E36DA /* Project object */; } diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index 08357cc..f92476a 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -8,6 +8,7 @@ import Foundation import SwiftData import SwiftUI import UniformTypeIdentifiers +import SwiftUIX struct CopiedEditorView: View { enum EditorFocused { @@ -16,7 +17,6 @@ struct CopiedEditorView: View { @Environment(\.presentationMode) var presentationMode: Binding @Environment(\.modelContext) private var modelContext @Bindable var item: CopiedItem - @State var title: String? @FocusState private var editorFocused: EditorFocused? @State private var bottomBarPlacement: ToolbarItemPlacement = .bottomBar @@ -27,29 +27,52 @@ struct CopiedEditorView: View { TextField(text: $item.title, label: { EmptyView() }) .font(.title2) .focused($editorFocused, equals: .title) + .padding(.top) Divider().padding(.vertical, 5).foregroundStyle(.primary) - HStack { - Text(item.type) + - Text(" - ") + - Text("\(item.text?.count ?? 0) characters") + + switch item.type { + case "public.plain-text": + HStack { + Text(item.type) + + Text(" - ") + + Text("\(item.text.count) characters") + } + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(.secondary) + .font(.caption2) + TextEditor(text: $item.text) + .frame( + maxHeight: .infinity + ) + .foregroundStyle(.primary) + .focused($editorFocused, equals: .content) + .onChange(of: editorFocused) { + bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar + } + case "public.url": + VStack { + LinkPresentationView(url: item.url) + .maxHeight(50) + TextEditor(text: $item.url.stringBinding) + .padding() + .foregroundStyle(.primary) + .focused($editorFocused, equals: .content) + .onChange(of: editorFocused) { + bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar + } +// .containerRelativeFrame(.horizontal, alignment: .top) + Spacer() + } + + default: + Spacer() + EmptyView() } - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(.secondary) - .font(.caption2) - TextEditor(text: Binding($item.text, nilAs: "")) - .frame( - maxHeight: .infinity - ) - .foregroundStyle(.primary) - .focused($editorFocused, equals: .content) - .onChange(of: editorFocused) { - bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar - } } .defaultFocus($editorFocused, EditorFocused.title) .onAppear(perform: { let _t = (item.title ) - let _c = (item.text ?? "") + let _c = (item.text) if (!_c.isEmpty) { editorFocused = .content @@ -118,7 +141,63 @@ struct CopiedEditorView: View { } } -#Preview { - CopiedEditorView(item: CopiedItem(content: "Testing 123", type: PasteboardContentType.text, title: "Preview Content", timestamp: Date())) - .modelContainer(for: CopiedItem.self, inMemory: true) +#Preview("Text Clip") { + CopiedEditorView( + item: CopiedItem( + content: "Text Content", + type: PasteboardContentType.text, + title: "Test Title", + timestamp: Date() + ) + ) + .modelContainer(for: CopiedItem.self, inMemory: true) +} + +#Preview("URL Clip with Title") { + CopiedEditorView( + item: CopiedItem( + content: URL(string: "https://www.reddit.com/")!, + type: PasteboardContentType.url, + title: "URL With Title", + timestamp: Date() + ) + ) + .modelContainer(for: CopiedItem.self, inMemory: true) +} + +#Preview("URL Clip no Title") { + CopiedEditorView( + item: CopiedItem( + content: URL(string: "https://www.reddit.com/"), + type: PasteboardContentType.url, + title: "", + timestamp: Date() + ) + ) + .modelContainer(for: CopiedItem.self, inMemory: true) +} + + +#Preview("Image Clip with Title") { + CopiedEditorView( + item: CopiedItem( + content: "Testing 123", + type: PasteboardContentType.text, + title: "Preview Content", + timestamp: Date() + ) + ) + .modelContainer(for: CopiedItem.self, inMemory: true) +} + +#Preview("Image Clip no Title") { + CopiedEditorView( + item: CopiedItem( + content: "Testing 123", + type: PasteboardContentType.text, + title: "Preview Content", + timestamp: Date() + ) + ) + .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index 2d0c1fe..de82f82 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -49,55 +49,66 @@ final class CopiedItem { var content: Data = Data() @Transient - var text: String? { - get { return type == PasteboardContentType.text.rawValue ? String(data: content, encoding: .utf8) : nil } + var text: String { + get { return String(data: content, encoding: .utf8) ?? ""} set { - content = Data(newValue!.utf8) + content = Data(newValue.utf8) } } @Transient - var url: URL? { - return type == PasteboardContentType.url.rawValue ? URL(string: String(data: content, encoding: .utf8)!) : nil + var url: URL { + get { return URL(string: String(data: content, encoding: .utf8)!)!} +// get { return type == PasteboardContentType.url.rawValue ? URL(string: String(data: content, encoding: .utf8)!) : nil } + set { + content = Data(newValue.absoluteString.removingPercentEncoding!.utf8) + } } @Transient var file: Data? { - return type == PasteboardContentType.file.rawValue ? content : nil + get { return type == PasteboardContentType.file.rawValue ? content : nil} + set { content = Data(newValue!)} } @Transient var image: UIImage? { - return type == PasteboardContentType.image.rawValue ? UIImage(data: content) : nil + get {return type == PasteboardContentType.image.rawValue ? UIImage(data: content) : nil} + set { + content = Data(newValue!.pngData()!) + } } - init(content: Any, type: PasteboardContentType, title: String = "", timestamp: Date?) { + init(content: Any, type: PasteboardContentType, title: String = "", timestamp: Date? = nil) { switch type { case .image: let I = (content as! UIImage) self.uid = hashString(data: I.pngData()!) self.type = PasteboardContentType.image.rawValue self.content = I.pngData()! - case .file: - let D = (content as! Data) - self.uid = hashString(data: D) - self.type = PasteboardContentType.file.rawValue - self.content = Data(D) - case .text: - let S = (content as! String) - self.uid = hashString(data: Data(S.utf8)) - self.type = PasteboardContentType.text.rawValue - self.content = Data(S.utf8) + self.title = title case .url: let U = (content as! URL) self.uid = hashString(data: Data(U.absoluteString.utf8)) self.type = PasteboardContentType.url.rawValue self.content = Data(U.absoluteString.utf8) + self.title = title.isEmpty ? (U.host() ?? U.formatted(.url)) : title + case .text: + let S = (content as! String) + self.uid = hashString(data: Data(S.utf8)) + self.type = PasteboardContentType.text.rawValue + self.content = Data(S.utf8) + self.title = title + case .file: + let D = (content as! Data) + self.uid = hashString(data: D) + self.type = PasteboardContentType.file.rawValue + self.content = Data(D) + self.title = title } if !self.content.isEmpty { - self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) + self.timestamp = timestamp ?? Date(timeIntervalSinceNow: 0) } - self.title = title } // exists function to check if title already exist or not @@ -127,6 +138,17 @@ final class CopiedItem { } } +public extension Binding where Value == URL { + var stringBinding: Binding { + .init(get: { + self.wrappedValue.absoluteString.removingPercentEncoding! + }, set: {newValue in + self.wrappedValue = URL(string: newValue)! + }) + } +} + + public extension Binding { init(_ source: Binding, _ defaultValue: Value) { self.init(get: { diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemList/CopiedItemList.swift similarity index 85% rename from transcopied/CopiedItemsList.swift rename to transcopied/CopiedItemList/CopiedItemList.swift index b2efec3..7132f87 100644 --- a/transcopied/CopiedItemsList.swift +++ b/transcopied/CopiedItemList/CopiedItemList.swift @@ -8,7 +8,7 @@ import SwiftData import SwiftUI -struct CopiedItemsList: View { +struct CopiedItemList: View { @Environment(\.modelContext) private var modelContext @Environment(PBManager.self) private var pbm @Query private var items: [CopiedItem] @@ -17,7 +17,7 @@ struct CopiedItemsList: View { List { ForEach(items) { item in NavigationLink { - CopiedEditorView(item: item, title: item.title) + CopiedEditorView(item: item) } label: { CopiedItemRow(item: item) } @@ -66,16 +66,6 @@ struct CopiedItemsList: View { item.content.contains(searchText.utf8) )) } - -// let filter = #Predicate { item in -// return searchText.isEmpty ? -// (item.type.localizedStandardContains(searchScope) || searchScope == "any") : -// ((item.type.localizedStandardContains(searchScope) || searchScope == "any") && -// (item.title.contains(searchText) || -// (item.content.contains(searchText.utf8) ?? false)) -// ) -// } - _items = Query( filter: filter, sort: \CopiedItem.timestamp, @@ -122,13 +112,13 @@ enum ContentTypeFilter: String { var id: String { return "\(self)" } } -struct CopiedItemsListContainer: View { +struct CopiedItemListContainer: View { @State private var searchText: String = "" @State private var searchTokens = [PasteboardContentType]() @State private var searchScope: ContentTypeFilter = .any var body: some View { - CopiedItemsList(searchText: searchText, searchScope: searchScope.rawValue) + CopiedItemList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) .searchScopes($searchScope, activation: .onSearchPresentation) { Text("Text").tag(ContentTypeFilter.text) @@ -142,7 +132,7 @@ struct CopiedItemsListContainer: View { #Preview { NavigationStack { - CopiedItemsListContainer() + CopiedItemListContainer() } .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index 8d9b9bf..10b1d12 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -32,66 +32,67 @@ struct ConditionalRowText: View { struct CopiedItemRow: View { @Bindable var item: CopiedItem + private var iconName: String + private var iconColor: Color var body: some View { - HStack(alignment: .top) { - VStack { - switch item.type { - case "public.image": - Image(systemName: "text.alignleft") - .imageScale(.large) - .aspectRatio(contentMode: .fit) - .foregroundColor(.accentColor) - .font(.system(size: 24)) - case "public.url": - Image(systemName: "text.alignleft") - .imageScale(.large) - .aspectRatio(contentMode: .fit) - .foregroundColor(.blue) - .font(.system(size: 24)) - case "public.plain-text": - Image(systemName: "text.alignleft") - .imageScale(.large) - .aspectRatio(contentMode: .fit) - .foregroundColor(.secondary) - .font(.system(size: 24)) - default: - Image(systemName: "text.alignleft") - .imageScale(.large) - .aspectRatio(contentMode: .fit) - .foregroundColor(.secondary) - .font(.system(size: 24)) - } - } - .frame(maxHeight: .infinity, alignment: .topTrailing) + HStack(alignment: .center) { VStack { HStack { + Image(systemName: self.iconName) + .resizable() + .scaledToFit() + .foregroundStyle(self.iconColor) Text( !item.title.isEmpty - ? item.title - : (item.text ?? "") + ? item.title + : (item.text) ) - .font(.callout) - .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) - .fixedSize(horizontal: false, vertical: true) + .underline(self.item.type.contains("url")) +// .foregroundStyle(.blue) } - Spacer() HStack { - Image(systemName: "info.circle") - .symbolRenderingMode(.monochrome) - Text("\(item.text?.count ?? 0) characters") + // TODO: make this section differ based on item type + if !item.text.isEmpty { + Image(systemName: "info.circle") + .symbolRenderingMode(.monochrome) + Text("\(item.text.count) characters") + } Image(systemName: "clock") .symbolRenderingMode(.monochrome) Text(relativeDateFmt(item.timestamp)) } + .dynamicTypeSize(.xSmall) .frame(maxWidth: .infinity, alignment: .leading) .font(.footnote) - .foregroundStyle(.secondary) + .foregroundStyle(.tertiary) } + .frame(maxHeight: 40) + } + } + + + init(item: CopiedItem) { + self.item = item + switch item.type { + case "public.image": + self.iconName = "photo" + self.iconColor = .accent + case "public.url": + self.iconName = "link" + self.iconColor = .blue + case "public.plain-text": + self.iconName = "text.alignleft" + self.iconColor = .secondary + case "public.content": + self.iconName = "curlybraces.square" + self.iconColor = .secondary + default: + self.iconName = "questionmark" + self.iconColor = .primary } - .frame(maxHeight: 40, alignment: .center) } private func relativeDateFmt(_ date: Date) -> String { @@ -125,7 +126,67 @@ struct CopiedItemRow: View { ) } .frame(height: 500, alignment: .center) - .listStyle(.plain) + .listStyle(.automatic) .modelContainer(for: CopiedItem.self, inMemory: true) } } + + +#Preview("Url Row", traits: .sizeThatFitsLayout) { + Group { + @State var exampleData: [CopiedItem] = [ + CopiedItem( + content: URL(string: "https://google.com")!.absoluteURL, + type: .url, + timestamp: Date.init(timeIntervalSinceNow: -10000) + ), + CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!.absoluteURL, type: .url, timestamp: nil), + CopiedItem( + content: URL(string: "https://areally.long.url/?q=123idhwiue")!, + type: .url, + title: "URL with a title", + timestamp: Date(timeIntervalSince1970: .zero) + ), + ] + List(exampleData) { item in + @Bindable var item = item + + CopiedItemRow( + item: item + ) + } + .frame(height: 500, alignment: .center) + .listStyle(.automatic) + .modelContainer(for: CopiedItem.self, inMemory: true) + } +} + +#Preview("Url Preview Row", traits: .sizeThatFitsLayout) { + Group { + @State var exampleData: [CopiedItem] = [ + CopiedItem( + content: URL(string: "https://google.com")!, + type: .url, + timestamp: Date.init(timeIntervalSinceNow: -10000) + ), + CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!, type: .url, timestamp: nil), + CopiedItem( + content: URL(string: "https://areally.long.url/?q=123idhwiue")!, + type: .url, + title: "URL with a title", + timestamp: Date(timeIntervalSince1970: .zero) + ), + ] + List(exampleData) { item in + @Bindable var item = item + + CopiedItemRow( + item: item + ) + } + .frame(height: 500, alignment: .center) + .listStyle(.automatic) + .modelContainer(for: CopiedItem.self, inMemory: true) + } +} + diff --git a/transcopied/PBManager.swift b/transcopied/PBManager.swift index d0b04bb..025638d 100644 --- a/transcopied/PBManager.swift +++ b/transcopied/PBManager.swift @@ -9,6 +9,8 @@ import Combine import Foundation import SwiftUI import UniformTypeIdentifiers +import LinkPresentation +import SwiftUIX public enum PasteType: String, CaseIterable { case image = "public.image" @@ -41,6 +43,9 @@ class PBManager { return "public.url" } if board.hasStrings { + if board.string!.isURL() { + return "public.url" + } return "public.plain-text" } if board.value(forPasteboardType: "public.content") != nil { @@ -49,21 +54,6 @@ class PBManager { return nil } - func pt2ct(pt: PasteType) -> PasteboardContentType? { - if pt == PasteType.image { - return PasteboardContentType.image - } - else if pt == PasteType.url { - return PasteboardContentType.url - } - else if pt == PasteType.text { - return PasteboardContentType.text - } - else { - return PasteboardContentType.file - } - } - func hashed(data: Any, type: PasteType) -> Int { switch type { case .image: @@ -90,6 +80,9 @@ class PBManager { return image.pngData() } if let string = board.string { + if string.isURL() { + return URL(string: string) + } return string } return board.value(forPasteboardType: "public.content") @@ -100,42 +93,40 @@ class PBManager { } } -// class PasteboardManager { -// // Board references the system's general pasteboard -// private var board: UIPasteboard = UIPasteboard.general -// -// // Property to check if the board can copy data -// var canCopy: Bool { -// let types: [String] = ["public.image", "public.url", "public.plain-text", "public.content"] -// guard !board.types.isEmpty else { return false } -// for type in types { -// if board.types.contains(type) { return true } -// } -// return false -// } -// -// // Function to get the UTI of the pasteboard contents -// func uti() -> String? { -// if board.hasImages { return "public.image" } -// if board.hasURLs { return "public.url" } -// if board.hasStrings { return "public.plain-text" } -// if board.data(forPasteboardType: "public.content") != nil { return "public.content" } -// return nil -// } -// -// // Function to retrieve data from the board -// func get() -> Any? { -// if let url = board.url { return url } -// if let image = board.image { return image.pngData() } -// if let string = board.string { return string } -// return board.data(forPasteboardType: "public.content") -// } -// -// // Function to set data to the board -// func set(data: [String: Any]) { -// board.setItems([data], options: [:]) -// } -// } +public extension String { + func isURL() -> Bool { + guard let url = URL(string: self) else { + return false + } + return !(url.scheme == nil || url.host() == nil) + } +} +struct TView: UIViewRepresentable { + func updateUIView(_ uiView: LPLinkView, context: Context) { + return + } + + func makeUIView(context: Context) -> LPLinkView { + let uiView = LPLinkView(url: URL(string: "https://www.google.com/")!) + + return uiView + } +} + +#Preview { + Group { + ActivityIndicator() + .animated(true) + .style(.large) + VStack { + Text("https://www.google.com/") + .frame(width: .infinity, alignment: .leading) + LinkPresentationView(url:URL(string: "https://www.facebook.com/")!) + .frame(width: 100, alignment: .leading) + } + .frame(height: 200) + } +} private struct PasteboardContextModifier: ViewModifier { func body(content: Content) -> some View { @@ -170,9 +161,6 @@ private struct ClipboardHasContentModifier: ViewModifier { } public extension View { - func onSceneActivate(perform action: @escaping () -> Void) -> some View { - modifier(SceneActivationActionModifier(action: action)) - } func onPasteboardContent(perform action: @escaping () -> Void) -> some View { modifier(ClipboardHasContentModifier(action: action)) diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index ec17741..e53d707 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -37,10 +37,9 @@ struct Transcopied: App { var body: some Scene { WindowGroup { NavigationStack { - CopiedItemsListContainer() + CopiedItemListContainer() } .pasteboardContext() -// .environment(self.pbm) .onSceneActivate { // whenever the list view is shown // if we have new stuff in clip From e0ec7691a9edc63a8ce76a83c9dfa901e4ae4499 Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Mon, 18 Mar 2024 22:22:41 -0500 Subject: [PATCH 11/19] blah --- Transcopied-Info.plist | 1 + Transcopied.xcodeproj/project.pbxproj | 76 ++++++++------- .../xcshareddata/xcschemes/Test.xcscheme | 94 ------------------- .../xcschemes/Transcopied.xcscheme | 22 ++--- TranscopiedDebug.entitlements | 4 +- TranscopiedRelease.entitlements | 4 +- transcopied/CopiedEditorView.swift | 13 ++- transcopied/CopiedItem.swift | 30 +++--- .../CopiedItemList/CopiedItemList.swift | 66 +++++++++---- .../CopiedItemList/CopiedItemListRow.swift | 79 ++++++++++------ .../Migrations/CopiedItemVersions.swift | 90 ++++++++++++++++-- transcopied/PBManager.swift | 2 +- transcopied/Transcopied.swift | 5 +- 13 files changed, 259 insertions(+), 227 deletions(-) delete mode 100644 Transcopied.xcodeproj/xcshareddata/xcschemes/Test.xcscheme diff --git a/Transcopied-Info.plist b/Transcopied-Info.plist index 26fa09f..634f4e4 100644 --- a/Transcopied-Info.plist +++ b/Transcopied-Info.plist @@ -5,6 +5,7 @@ UIBackgroundModes fetch + processing remote-notification diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 1cf87ab..fbacb93 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -13,12 +13,13 @@ 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D17E8312B779DEC002FE8E7 /* Activitea.swift */; }; 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */; }; 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; + 1D8B72D92B9EA6B30028D7D4 /* CompoundPredicate in Frameworks */ = {isa = PBXBuildFile; productRef = 1D8B72D82B9EA6B30028D7D4 /* CompoundPredicate */; }; + 1D95123D2BA1AFA7009AD8B6 /* CopiedItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */; }; 1DBC765B2B1F5FA6004B1261 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67922B13168E000E36DA /* Assets.xcassets */; }; 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC765E2B20BAB2004B1261 /* PBManager.swift */; }; 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD331C82B19846400708F46 /* ModalMessage.swift */; }; 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */; }; 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678C2B13168D000E36DA /* Transcopied.swift */; }; - 1DFE678F2B13168D000E36DA /* CopiedItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */; }; 1DFE67962B13168E000E36DA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67952B13168E000E36DA /* Preview Assets.xcassets */; }; /* End PBXBuildFile section */ @@ -53,6 +54,7 @@ buildActionMask = 2147483647; files = ( 1D086B992B95340F004939CD /* SwiftUIX in Frameworks */, + 1D8B72D92B9EA6B30028D7D4 /* CompoundPredicate in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -143,7 +145,6 @@ isa = PBXNativeTarget; buildConfigurationList = 1DFE67992B13168E000E36DA /* Build configuration list for PBXNativeTarget "Transcopied" */; buildPhases = ( - 1DD331C52B194D4F00708F46 /* ShellScript */, 1DFE67852B13168D000E36DA /* Sources */, 1DFE67862B13168D000E36DA /* Frameworks */, 1DFE67872B13168D000E36DA /* Resources */, @@ -155,6 +156,7 @@ name = Transcopied; packageProductDependencies = ( 1D086B982B95340F004939CD /* SwiftUIX */, + 1D8B72D82B9EA6B30028D7D4 /* CompoundPredicate */, ); productName = nipper; productReference = 1DFE67892B13168D000E36DA /* Transcopied.app */; @@ -166,8 +168,8 @@ 1DFE67812B13168D000E36DA /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1500; + BuildIndependentTargetsInParallel = NO; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1500; TargetAttributes = { 1DFE67882B13168D000E36DA = { @@ -186,6 +188,7 @@ mainGroup = 1DFE67802B13168D000E36DA; packageReferences = ( 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */, + 1D8B72D72B9EA6B30028D7D4 /* XCRemoteSwiftPackageReference "CompoundPredicate" */, ); productRefGroup = 1DFE678A2B13168D000E36DA /* Products */; projectDirPath = ""; @@ -208,43 +211,21 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 1DD331C52B194D4F00708F46 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n#iif [[ \"$(uname -m)\" == arm64 ]]; then\n# export PATH=\"/opt/homebrew/bin:$PATH\"\n#fi\n\n#if which swiftlint >/dev/null; then\n #swiftlint;\n# echo \"YAY\"\n#else\n# echo \"Warning swiftlint not installed! See https://github.com/realm/SwiftLint\"\n#fi\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 1DFE67852B13168D000E36DA /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1DFE678F2B13168D000E36DA /* CopiedItemList.swift in Sources */, - 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */, - 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, - 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */, - 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, - 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */, - 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */, + 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */, 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */, + 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */, + 1D95123D2BA1AFA7009AD8B6 /* CopiedItemList.swift in Sources */, + 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */, + 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */, + 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, + 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -254,7 +235,7 @@ 1DFE67972B13168E000E36DA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; + ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "\"AppIcon Dark Alt\" \"AppIcon Light Alt\" \"AppIcon Dark\""; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon Light"; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -294,7 +275,7 @@ DEVELOPMENT_TEAM = V6MX2ZRT2L; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -303,6 +284,10 @@ "DEBUG=1", "$(inherited)", ); + "GCC_PREPROCESSOR_DEFINITIONS[arch=*]" = ( + "DEBUG=1", + "$(inherited)", + ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -326,7 +311,7 @@ 1DFE67982B13168E000E36DA /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; + ALWAYS_SEARCH_USER_PATHS = YES; ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "\"AppIcon Dark Alt\" \"AppIcon Light Alt\" \"AppIcon Dark\""; ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon Light"; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -366,7 +351,7 @@ DEVELOPMENT_TEAM = V6MX2ZRT2L; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -478,7 +463,7 @@ 1DFE67982B13168E000E36DA /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 1DFE67992B13168E000E36DA /* Build configuration list for PBXNativeTarget "Transcopied" */ = { isa = XCConfigurationList; @@ -487,7 +472,7 @@ 1DFE679B2B13168E000E36DA /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ @@ -500,6 +485,14 @@ minimumVersion = 0.1.9; }; }; + 1D8B72D72B9EA6B30028D7D4 /* XCRemoteSwiftPackageReference "CompoundPredicate" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/NoahKamara/CompoundPredicate"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.1.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -508,6 +501,11 @@ package = 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */; productName = SwiftUIX; }; + 1D8B72D82B9EA6B30028D7D4 /* CompoundPredicate */ = { + isa = XCSwiftPackageProductDependency; + package = 1D8B72D72B9EA6B30028D7D4 /* XCRemoteSwiftPackageReference "CompoundPredicate" */; + productName = CompoundPredicate; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 1DFE67812B13168D000E36DA /* Project object */; diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Test.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Test.xcscheme deleted file mode 100644 index 55ddc8e..0000000 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Test.xcscheme +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme index e18053d..3ff9fcf 100644 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme @@ -1,20 +1,11 @@ + version = "1.7"> + buildImplicitDependencies = "YES"> - - - - + isEnabled = "NO"> diff --git a/TranscopiedDebug.entitlements b/TranscopiedDebug.entitlements index d7422db..49cdde9 100644 --- a/TranscopiedDebug.entitlements +++ b/TranscopiedDebug.entitlements @@ -6,7 +6,7 @@ development com.apple.developer.icloud-container-identifiers - iCloud.transcopied + iCloud.transcopied.dev.1 com.apple.developer.icloud-services @@ -15,7 +15,7 @@ com.apple.developer.ubiquity-container-identifiers - iCloud.transcopied + iCloud.transcopied.dev.1 com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) diff --git a/TranscopiedRelease.entitlements b/TranscopiedRelease.entitlements index 159caf4..b0d6503 100644 --- a/TranscopiedRelease.entitlements +++ b/TranscopiedRelease.entitlements @@ -6,7 +6,7 @@ production com.apple.developer.icloud-container-identifiers - iCloud.transcopied + iCloud.transcopied.prod com.apple.developer.icloud-services @@ -15,7 +15,7 @@ com.apple.developer.ubiquity-container-identifiers - iCloud.transcopied + iCloud.transcopied.prod com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index f92476a..954fd25 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -60,9 +60,18 @@ struct CopiedEditorView: View { .onChange(of: editorFocused) { bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar } -// .containerRelativeFrame(.horizontal, alignment: .top) Spacer() } + case "public.image": + VStack { + Image(data: item.data)! + .resizable(true) + .scaledToFit() + .fill(alignment: .center) + .padding() + Spacer() + } + default: Spacer() @@ -168,7 +177,7 @@ struct CopiedEditorView: View { #Preview("URL Clip no Title") { CopiedEditorView( item: CopiedItem( - content: URL(string: "https://www.reddit.com/"), + content: URL(string: "https://www.reddit.com/") as Any, type: PasteboardContentType.url, title: "", timestamp: Date() diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index de82f82..6d8eb04 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -44,38 +44,38 @@ final class CopiedItem { var title: String = "" var type: String = "" var timestamp: Date = Date(timeIntervalSince1970: .zero) - + var content: String = "" @Attribute(.externalStorage) - var content: Data = Data() + var data: Data = Data() @Transient var text: String { - get { return String(data: content, encoding: .utf8) ?? ""} + get { return content } set { - content = Data(newValue.utf8) + content = newValue } } @Transient var url: URL { - get { return URL(string: String(data: content, encoding: .utf8)!)!} + get { return URL(string: content)!} // get { return type == PasteboardContentType.url.rawValue ? URL(string: String(data: content, encoding: .utf8)!) : nil } set { - content = Data(newValue.absoluteString.removingPercentEncoding!.utf8) + content = newValue.absoluteString.removingPercentEncoding! } } @Transient var file: Data? { - get { return type == PasteboardContentType.file.rawValue ? content : nil} - set { content = Data(newValue!)} + get { return type == PasteboardContentType.file.rawValue ? data : nil} + set { data = Data(newValue!)} } @Transient var image: UIImage? { - get {return type == PasteboardContentType.image.rawValue ? UIImage(data: content) : nil} + get {return type == PasteboardContentType.image.rawValue ? UIImage(data: data) : nil} set { - content = Data(newValue!.pngData()!) + data = Data(newValue!.pngData()!) } } @@ -85,25 +85,25 @@ final class CopiedItem { let I = (content as! UIImage) self.uid = hashString(data: I.pngData()!) self.type = PasteboardContentType.image.rawValue - self.content = I.pngData()! + self.data = I.pngData()! self.title = title case .url: let U = (content as! URL) self.uid = hashString(data: Data(U.absoluteString.utf8)) self.type = PasteboardContentType.url.rawValue - self.content = Data(U.absoluteString.utf8) + self.content = U.absoluteString self.title = title.isEmpty ? (U.host() ?? U.formatted(.url)) : title case .text: let S = (content as! String) self.uid = hashString(data: Data(S.utf8)) self.type = PasteboardContentType.text.rawValue - self.content = Data(S.utf8) + self.content = S self.title = title case .file: let D = (content as! Data) self.uid = hashString(data: D) self.type = PasteboardContentType.file.rawValue - self.content = Data(D) + self.data = Data(D) self.title = title } if !self.content.isEmpty { @@ -200,7 +200,7 @@ public extension Binding where Value: Equatable { VStack { Text(item.title) Text(item.type) - Text(String(data: item.content, encoding: .utf8)!) + Text(item.content) Text(item.timestamp.ISO8601Format()) } } diff --git a/transcopied/CopiedItemList/CopiedItemList.swift b/transcopied/CopiedItemList/CopiedItemList.swift index 7132f87..085ef55 100644 --- a/transcopied/CopiedItemList/CopiedItemList.swift +++ b/transcopied/CopiedItemList/CopiedItemList.swift @@ -6,6 +6,7 @@ // import SwiftData import SwiftUI +import CompoundPredicate struct CopiedItemList: View { @@ -54,23 +55,50 @@ struct CopiedItemList: View { } } + static func contentAsStringMatch(content: Data) -> Predicate { +// let dataContent = Data(content.utf8) + let strContent = String(data: content, encoding: .utf8)! + + return #Predicate { + content.isEmpty || ( + $0.content.contains(strContent) || + $0.title.contains(strContent) + ) + } + } + init(searchText: String, searchScope: String) { - let emptySearch = searchText.isEmpty - let anyScope = searchScope == ContentTypeFilter.any.rawValue - let noFilter = emptySearch && anyScope - let filter = #Predicate { item in - return noFilter || - (anyScope || item.type.localizedStandardContains(searchScope)) && - (emptySearch || ( - item.title.contains(searchText) || - item.content.contains(searchText.utf8) - )) + let searchData = searchText.data(using: .utf8) ?? nil + + let emptyScope = #Predicate {item in + return searchScope.isEmpty + } + let matchesScope = #Predicate{item in + item.type.localizedStandardContains(searchScope) } + let emptySearch = #Predicate{item in + searchText.isEmpty + } + let matchingTitle = #Predicate{item in + item.title.contains(searchText) + } +// let matchingContent = #Predicate{item in +// searchData != nil ? item.content.contains(searchData!) : false +// } + let matchingContent = CopiedItemList.contentAsStringMatch(content: searchData!) + + let scopeFilter = [emptyScope, matchesScope].disjunction() + let queryFilter = [emptySearch, [matchingTitle, matchingContent].disjunction()].disjunction() + +// let queryFilter = [matchingContent, matchingTitle].disjunction() + _items = Query( - filter: filter, +// filter: [queryFilter, scopeFilter].conjunction(), + filter: queryFilter, sort: \CopiedItem.timestamp, order: .reverse ) + } private func addItem() { @@ -104,7 +132,7 @@ struct CopiedItemList: View { } } enum ContentTypeFilter: String { - case text = "public.text" + case text = "public.plain-text" case url = "public.url" case image = "public.image" case file = "public.content" @@ -120,13 +148,13 @@ struct CopiedItemListContainer: View { var body: some View { CopiedItemList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) - .searchScopes($searchScope, activation: .onSearchPresentation) { - Text("Text").tag(ContentTypeFilter.text) - Text("URL").tag(ContentTypeFilter.url) - Text("Image").tag(ContentTypeFilter.image) - Text("File").tag(ContentTypeFilter.file) - Text("All").tag(ContentTypeFilter.any) - } +// .searchScopes($searchScope, activation: .onSearchPresentation) { +// Text("Text").tag(ContentTypeFilter.text) +// Text("URL").tag(ContentTypeFilter.url) +// Text("Image").tag(ContentTypeFilter.image) +// Text("File").tag(ContentTypeFilter.file) +// Text("All").tag(ContentTypeFilter.any) +// } } } diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index 10b1d12..f9c2f4d 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -7,6 +7,7 @@ import SwiftUI + struct ConditionalRowText: View { var main: String? var alt: String? = "" @@ -35,41 +36,59 @@ struct CopiedItemRow: View { private var iconName: String private var iconColor: Color + private var bytefmt: ByteCountFormatter = ByteCountFormatter() + var body: some View { HStack(alignment: .center) { - VStack { - HStack { - Image(systemName: self.iconName) - .resizable() - .scaledToFit() - .foregroundStyle(self.iconColor) - Text( - !item.title.isEmpty - ? item.title - : (item.text) - ) - .truncationMode(.tail) - .frame(maxWidth: .infinity, alignment: .leading) - .underline(self.item.type.contains("url")) -// .foregroundStyle(.blue) - } - HStack { - // TODO: make this section differ based on item type - if !item.text.isEmpty { - Image(systemName: "info.circle") + GeometryReader { geometry in + VStack { + HStack { + Image(systemName: self.iconName) + .resizable() + .scaledToFit() + .foregroundStyle(self.iconColor) + Text( + !item.title.isEmpty + ? item.title + : (item.text) + ) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + .underline(self.item.type.contains("url")) + // .foregroundStyle(.blue) + } + HStack { + // TODO: make this section differ based on item type + if !item.text.isEmpty { + Image(systemName: "info.circle") + .symbolRenderingMode(.monochrome) + Text("\(item.text.count) characters") + } + Image(systemName: "clock") .symbolRenderingMode(.monochrome) - Text("\(item.text.count) characters") + Text(relativeDateFmt(item.timestamp)) + } + .dynamicTypeSize(.xSmall) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.footnote) + .foregroundStyle(.tertiary) + + if (self.item.type == "public.image") { + HStack { + Text("Image Data: ") + Text( + self.bytefmt.string(fromByteCount: Int64(item.content.count)) + ) + +// Image(data: self.item.content)! +// .resizable() +// .clipped() +// .maxHeight(.infinity) +// .maxWidth(100) + } } - Image(systemName: "clock") - .symbolRenderingMode(.monochrome) - Text(relativeDateFmt(item.timestamp)) - } - .dynamicTypeSize(.xSmall) - .frame(maxWidth: .infinity, alignment: .leading) - .font(.footnote) - .foregroundStyle(.tertiary) + }.frame(maxHeight: 80) } - .frame(maxHeight: 40) } } diff --git a/transcopied/Migrations/CopiedItemVersions.swift b/transcopied/Migrations/CopiedItemVersions.swift index a673d50..ce17d5e 100644 --- a/transcopied/Migrations/CopiedItemVersions.swift +++ b/transcopied/Migrations/CopiedItemVersions.swift @@ -86,8 +86,9 @@ enum CopiedItemSchemaV2: VersionedSchema { var type: String = "" var timestamp: Date = Date.init(timeIntervalSince1970: .zero) + var content: String = "" @Attribute(.externalStorage) - var content: Data = Data() + var data: Data = Data() @Transient var text: String? @Transient var url: URL? @@ -100,22 +101,80 @@ enum CopiedItemSchemaV2: VersionedSchema { let I = (content as! UIImage) self.uid = hashString(data: I.pngData()!) self.type = PasteboardContentType.image.rawValue - self.content = I.pngData()! + self.data = I.pngData()! case .file: let D = (content as! Data) self.uid = hashString(data: D) self.type = PasteboardContentType.file.rawValue - self.content = Data(D) + self.data = Data(D) case .text: let S = (content as! String) self.uid = hashString(data: Data(S.utf8)) self.type = PasteboardContentType.text.rawValue - self.content = Data(S.utf8) + self.content = S case .url: let U = (content as! URL) self.uid = hashString(data: Data(U.absoluteString.utf8)) self.type = PasteboardContentType.url.rawValue - self.content = Data(U.absoluteString.utf8) + self.content = U.absoluteString + } + if !self.content.isEmpty { + self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) + } + self.title = title + } + } +} + + +enum CopiedItemSchemaV3: VersionedSchema { + static var versionIdentifier: Schema.Version = .init(3, 0, 0) + static var models: [any PersistentModel.Type] { + return [CopiedItem.self] + } + + @Model + final class CopiedItem { + var uid: String = "00000000-0000-0000-0000-000000000000" + var title: String = "" + var type: String = "" + var timestamp: Date = Date.init(timeIntervalSince1970: .zero) + + var content: String = "" + @Attribute(.externalStorage) + var data: Data = Data() + + @Transient var text: String? + @Transient var url: URL? + @Transient var file: Data? + @Transient var image: UIImage? + + init(content: Any, type: PasteboardContentType, title: String = "", timestamp: Date?) { + switch type { + case .image: + let I = (content as! UIImage) + self.uid = hashString(data: I.pngData()!) + self.type = PasteboardContentType.image.rawValue + self.data = I.pngData()! + self.content = "" + case .file: + let D = (content as! Data) + self.uid = hashString(data: D) + self.type = PasteboardContentType.file.rawValue + self.data = Data(D) + self.content = "" + case .text: + let S = (content as! String) + self.uid = hashString(data: Data(S.utf8)) + self.type = PasteboardContentType.text.rawValue + self.content = S + self.data = Data() + case .url: + let U = (content as! URL) + self.uid = hashString(data: Data(U.absoluteString.utf8)) + self.type = PasteboardContentType.url.rawValue + self.content = U.absoluteString + self.data = Data() } if !self.content.isEmpty { self.timestamp = timestamp ?? Date(timeIntervalSinceNow: TimeInterval(0)) @@ -128,12 +187,13 @@ enum CopiedItemSchemaV2: VersionedSchema { enum CopiedItemsMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { - [CopiedItemSchemaV1.self, CopiedItemSchemaV2.self] + [CopiedItemSchemaV1.self, CopiedItemSchemaV2.self, CopiedItemSchemaV3.self] } static var stages: [MigrationStage] { [ - V1__V2 +// V1__V2, +// V2__V3 ] } @@ -154,4 +214,20 @@ enum CopiedItemsMigrationPlan: SchemaMigrationPlan { ) static let V1__V2 = MigrationStage.lightweight(fromVersion: CopiedItemSchemaV1.self, toVersion: CopiedItemSchemaV2.self) + static let V2__V3 = MigrationStage.lightweight(fromVersion: CopiedItemSchemaV2.self, toVersion: CopiedItemSchemaV3.self) +// static let V2__v3 = MigrationStage.custom( +// fromVersion: CopiedItemSchemaV2.self, +// toVersion: CopiedItemSchemaV3.self, +// willMigrate: { context in +// +// }, +// didMigrate: { context in +// let copieditems = try context.fetch(FetchDescriptor()) +// for item in copieditems { +// if item.content != nil { +// item.dummyColumn = Data(item.content!.utf8) +// } +// } +// } +// ) } diff --git a/transcopied/PBManager.swift b/transcopied/PBManager.swift index 025638d..c11d663 100644 --- a/transcopied/PBManager.swift +++ b/transcopied/PBManager.swift @@ -77,7 +77,7 @@ class PBManager { return url } if let image = board.image { - return image.pngData() + return image } if let string = board.string { if string.isURL() { diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index e53d707..66118db 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -20,12 +20,13 @@ struct Transcopied: App { schema: schema, isStoredInMemoryOnly: false, allowsSave: true, - cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("iCloud.Transcopied") + cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("transcopied.dev.1") ) do { return try ModelContainer( for: schema, - migrationPlan: CopiedItemsMigrationPlan.self, +// migrationPlan: .none, +// migrationPlan: CopiedItemsMigrationPlan.self, configurations: [modelConfiguration] ) } From 0e42614005f236acf4ec9c2e325ee38fe661f9ae Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Mon, 18 Mar 2024 23:28:45 -0500 Subject: [PATCH 12/19] got search fully working --- Transcopied.xcodeproj/project.pbxproj | 6 +- .../xcschemes/Transcopied.xcscheme | 2 +- transcopied/CopiedItem.swift | 38 ++++---- .../CopiedItemList/CopiedItemList.swift | 67 +++++++------- .../CopiedItemList/CopiedItemListRow.swift | 87 +++++++------------ transcopied/Transcopied.swift | 12 ++- 6 files changed, 93 insertions(+), 119 deletions(-) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index fbacb93..9b1d333 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -168,9 +168,9 @@ 1DFE67812B13168D000E36DA /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = NO; + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1520; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 1520; TargetAttributes = { 1DFE67882B13168D000E36DA = { CreatedOnToolsVersion = 15.0; @@ -401,7 +401,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = ""; + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.slyboots.transcopied; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme index 3ff9fcf..310e319 100644 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme @@ -1,6 +1,6 @@ (isNotNil source: Binding, defaultValue: T) where Value == Data { -// self.init -// } } public extension Binding where Value: Equatable { @@ -188,21 +184,25 @@ public extension Binding where Value: Equatable { ) } } - #Preview { Group { - @Bindable var item: CopiedItem = CopiedItem( - content: "Test Content", - type: .text, - title: "Test Title", - timestamp: Date(timeIntervalSinceNow: 0) - ) - VStack { - Text(item.title) - Text(item.type) - Text(item.content) - Text(item.timestamp.ISO8601Format()) - } + Text("Test") } - .modelContainer(for: CopiedItem.self, inMemory: true, isAutosaveEnabled: true) } +//#Preview { +// Group { +// @Bindable var item: CopiedItem = CopiedItem( +// content: "Test Content", +// type: .text, +// title: "Test Title", +// timestamp: Date(timeIntervalSinceNow: 0) +// ) +// VStack { +// Text(item.title) +// Text(item.type) +// Text(item.content) +// Text(item.timestamp.ISO8601Format()) +// } +// } +// .modelContainer(for: CopiedItem.self, inMemory: true, isAutosaveEnabled: true) +//} diff --git a/transcopied/CopiedItemList/CopiedItemList.swift b/transcopied/CopiedItemList/CopiedItemList.swift index 085ef55..e6a91af 100644 --- a/transcopied/CopiedItemList/CopiedItemList.swift +++ b/transcopied/CopiedItemList/CopiedItemList.swift @@ -4,10 +4,9 @@ // // Created by Dakota Lorance on 11/26/23. // +import CompoundPredicate import SwiftData import SwiftUI -import CompoundPredicate - struct CopiedItemList: View { @Environment(\.modelContext) private var modelContext @@ -26,7 +25,7 @@ struct CopiedItemList: View { } .onDelete(perform: deleteItems) } - .onAppear(perform: {self.addItem()}) + .onAppear(perform: { addItem() }) .navigationTitle("Clippings") .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -55,59 +54,54 @@ struct CopiedItemList: View { } } - static func contentAsStringMatch(content: Data) -> Predicate { + static func contentAsStringMatch(content: Data) -> Predicate { // let dataContent = Data(content.utf8) let strContent = String(data: content, encoding: .utf8)! - + return #Predicate { content.isEmpty || ( $0.content.contains(strContent) || - $0.title.contains(strContent) + $0.title.contains(strContent) ) } } - - init(searchText: String, searchScope: String) { - let searchData = searchText.data(using: .utf8) ?? nil - let emptyScope = #Predicate {item in + init(searchText: String, searchScope: String) { + let emptyScope = #Predicate { _ in return searchScope.isEmpty } - let matchesScope = #Predicate{item in + let matchesScope = #Predicate { item in item.type.localizedStandardContains(searchScope) } - let emptySearch = #Predicate{item in + let emptySearch = #Predicate { _ in searchText.isEmpty } - let matchingTitle = #Predicate{item in - item.title.contains(searchText) + let matchingTitle = #Predicate { item in + item.title.localizedStandardContains(searchText) + } + let matchingContent = #Predicate { item in + item.content.localizedStandardContains(searchText) } -// let matchingContent = #Predicate{item in -// searchData != nil ? item.content.contains(searchData!) : false -// } - let matchingContent = CopiedItemList.contentAsStringMatch(content: searchData!) let scopeFilter = [emptyScope, matchesScope].disjunction() let queryFilter = [emptySearch, [matchingTitle, matchingContent].disjunction()].disjunction() -// let queryFilter = [matchingContent, matchingTitle].disjunction() - _items = Query( -// filter: [queryFilter, scopeFilter].conjunction(), - filter: queryFilter, + filter: [queryFilter, scopeFilter].conjunction(), sort: \CopiedItem.timestamp, order: .reverse ) - } private func addItem() { withAnimation { - let content = self.pbm.get() - guard !(content == nil) else { return } + let content = pbm.get() + guard !(content == nil) else { + return + } + + let pbtype = pbm.uti - let pbtype = self.pbm.uti - let newItem = CopiedItem( content: content!, type: PasteboardContentType[pbtype!]!, @@ -115,9 +109,9 @@ struct CopiedItemList: View { timestamp: Date() ) do { - try newItem.save(context: modelContext) - } catch _ { + } + catch _ { return } } @@ -131,6 +125,7 @@ struct CopiedItemList: View { } } } + enum ContentTypeFilter: String { case text = "public.plain-text" case url = "public.url" @@ -148,13 +143,13 @@ struct CopiedItemListContainer: View { var body: some View { CopiedItemList(searchText: searchText, searchScope: searchScope.rawValue) .searchable(text: $searchText) -// .searchScopes($searchScope, activation: .onSearchPresentation) { -// Text("Text").tag(ContentTypeFilter.text) -// Text("URL").tag(ContentTypeFilter.url) -// Text("Image").tag(ContentTypeFilter.image) -// Text("File").tag(ContentTypeFilter.file) -// Text("All").tag(ContentTypeFilter.any) -// } + .searchScopes($searchScope, activation: .onSearchPresentation) { + Text("Text").tag(ContentTypeFilter.text) + Text("URL").tag(ContentTypeFilter.url) + Text("Image").tag(ContentTypeFilter.image) + Text("File").tag(ContentTypeFilter.file) + Text("All").tag(ContentTypeFilter.any) + } } } diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index f9c2f4d..b5574fc 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -151,61 +151,32 @@ struct CopiedItemRow: View { } -#Preview("Url Row", traits: .sizeThatFitsLayout) { - Group { - @State var exampleData: [CopiedItem] = [ - CopiedItem( - content: URL(string: "https://google.com")!.absoluteURL, - type: .url, - timestamp: Date.init(timeIntervalSinceNow: -10000) - ), - CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!.absoluteURL, type: .url, timestamp: nil), - CopiedItem( - content: URL(string: "https://areally.long.url/?q=123idhwiue")!, - type: .url, - title: "URL with a title", - timestamp: Date(timeIntervalSince1970: .zero) - ), - ] - List(exampleData) { item in - @Bindable var item = item - - CopiedItemRow( - item: item - ) - } - .frame(height: 500, alignment: .center) - .listStyle(.automatic) - .modelContainer(for: CopiedItem.self, inMemory: true) - } -} - -#Preview("Url Preview Row", traits: .sizeThatFitsLayout) { - Group { - @State var exampleData: [CopiedItem] = [ - CopiedItem( - content: URL(string: "https://google.com")!, - type: .url, - timestamp: Date.init(timeIntervalSinceNow: -10000) - ), - CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!, type: .url, timestamp: nil), - CopiedItem( - content: URL(string: "https://areally.long.url/?q=123idhwiue")!, - type: .url, - title: "URL with a title", - timestamp: Date(timeIntervalSince1970: .zero) - ), - ] - List(exampleData) { item in - @Bindable var item = item - - CopiedItemRow( - item: item - ) - } - .frame(height: 500, alignment: .center) - .listStyle(.automatic) - .modelContainer(for: CopiedItem.self, inMemory: true) - } -} - +//#Preview("Url Preview Row", traits: .sizeThatFitsLayout) { +// Group { +// @State var exampleData: [CopiedItem] = [ +// CopiedItem( +// content: URL(string: "https://google.com")!, +// type: .url, +// timestamp: Date.init(timeIntervalSinceNow: -10000) +// ), +// CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!, type: .url, timestamp: nil), +// CopiedItem( +// content: URL(string: "https://areally.long.url/?q=123idhwiue")!, +// type: .url, +// title: "URL with a title", +// timestamp: Date(timeIntervalSince1970: .zero) +// ), +// ] +// List(exampleData) { item in +// @Bindable var item = item +// +// CopiedItemRow( +// item: item +// ) +// } +// .frame(height: 500, alignment: .center) +// .listStyle(.automatic) +// .modelContainer(for: CopiedItem.self, inMemory: true) +// } +//} +// diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index 66118db..b002860 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -16,16 +16,24 @@ struct Transcopied: App { let schema = Schema([ CopiedItem.self, ]) +#if DEBUG let modelConfiguration = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, allowsSave: true, - cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("transcopied.dev.1") + cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("iCloud.transcopied.dev.1") ) +#else + let modelConfiguration = ModelConfiguration( + schema: schema, + isStoredInMemoryOnly: false, + allowsSave: true, + cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("iCloud.transcopied.prod") + ) +#endif do { return try ModelContainer( for: schema, -// migrationPlan: .none, // migrationPlan: CopiedItemsMigrationPlan.self, configurations: [modelConfiguration] ) From f42d21fae56211c928219b622e81215078f6dbed Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 00:48:10 -0500 Subject: [PATCH 13/19] switched to swiftformat --- .gitignore | 2 + .swiftformat | 12 + .swiftlint.yml | 266 ------------------ Transcopied.xcodeproj/project.pbxproj | 22 ++ .../xcschemes/Transcopied.xcscheme | 88 ------ 5 files changed, 36 insertions(+), 354 deletions(-) create mode 100644 .swiftformat delete mode 100644 .swiftlint.yml delete mode 100644 Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme diff --git a/.gitignore b/.gitignore index 1c4a7ad..5e0c449 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ ## User settings xcuserdata/ +BuildTools/.swiftpm +BuildTools/.build ## Obj-C/Swift specific *.hmap diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..ec88f94 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,12 @@ +--binarygrouping none +--decimalgrouping none +--elseposition next-line +--hexgrouping none +--ifdef outdent +--indentcase true +--octalgrouping none +--self init-only +--semicolons never +--stripunusedargs closure-only +--wraparguments before-first +--wrapcollections before-first diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index 58a7f12..0000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,266 +0,0 @@ -disabled_rules: -- comment_spacing - -# If true, SwiftLint will not fail if no lintable files are found. -allow_zero_lintable_files: true -# If true, SwiftLint will treat all warnings as errors. -strict: false -reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, codeclimate, junit, html, emoji, sonarqube, markdown, github-actions-logging, summary) -blanket_disable_command: - severity: warning - allowed_rules: ["file_header", "file_length", "file_name", "file_name_no_space", "single_test_class"] - always_blanket_disable: [] -block_based_kvo: - severity: warning -class_delegate_protocol: - severity: warning -closing_brace: - severity: warning -closure_parameter_position: - severity: warning -colon: - severity: warning - flexible_right_spacing: false - apply_to_dictionaries: true -comma: - severity: warning -# comment_spacing: false - # severity: warning -compiler_protocol_init: - severity: warning -computed_accessors_order: - severity: warning - order: get_set -control_statement: - severity: warning -cyclomatic_complexity: - warning: 10 - error: 20 - ignores_case_statements: false -deployment_target: - severity: warning - iOSApplicationExtension_deployment_target: 7.0 - iOS_deployment_target: 7.0 - macOSApplicationExtension_deployment_target: 10.9 - macOS_deployment_target: 10.9 - tvOSApplicationExtension_deployment_target: 9.0 - tvOS_deployment_target: 9.0 - watchOSApplicationExtension_deployment_target: 1.0 - watchOS_deployment_target: 1.0 -discouraged_direct_init: - severity: warning - types: ["Bundle", "Bundle.init", "Bundle.init.init", "NSError", "NSError.init", "NSError.init.init", "UIDevice", "UIDevice.init", "UIDevice.init.init"] -duplicate_conditions: - severity: error -duplicate_enum_cases: - severity: error -duplicate_imports: - severity: warning -duplicated_key_in_dictionary_literal: - severity: warning -dynamic_inline: - severity: error -empty_enum_arguments: - severity: warning -empty_parameters: - severity: warning -empty_parentheses_with_trailing_closure: - severity: warning -file_length: - warning: 400 - error: 1000 - ignore_comment_only_lines: false -for_where: - severity: warning - allow_for_as_filter: false -force_cast: - severity: warning -force_try: - severity: error -function_body_length: - warning: 50 - error: 100 -function_parameter_count: - warning: 5 - error: 8 - ignores_default_parameters: true -generic_type_name: - min_length: - warning: 1 - error: 0 - max_length: - warning: 20 - error: 1000 - excluded: [] - allowed_symbols: [] - unallowed_symbols_severity: error - validates_start_with_lowercase: error -identifier_name: - min_length: - warning: 3 - error: 2 - max_length: - warning: 40 - error: 60 - excluded: ["^^id$$"] - allowed_symbols: [] - unallowed_symbols_severity: error - validates_start_with_lowercase: error -implicit_getter: - severity: warning -inclusive_language: - severity: warning -invalid_swiftlint_command: - severity: warning -is_disjoint: - severity: warning -large_tuple: - warning: 2 - error: 3 -leading_whitespace: - severity: warning -legacy_cggeometry_functions: - severity: warning -legacy_constant: - severity: warning -legacy_constructor: - severity: warning -legacy_hashing: - severity: warning -legacy_nsgeometry_functions: - severity: warning -legacy_random: - severity: warning -line_length: - warning: 120 - error: 200 - ignores_urls: false - ignores_function_declarations: false - ignores_comments: false - ignores_interpolated_strings: false -mark: - severity: warning -multiple_closures_with_trailing_closure: - severity: warning -nesting: - type_level: - warning: 1 - function_level: - warning: 2 - check_nesting_in_closures_and_statements: true - always_allow_one_type_in_functions: false -no_fallthrough_only: - severity: warning -no_space_in_method_call: - severity: warning -notification_center_detachment: - severity: warning -ns_number_init_as_function_reference: - severity: warning -nsobject_prefer_isequal: - severity: warning -opening_brace: - severity: warning - allow_multiline_func: false -operator_whitespace: - severity: warning -orphaned_doc_comment: - severity: warning -private_over_fileprivate: - severity: warning - validate_extensions: false -private_unit_test: - severity: warning - test_parent_classes: ["QuickSpec", "XCTestCase"] -protocol_property_accessors_order: - severity: warning -reduce_boolean: - severity: warning -redundant_discardable_let: - severity: warning -redundant_objc_attribute: - severity: warning -redundant_optional_initialization: - severity: warning -redundant_set_access_control: - severity: warning -redundant_string_enum_value: - severity: warning -redundant_void_return: - severity: warning -return_arrow_whitespace: - severity: warning -self_in_property_initialization: - severity: warning -shorthand_operator: - severity: error -statement_position: - severity: warning - statement_mode: uncuddled_else -superfluous_disable_command: - severity: warning -switch_case_alignment: - # severity: warning - indented_cases: true -syntactic_sugar: - severity: warning -todo: - severity: warning -trailing_comma: - severity: warning - mandatory_comma: true -trailing_newline: - severity: warning -trailing_semicolon: - severity: warning -trailing_whitespace: - severity: warning - ignores_empty_lines: false - ignores_comments: true -type_body_length: - warning: 250 - error: 350 -type_name: - min_length: - warning: 3 - error: 0 - max_length: - warning: 40 - error: 1000 - excluded: [] - allowed_symbols: [] - unallowed_symbols_severity: error - validates_start_with_lowercase: error - validate_protocols: true -unavailable_condition: - severity: warning -unneeded_break_in_switch: - severity: warning -unneeded_override: - severity: warning -unneeded_synthesized_initializer: - severity: warning -unused_closure_parameter: - severity: warning -unused_control_flow_label: - severity: warning -unused_enumerated: - severity: warning -unused_optional_binding: - severity: warning - ignore_optional_try: false -unused_setter_value: - severity: warning -valid_ibinspectable: - severity: warning -vertical_parameter_alignment: - severity: warning -vertical_whitespace: - severity: warning - max_empty_lines: 2 -void_function_in_ternary: - severity: warning -void_return: - severity: warning -xctfail_message: - severity: warning diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 9b1d333..14f72f2 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -145,6 +145,7 @@ isa = PBXNativeTarget; buildConfigurationList = 1DFE67992B13168E000E36DA /* Build configuration list for PBXNativeTarget "Transcopied" */; buildPhases = ( + 1D5046D52BA954FA00ED86BA /* ShellScript */, 1DFE67852B13168D000E36DA /* Sources */, 1DFE67862B13168D000E36DA /* Frameworks */, 1DFE67872B13168D000E36DA /* Resources */, @@ -211,6 +212,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 1D5046D52BA954FA00ED86BA /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftformat > /dev/null; then\n swiftformat --swiftversion 5.0 --lint --lenient \"${SRCROOT}\"\nelse\n echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 1DFE67852B13168D000E36DA /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme b/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme deleted file mode 100644 index 310e319..0000000 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From c5ab6fe01cc44761aee8edd36e8930ee7ab47bac Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 01:05:22 -0500 Subject: [PATCH 14/19] formatting --- .swiftformat | 2 ++ transcopied/AppDetails.swift | 6 ++-- transcopied/CopiedEditorView.swift | 18 +++++------ transcopied/CopiedItem.swift | 23 ++++++------- .../CopiedItemList/CopiedItemList.swift | 2 +- .../CopiedItemList/CopiedItemListRow.swift | 31 ++++++++---------- .../Migrations/CopiedItemVersions.swift | 32 +++++++++++-------- transcopied/PBManager.swift | 30 ++++++++--------- transcopied/Transcopied.swift | 8 ++--- 9 files changed, 76 insertions(+), 76 deletions(-) diff --git a/.swiftformat b/.swiftformat index ec88f94..b801885 100644 --- a/.swiftformat +++ b/.swiftformat @@ -10,3 +10,5 @@ --stripunusedargs closure-only --wraparguments before-first --wrapcollections before-first + +--disable redundantType diff --git a/transcopied/AppDetails.swift b/transcopied/AppDetails.swift index edca5e5..e2be206 100644 --- a/transcopied/AppDetails.swift +++ b/transcopied/AppDetails.swift @@ -8,9 +8,9 @@ import SwiftUI struct AppDetails: View { - @AppStorage("support") private var support: URL = URL(string: "mailto:transcopied@dwl.dev")! - @AppStorage("project") private var project: URL = URL(string: "https://github.com/slyboots/transcopied")! - @AppStorage("privacy") private var privacy: URL = URL(string: "https://transcopied.dwl.dev/privacy")! + @AppStorage("support") private var support = URL(string: "mailto:transcopied@dwl.dev")! + @AppStorage("project") private var project = URL(string: "https://github.com/slyboots/transcopied")! + @AppStorage("privacy") private var privacy = URL(string: "https://transcopied.dwl.dev/privacy")! var body: some View { List { diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index 954fd25..283f09a 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -7,13 +7,15 @@ import Foundation import SwiftData import SwiftUI -import UniformTypeIdentifiers import SwiftUIX +import UniformTypeIdentifiers struct CopiedEditorView: View { enum EditorFocused { - case title, content + case title + case content } + @Environment(\.presentationMode) var presentationMode: Binding @Environment(\.modelContext) private var modelContext @Bindable var item: CopiedItem @@ -34,8 +36,8 @@ struct CopiedEditorView: View { case "public.plain-text": HStack { Text(item.type) + - Text(" - ") + - Text("\(item.text.count) characters") + Text(" - ") + + Text("\(item.text.count) characters") } .frame(maxWidth: .infinity, alignment: .leading) .foregroundStyle(.secondary) @@ -72,7 +74,6 @@ struct CopiedEditorView: View { Spacer() } - default: Spacer() EmptyView() @@ -80,13 +81,13 @@ struct CopiedEditorView: View { } .defaultFocus($editorFocused, EditorFocused.title) .onAppear(perform: { - let _t = (item.title ) + let _t = (item.title) let _c = (item.text) - if (!_c.isEmpty) { + if !_c.isEmpty { editorFocused = .content } - else if (!_t.isEmpty && _c.isEmpty) { + else if !_t.isEmpty, _c.isEmpty { editorFocused = .title } else { @@ -186,7 +187,6 @@ struct CopiedEditorView: View { .modelContainer(for: CopiedItem.self, inMemory: true) } - #Preview("Image Clip with Title") { CopiedEditorView( item: CopiedItem( diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index 52e3110..00d2f3a 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -43,10 +43,10 @@ final class CopiedItem { var uid: String = "00000000-0000-0000-0000-000000000000" var title: String = "" var type: String = "" - var timestamp: Date = Date(timeIntervalSince1970: .zero) + var timestamp = Date(timeIntervalSince1970: .zero) var content: String = "" @Attribute(.externalStorage) - var data: Data = Data() + var data = Data() @Transient var text: String { @@ -58,7 +58,7 @@ final class CopiedItem { @Transient var url: URL { - get { return URL(string: content)!} + get { return URL(string: content)! } // get { return type == PasteboardContentType.url.rawValue ? URL(string: String(data: content, encoding: .utf8)!) : nil } set { content = newValue.absoluteString.removingPercentEncoding! @@ -67,13 +67,13 @@ final class CopiedItem { @Transient var file: Data? { - get { return type == PasteboardContentType.file.rawValue ? data : nil} - set { data = Data(newValue!)} + get { return type == PasteboardContentType.file.rawValue ? data : nil } + set { data = Data(newValue!) } } @Transient var image: UIImage? { - get {return type == PasteboardContentType.image.rawValue ? UIImage(data: data) : nil} + get { return type == PasteboardContentType.image.rawValue ? UIImage(data: data) : nil } set { data = Data(newValue!.pngData()!) } @@ -106,7 +106,7 @@ final class CopiedItem { self.data = Data(D) self.title = title } - if !self.content.isEmpty || !self.data.isEmpty { + if !self.content.isEmpty || !data.isEmpty { self.timestamp = timestamp ?? Date(timeIntervalSinceNow: 0) } } @@ -142,13 +142,12 @@ public extension Binding where Value == URL { var stringBinding: Binding { .init(get: { self.wrappedValue.absoluteString.removingPercentEncoding! - }, set: {newValue in + }, set: { newValue in self.wrappedValue = URL(string: newValue)! }) } } - public extension Binding { init(_ source: Binding, _ defaultValue: Value) { self.init(get: { @@ -184,12 +183,14 @@ public extension Binding where Value: Equatable { ) } } + #Preview { Group { Text("Test") } } -//#Preview { + +// #Preview { // Group { // @Bindable var item: CopiedItem = CopiedItem( // content: "Test Content", @@ -205,4 +206,4 @@ public extension Binding where Value: Equatable { // } // } // .modelContainer(for: CopiedItem.self, inMemory: true, isAutosaveEnabled: true) -//} +// } diff --git a/transcopied/CopiedItemList/CopiedItemList.swift b/transcopied/CopiedItemList/CopiedItemList.swift index e6a91af..946bfc3 100644 --- a/transcopied/CopiedItemList/CopiedItemList.swift +++ b/transcopied/CopiedItemList/CopiedItemList.swift @@ -68,7 +68,7 @@ struct CopiedItemList: View { init(searchText: String, searchScope: String) { let emptyScope = #Predicate { _ in - return searchScope.isEmpty + searchScope.isEmpty } let matchesScope = #Predicate { item in item.type.localizedStandardContains(searchScope) diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index b5574fc..9a2a071 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -7,7 +7,6 @@ import SwiftUI - struct ConditionalRowText: View { var main: String? var alt: String? = "" @@ -36,25 +35,25 @@ struct CopiedItemRow: View { private var iconName: String private var iconColor: Color - private var bytefmt: ByteCountFormatter = ByteCountFormatter() + private var bytefmt = ByteCountFormatter() var body: some View { HStack(alignment: .center) { - GeometryReader { geometry in + GeometryReader { _ in VStack { HStack { - Image(systemName: self.iconName) + Image(systemName: iconName) .resizable() .scaledToFit() - .foregroundStyle(self.iconColor) + .foregroundStyle(iconColor) Text( !item.title.isEmpty - ? item.title - : (item.text) + ? item.title + : (item.text) ) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) - .underline(self.item.type.contains("url")) + .underline(item.type.contains("url")) // .foregroundStyle(.blue) } HStack { @@ -72,14 +71,14 @@ struct CopiedItemRow: View { .frame(maxWidth: .infinity, alignment: .leading) .font(.footnote) .foregroundStyle(.tertiary) - - if (self.item.type == "public.image") { + + if item.type == "public.image" { HStack { Text("Image Data: ") Text( - self.bytefmt.string(fromByteCount: Int64(item.content.count)) + bytefmt.string(fromByteCount: Int64(item.content.count)) ) - + // Image(data: self.item.content)! // .resizable() // .clipped() @@ -92,7 +91,6 @@ struct CopiedItemRow: View { } } - init(item: CopiedItem) { self.item = item switch item.type { @@ -150,8 +148,7 @@ struct CopiedItemRow: View { } } - -//#Preview("Url Preview Row", traits: .sizeThatFitsLayout) { +// #Preview("Url Preview Row", traits: .sizeThatFitsLayout) { // Group { // @State var exampleData: [CopiedItem] = [ // CopiedItem( @@ -169,7 +166,7 @@ struct CopiedItemRow: View { // ] // List(exampleData) { item in // @Bindable var item = item -// +// // CopiedItemRow( // item: item // ) @@ -178,5 +175,5 @@ struct CopiedItemRow: View { // .listStyle(.automatic) // .modelContainer(for: CopiedItem.self, inMemory: true) // } -//} +// } // diff --git a/transcopied/Migrations/CopiedItemVersions.swift b/transcopied/Migrations/CopiedItemVersions.swift index ce17d5e..050f91d 100644 --- a/transcopied/Migrations/CopiedItemVersions.swift +++ b/transcopied/Migrations/CopiedItemVersions.swift @@ -5,16 +5,16 @@ // Created by Dakota Lorance on 3/3/24. // +import CoreData import Foundation import SwiftData -import CoreData import UIKit enum CopiedItemSchemaV1: VersionedSchema { // initial structure static var versionIdentifier: Schema.Version = .init(1, 0, 0) static var models: [any PersistentModel.Type] { - return [CopiedItem.self] + [CopiedItem.self] } enum CopiedItemType: String, Codable { @@ -45,7 +45,7 @@ enum CopiedItemSchemaV1_5: VersionedSchema { // over to Data content columns static var versionIdentifier: Schema.Version = .init(1, 5, 0) static var models: [any PersistentModel.Type] { - return [CopiedItem.self] + [CopiedItem.self] } enum CopiedItemType: String, Codable { @@ -76,7 +76,7 @@ enum CopiedItemSchemaV1_5: VersionedSchema { enum CopiedItemSchemaV2: VersionedSchema { static var versionIdentifier: Schema.Version = .init(2, 0, 0) static var models: [any PersistentModel.Type] { - return [CopiedItem.self] + [CopiedItem.self] } @Model @@ -84,7 +84,7 @@ enum CopiedItemSchemaV2: VersionedSchema { var uid: String = "00000000-0000-0000-0000-000000000000" var title: String = "" var type: String = "" - var timestamp: Date = Date.init(timeIntervalSince1970: .zero) + var timestamp: Date = Date(timeIntervalSince1970: .zero) var content: String = "" @Attribute(.externalStorage) @@ -126,11 +126,10 @@ enum CopiedItemSchemaV2: VersionedSchema { } } - enum CopiedItemSchemaV3: VersionedSchema { static var versionIdentifier: Schema.Version = .init(3, 0, 0) static var models: [any PersistentModel.Type] { - return [CopiedItem.self] + [CopiedItem.self] } @Model @@ -138,7 +137,7 @@ enum CopiedItemSchemaV3: VersionedSchema { var uid: String = "00000000-0000-0000-0000-000000000000" var title: String = "" var type: String = "" - var timestamp: Date = Date.init(timeIntervalSince1970: .zero) + var timestamp: Date = Date(timeIntervalSince1970: .zero) var content: String = "" @Attribute(.externalStorage) @@ -184,7 +183,6 @@ enum CopiedItemSchemaV3: VersionedSchema { } } - enum CopiedItemsMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [CopiedItemSchemaV1.self, CopiedItemSchemaV2.self, CopiedItemSchemaV3.self] @@ -192,7 +190,7 @@ enum CopiedItemsMigrationPlan: SchemaMigrationPlan { static var stages: [MigrationStage] { [ -// V1__V2, + // V1__V2, // V2__V3 ] } @@ -200,7 +198,7 @@ enum CopiedItemsMigrationPlan: SchemaMigrationPlan { static let V1__V1_5 = MigrationStage.custom( fromVersion: CopiedItemSchemaV1.self, toVersion: CopiedItemSchemaV2.self, - willMigrate: { context in + willMigrate: { _ in }, didMigrate: { context in @@ -213,13 +211,19 @@ enum CopiedItemsMigrationPlan: SchemaMigrationPlan { } ) - static let V1__V2 = MigrationStage.lightweight(fromVersion: CopiedItemSchemaV1.self, toVersion: CopiedItemSchemaV2.self) - static let V2__V3 = MigrationStage.lightweight(fromVersion: CopiedItemSchemaV2.self, toVersion: CopiedItemSchemaV3.self) + static let V1__V2 = MigrationStage.lightweight( + fromVersion: CopiedItemSchemaV1.self, + toVersion: CopiedItemSchemaV2.self + ) + static let V2__V3 = MigrationStage.lightweight( + fromVersion: CopiedItemSchemaV2.self, + toVersion: CopiedItemSchemaV3.self + ) // static let V2__v3 = MigrationStage.custom( // fromVersion: CopiedItemSchemaV2.self, // toVersion: CopiedItemSchemaV3.self, // willMigrate: { context in -// +// // }, // didMigrate: { context in // let copieditems = try context.fetch(FetchDescriptor()) diff --git a/transcopied/PBManager.swift b/transcopied/PBManager.swift index c11d663..47416fd 100644 --- a/transcopied/PBManager.swift +++ b/transcopied/PBManager.swift @@ -7,10 +7,10 @@ import Combine import Foundation -import SwiftUI -import UniformTypeIdentifiers import LinkPresentation +import SwiftUI import SwiftUIX +import UniformTypeIdentifiers public enum PasteType: String, CaseIterable { case image = "public.image" @@ -18,9 +18,7 @@ public enum PasteType: String, CaseIterable { case text = "public.plain-text" case file = "public.content" static subscript(index: String) -> PasteType? { - get { - return PasteType(rawValue: index) ?? PasteType.allCases.first(where: {"\($0)" == index })! - } + PasteType(rawValue: index) ?? PasteType.allCases.first(where: { index == "\($0)" })! } } @@ -57,13 +55,13 @@ class PBManager { func hashed(data: Any, type: PasteType) -> Int { switch type { case .image: - return ((data as? Data)?.base64EncodedString().hashValue)! + ((data as? Data)?.base64EncodedString().hashValue)! case .url: - return ((data as? URL)?.absoluteString.hashValue)! + ((data as? URL)?.absoluteString.hashValue)! case .text: - return (data as? String)!.hashValue + (data as? String)!.hashValue default: - return (data as? Data)!.hashValue + (data as? Data)!.hashValue } } @@ -101,14 +99,13 @@ public extension String { return !(url.scheme == nil || url.host() == nil) } } + struct TView: UIViewRepresentable { - func updateUIView(_ uiView: LPLinkView, context: Context) { - return - } - + func updateUIView(_ uiView: LPLinkView, context: Context) {} + func makeUIView(context: Context) -> LPLinkView { let uiView = LPLinkView(url: URL(string: "https://www.google.com/")!) - + return uiView } } @@ -121,7 +118,7 @@ struct TView: UIViewRepresentable { VStack { Text("https://www.google.com/") .frame(width: .infinity, alignment: .leading) - LinkPresentationView(url:URL(string: "https://www.facebook.com/")!) + LinkPresentationView(url: URL(string: "https://www.facebook.com/")!) .frame(width: 100, alignment: .leading) } .frame(height: 200) @@ -161,7 +158,6 @@ private struct ClipboardHasContentModifier: ViewModifier { } public extension View { - func onPasteboardContent(perform action: @escaping () -> Void) -> some View { modifier(ClipboardHasContentModifier(action: action)) } @@ -177,7 +173,7 @@ extension UIPasteboard { } var hasContentPublisher: AnyPublisher { - return Just(hasContent) + Just(hasContent) .merge( with: NotificationCenter.default .publisher(for: UIPasteboard.changedNotification, object: self) diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index b002860..ef350eb 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -1,5 +1,5 @@ // -// TranscopiedApp.swift +// Transcopied.swift // Transcopied // // Created by Dakota Lorance on 11/26/23. @@ -10,7 +10,7 @@ import SwiftUI @main struct Transcopied: App { - @State private var pbm: PBManager = PBManager() + @State private var pbm = PBManager() var sharedModelContainer: ModelContainer = { let schema = Schema([ @@ -52,9 +52,9 @@ struct Transcopied: App { .onSceneActivate { // whenever the list view is shown // if we have new stuff in clip - if self.pbm.canCopy { + if pbm.canCopy { // then save the data from the clipboard for use later - self.pbm.incomingBuffer = self.pbm.get() + pbm.incomingBuffer = pbm.get() } } } From a4b007e48a267eb8f228bdddf180411acedec072 Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 03:37:42 -0500 Subject: [PATCH 15/19] fix editor view copying --- transcopied/CopiedEditorView.swift | 36 ++++++++++++++++++------------ transcopied/PBManager.swift | 30 +++++++++++++++++++++++-- transcopied/Transcopied.swift | 8 +++++++ 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index 283f09a..b405586 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -18,6 +18,7 @@ struct CopiedEditorView: View { @Environment(\.presentationMode) var presentationMode: Binding @Environment(\.modelContext) private var modelContext + @Environment(PBManager.self) private var pbm @Bindable var item: CopiedItem @FocusState private var editorFocused: EditorFocused? @@ -54,23 +55,19 @@ struct CopiedEditorView: View { case "public.url": VStack { LinkPresentationView(url: item.url) - .maxHeight(50) - TextEditor(text: $item.url.stringBinding) - .padding() + .maxHeight(75) + TextEditor(text: $item.content) .foregroundStyle(.primary) .focused($editorFocused, equals: .content) .onChange(of: editorFocused) { bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar } - Spacer() } case "public.image": VStack { Image(data: item.data)! - .resizable(true) + .resizable() .scaledToFit() - .fill(alignment: .center) - .padding() Spacer() } @@ -147,7 +144,13 @@ struct CopiedEditorView: View { } private func setClipboard() { - UIPasteboard.general.setValue(item.content, forPasteboardType: UTType.plainText.identifier) + let binaryTypes = [PasteboardContentType.image.rawValue, PasteboardContentType.file.rawValue] + if binaryTypes.contains([item.type]) { + pbm.set(item.data, type: item.type) + } + else { + pbm.set(item.content, type: item.type) + } } } @@ -160,6 +163,7 @@ struct CopiedEditorView: View { timestamp: Date() ) ) + .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) } @@ -172,6 +176,7 @@ struct CopiedEditorView: View { timestamp: Date() ) ) + .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) } @@ -184,29 +189,32 @@ struct CopiedEditorView: View { timestamp: Date() ) ) + .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) } #Preview("Image Clip with Title") { CopiedEditorView( item: CopiedItem( - content: "Testing 123", - type: PasteboardContentType.text, - title: "Preview Content", + content: UIImage(systemName: "info.circle")!, + type: PasteboardContentType.image, + title: "Title", timestamp: Date() ) ) + .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) } #Preview("Image Clip no Title") { CopiedEditorView( item: CopiedItem( - content: "Testing 123", - type: PasteboardContentType.text, - title: "Preview Content", + content: UIImage(systemName: "clock")!, + type: PasteboardContentType.image, + title: "", timestamp: Date() ) ) + .pasteboardContext() .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/PBManager.swift b/transcopied/PBManager.swift index 47416fd..e9ecfb2 100644 --- a/transcopied/PBManager.swift +++ b/transcopied/PBManager.swift @@ -86,8 +86,34 @@ class PBManager { return board.value(forPasteboardType: "public.content") } - func set(data: [String: Any]) { - board.setItems([data]) + func set(_ data: Any, type: String) { + switch type { + case PasteboardContentType.text.rawValue: + board.setValue(data, forPasteboardType: type) + case PasteboardContentType.url.rawValue: + board.setValue(data, forPasteboardType: type) + case PasteboardContentType.image.rawValue: + board.setValue(data, forPasteboardType: type) + case PasteboardContentType.file.rawValue: + board.setValue(data, forPasteboardType: type) + default: + board.setValue(data, forPasteboardType: UIPasteboard.typeAutomatic) + } + } + + func set(_ data: CopiedItem) { + switch data.type { + case PasteboardContentType.text.rawValue: + board.setValue(data.content, forPasteboardType: data.type) + case PasteboardContentType.url.rawValue: + board.setValue(data.content, forPasteboardType: data.type) + case PasteboardContentType.image.rawValue: + board.setValue(data.data, forPasteboardType: data.type) + case PasteboardContentType.file.rawValue: + board.setValue(data.data, forPasteboardType: data.type) + default: + board.setValue(data.content.isEmpty ? data.data : data.content, forPasteboardType: UIPasteboard.typeAutomatic) + } } } diff --git a/transcopied/Transcopied.swift b/transcopied/Transcopied.swift index ef350eb..04fd5f6 100644 --- a/transcopied/Transcopied.swift +++ b/transcopied/Transcopied.swift @@ -49,6 +49,14 @@ struct Transcopied: App { CopiedItemListContainer() } .pasteboardContext() + .onPasteboardContent { + // whenever the list view is shown + // if we have new stuff in clip + if pbm.canCopy { + // then save the data from the clipboard for use later + pbm.incomingBuffer = pbm.get() + } + } .onSceneActivate { // whenever the list view is shown // if we have new stuff in clip From b27fed9e15534e61e120e64da8664f993659ecbc Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 03:45:04 -0500 Subject: [PATCH 16/19] image row previews --- .../CopiedItemList/CopiedItemListRow.swift | 94 ++++++++++++------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index 9a2a071..02656b0 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -18,14 +18,14 @@ struct ConditionalRowText: View { func calc(main: String?, alt: String?, fallback: String) -> String { if main == nil, alt == nil { - return fallback + fallback } else if alt != nil { - return main != nil ? String(main!.prefix(100)) : String(alt!.prefix(100)) + main != nil ? String(main!.prefix(100)) : String(alt!.prefix(100)) } else { // main should be safe to use - return String(main!.prefix(100)) + String(main!.prefix(100)) } } } @@ -88,7 +88,7 @@ struct CopiedItemRow: View { } }.frame(maxHeight: 80) } - } + }.padding() } init(item: CopiedItem) { @@ -148,32 +148,60 @@ struct CopiedItemRow: View { } } -// #Preview("Url Preview Row", traits: .sizeThatFitsLayout) { -// Group { -// @State var exampleData: [CopiedItem] = [ -// CopiedItem( -// content: URL(string: "https://google.com")!, -// type: .url, -// timestamp: Date.init(timeIntervalSinceNow: -10000) -// ), -// CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!, type: .url, timestamp: nil), -// CopiedItem( -// content: URL(string: "https://areally.long.url/?q=123idhwiue")!, -// type: .url, -// title: "URL with a title", -// timestamp: Date(timeIntervalSince1970: .zero) -// ), -// ] -// List(exampleData) { item in -// @Bindable var item = item -// -// CopiedItemRow( -// item: item -// ) -// } -// .frame(height: 500, alignment: .center) -// .listStyle(.automatic) -// .modelContainer(for: CopiedItem.self, inMemory: true) -// } -// } -// +#Preview("Url Preview Row", traits: .sizeThatFitsLayout) { + Group { + @State var exampleData: [CopiedItem] = [ + CopiedItem( + content: URL(string: "https://google.com")!, + type: .url, + timestamp: Date(timeIntervalSinceNow: -10000) + ), + CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!, type: .url, timestamp: nil), + CopiedItem( + content: URL(string: "https://areally.long.url/?q=123idhwiue")!, + type: .url, + title: "URL with a title", + timestamp: Date(timeIntervalSince1970: .zero) + ), + ] + List(exampleData) { item in + @Bindable var item = item + + CopiedItemRow( + item: item + ) + } + .frame(height: 500, alignment: .center) + .listStyle(.automatic) + .modelContainer(for: CopiedItem.self, inMemory: true) + } +} + +#Preview("Image Preview Row", traits: .sizeThatFitsLayout) { + Group { + @State var exampleData: [CopiedItem] = [ + CopiedItem( + content: UIImage(systemName: "person.text.rectangle.fill"), + type: .image, + title: "", + timestamp: Date(timeIntervalSince1970: .zero) + ), + CopiedItem( + content: UIImage(systemName: "clock"), + type: .image, + title: "Image with a title", + timestamp: Date(timeIntervalSince1970: .zero) + ), + ] + List(exampleData) { item in + @Bindable var item = item + + CopiedItemRow( + item: item + ) + } + .frame(height: 500, alignment: .center) + .listStyle(.automatic) + .modelContainer(for: CopiedItem.self, inMemory: true) + } +} From 8335704f78f939b6f1f599c6bc87b07d83169fba Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 10:17:21 -0500 Subject: [PATCH 17/19] finished main v2 --- Transcopied.xcodeproj/project.pbxproj | 2 +- .../CopiedItemList/CopiedItemListRow.swift | 92 ++++++++++--------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 14f72f2..af4fe4c 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -229,7 +229,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftformat > /dev/null; then\n swiftformat --swiftversion 5.0 --lint --lenient \"${SRCROOT}\"\nelse\n echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\nfi\n"; + shellScript = "#if [[ \"$(uname -m)\" == arm64 ]]; then\n# export PATH=\"/opt/homebrew/bin:$PATH\"\n#fi\n\n#if which swiftformat > /dev/null; then\n# swiftformat --swiftversion 5.0 --lint --lenient \"${SRCROOT}\"\n#else\n# echo \"warning: SwiftFormat not installed, download from https://github.com/nicklockwood/SwiftFormat\"\n#fi\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index 02656b0..fbee314 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftUIX struct ConditionalRowText: View { var main: String? @@ -38,57 +39,57 @@ struct CopiedItemRow: View { private var bytefmt = ByteCountFormatter() var body: some View { - HStack(alignment: .center) { - GeometryReader { _ in - VStack { - HStack { - Image(systemName: iconName) - .resizable() - .scaledToFit() - .foregroundStyle(iconColor) - Text( - !item.title.isEmpty - ? item.title - : (item.text) - ) + HStack(alignment: .top ) { + Image(systemName: iconName) + .foregroundStyle(Color.accent) + .alignmentGuide(.lastTextBaseline) + VStack(alignment: .leading) { + if !item.title.isEmpty { + Text(item.title) + .lineLimit(1) .truncationMode(.tail) - .frame(maxWidth: .infinity, alignment: .leading) - .underline(item.type.contains("url")) - // .foregroundStyle(.blue) - } - HStack { - // TODO: make this section differ based on item type - if !item.text.isEmpty { + } + if ["public.url", "public.plain-text"].contains([item.type]) { + Text(item.content) + .lineLimit(5) + .truncationMode(.tail) + } + if item.type == "public.image" { + Image(image: item.image!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxHeight:80) + .clipped() + } + HStack() { + if !item.text.isEmpty { + HStack(spacing:0) { Image(systemName: "info.circle") .symbolRenderingMode(.monochrome) Text("\(item.text.count) characters") } - Image(systemName: "clock") - .symbolRenderingMode(.monochrome) - Text(relativeDateFmt(item.timestamp)) } - .dynamicTypeSize(.xSmall) - .frame(maxWidth: .infinity, alignment: .leading) - .font(.footnote) - .foregroundStyle(.tertiary) - - if item.type == "public.image" { + if item.image != nil { HStack { - Text("Image Data: ") - Text( - bytefmt.string(fromByteCount: Int64(item.content.count)) - ) - -// Image(data: self.item.content)! -// .resizable() -// .clipped() -// .maxHeight(.infinity) -// .maxWidth(100) + Text("PNG "+item.image!.size.width.formatted()+"x"+item.image!.size.height.formatted()) } } - }.frame(maxHeight: 80) + HStack(spacing:0) { + Image(systemName: "clock") + .symbolRenderingMode(.monochrome) + Text(relativeDateFmt(item.timestamp)) + } + } + .font(.caption) + .foregroundStyle(.tertiary) + } + if item.type == "public.url" { + Spacer() + LinkPresentationView(url: item.url) + .maxWidth(50) + .clipped() } - }.padding() + } } init(item: CopiedItem) { @@ -134,10 +135,15 @@ struct CopiedItemRow: View { title: "TITLE", timestamp: Date(timeIntervalSince1970: .zero) ), + CopiedItem( + content: "iuweghcdiouwgcoewudgsddddddddddddddddddddddddddddddddddddddsdddddddddddddddddddddddddddddddddddddddddddchoewudchoewudchoecwidhcduwhcouwdhcoudwhcowudhcoh", + type: .text, + title: "", + timestamp: Date() + ), ] List(exampleData) { item in @Bindable var item = item - CopiedItemRow( item: item ) @@ -200,7 +206,7 @@ struct CopiedItemRow: View { item: item ) } - .frame(height: 500, alignment: .center) + .frame(height: 500) .listStyle(.automatic) .modelContainer(for: CopiedItem.self, inMemory: true) } From b3398dfd211dd54db6d274302b26ff31465a617e Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 20:53:08 -0500 Subject: [PATCH 18/19] finalized layout for v2 --- Transcopied.xcodeproj/project.pbxproj | 12 ++++ .../CopiedItemList/CopiedItemListRow.swift | 23 +++++--- transcopied/Utils/Debugging.swift | 58 +++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 transcopied/Utils/Debugging.swift diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index af4fe4c..a4e4b8b 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8346852B1415AB004ACF46 /* CopiedItem.swift */; }; 1D8B72D92B9EA6B30028D7D4 /* CompoundPredicate in Frameworks */ = {isa = PBXBuildFile; productRef = 1D8B72D82B9EA6B30028D7D4 /* CompoundPredicate */; }; 1D95123D2BA1AFA7009AD8B6 /* CopiedItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */; }; + 1DAB74972BAA634D003A591F /* Debugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAB74962BAA634D003A591F /* Debugging.swift */; }; 1DBC765B2B1F5FA6004B1261 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67922B13168E000E36DA /* Assets.xcassets */; }; 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DBC765E2B20BAB2004B1261 /* PBManager.swift */; }; 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DD331C82B19846400708F46 /* ModalMessage.swift */; }; @@ -29,6 +30,7 @@ 1D17E8312B779DEC002FE8E7 /* Activitea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Activitea.swift; sourceTree = ""; }; 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetails.swift; sourceTree = ""; }; 1D8346852B1415AB004ACF46 /* CopiedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopiedItem.swift; sourceTree = ""; }; + 1DAB74962BAA634D003A591F /* Debugging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debugging.swift; sourceTree = ""; }; 1DBC765D2B20040B004B1261 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 1DBC765E2B20BAB2004B1261 /* PBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBManager.swift; sourceTree = ""; }; 1DD331BB2B1828CE00708F46 /* SwiftData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftData.framework; path = System/Library/Frameworks/SwiftData.framework; sourceTree = SDKROOT; }; @@ -78,6 +80,14 @@ path = CopiedItemList; sourceTree = ""; }; + 1DAB74952BAA6316003A591F /* Utils */ = { + isa = PBXGroup; + children = ( + 1DAB74962BAA634D003A591F /* Debugging.swift */, + ); + path = Utils; + sourceTree = ""; + }; 1DD331BA2B1828CE00708F46 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -115,6 +125,7 @@ 1DFE678B2B13168D000E36DA /* transcopied */ = { isa = PBXGroup; children = ( + 1DAB74952BAA6316003A591F /* Utils */, 1D086B952B948394004939CD /* CopiedItemList */, 1D086B912B946169004939CD /* Migrations */, 1DFE678C2B13168D000E36DA /* Transcopied.swift */, @@ -242,6 +253,7 @@ 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, 1DF36DD72B16EDC80037FA6A /* CopiedEditorView.swift in Sources */, 1D8346862B1415AB004ACF46 /* CopiedItem.swift in Sources */, + 1DAB74972BAA634D003A591F /* Debugging.swift in Sources */, 1D086B972B9483F0004939CD /* CopiedItemListRow.swift in Sources */, 1D95123D2BA1AFA7009AD8B6 /* CopiedItemList.swift in Sources */, 1D086B932B9461E8004939CD /* CopiedItemVersions.swift in Sources */, diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index fbee314..8f1181a 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -39,15 +39,19 @@ struct CopiedItemRow: View { private var bytefmt = ByteCountFormatter() var body: some View { - HStack(alignment: .top ) { - Image(systemName: iconName) - .foregroundStyle(Color.accent) - .alignmentGuide(.lastTextBaseline) + HStack(alignment:.top, spacing: 0) { + VStack(){ + Image(systemName: iconName) + .foregroundStyle(Color.accent) + .padding(.vertical, .extraSmall) + } VStack(alignment: .leading) { if !item.title.isEmpty { Text(item.title) .lineLimit(1) .truncationMode(.tail) + .padding(0) + .contentMargins(0) } if ["public.url", "public.plain-text"].contains([item.type]) { Text(item.content) @@ -61,6 +65,7 @@ struct CopiedItemRow: View { .frame(maxHeight:80) .clipped() } + Spacer() HStack() { if !item.text.isEmpty { HStack(spacing:0) { @@ -84,10 +89,14 @@ struct CopiedItemRow: View { .foregroundStyle(.tertiary) } if item.type == "public.url" { + // TODO: Move this to be under the URL with the URL font size set smaller Spacer() - LinkPresentationView(url: item.url) - .maxWidth(50) - .clipped() + VStack { + LinkPresentationView(url: item.url) + .squareFrame() + .maxWidth(50) + .maxHeight(50) + } } } } diff --git a/transcopied/Utils/Debugging.swift b/transcopied/Utils/Debugging.swift new file mode 100644 index 0000000..77a235f --- /dev/null +++ b/transcopied/Utils/Debugging.swift @@ -0,0 +1,58 @@ +// +// Debugging.swift +// Transcopied +// +// Created by Dakota Lorance on 3/19/24. +// + +import SwiftUI + +extension Color { + /// Returns a random RGB Color value + static var random: Color { + get { + return Color( + red: Double.random(in: 0.1..<0.95), + green: Double.random(in: 0.1..<0.95), + blue: Double.random(in: 0.1..<0.95) + ) + } + } +} + +extension View { + func debugModifier(_ modifier: (Self) -> some View) -> some View { +#if DEBUG + return modifier(self) +#else + return self +#endif + } +} + +extension View { + func debugBorder(_ color: Color = Color.random, width: CGFloat = 1, opacity: Double = 1.0) -> some View { + debugModifier { + $0.border(color.opacity(opacity), width: width) +// .border(cornerRadius: 0.0, style: StrokeStyle(lineWidth: width*(dashed ? 1.5:0), dash: [10, 10])) + } + } + func debugBackground(_ color: Color = .random, opacity: Double = 0.7) -> some View { + debugModifier { + $0.background(color.opacity(opacity)) + } + } +} + +#Preview { + Group { + HStack(alignment: .firstTextBaseline) { + Text("Test") + .debugBorder(.black) + .debugBackground(.green) + } + .padding() + .debugBorder(.black) + .debugBackground(.magenta) + } +} From 2141d8e2f6e6b5a88618cf0886cba0530a6d2843 Mon Sep 17 00:00:00 2001 From: slyboots <0@dwl.dev> Date: Tue, 19 Mar 2024 23:04:01 -0500 Subject: [PATCH 19/19] added multiselect / delete to list --- transcopied/CopiedEditorView.swift | 2 +- .../CopiedItemList/CopiedItemList.swift | 112 +++++++++++++++--- .../CopiedItemList/CopiedItemListRow.swift | 5 +- 3 files changed, 100 insertions(+), 19 deletions(-) diff --git a/transcopied/CopiedEditorView.swift b/transcopied/CopiedEditorView.swift index b405586..a123c9b 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -101,7 +101,7 @@ struct CopiedEditorView: View { setClipboard() copiedHapticTriggered.toggle() }, label: { - Label("Copy", systemImage: "square.and.arrow.down.on.square") + Label("Copy", systemImage: "square.and.arrow.up.on.square") .sensoryFeedback(.success, trigger: copiedHapticTriggered) }) Spacer() diff --git a/transcopied/CopiedItemList/CopiedItemList.swift b/transcopied/CopiedItemList/CopiedItemList.swift index 946bfc3..d2bde38 100644 --- a/transcopied/CopiedItemList/CopiedItemList.swift +++ b/transcopied/CopiedItemList/CopiedItemList.swift @@ -11,10 +11,12 @@ import SwiftUI struct CopiedItemList: View { @Environment(\.modelContext) private var modelContext @Environment(PBManager.self) private var pbm + @Environment(\.editMode) private var editMode @Query private var items: [CopiedItem] + @State private var selection = Set() var body: some View { - List { + List(selection: $selection) { ForEach(items) { item in NavigationLink { CopiedEditorView(item: item) @@ -22,24 +24,40 @@ struct CopiedItemList: View { CopiedItemRow(item: item) } .foregroundStyle(.primary) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + deleteItem(item) + } label: { + Label("Delete", systemImage: "trash") + } + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + Button() { + pbm.set(item) + } label: { + Label("Copy", systemImage: "arrow.up.doc.on.clipboard") + .tint(.green) + } + } } - .onDelete(perform: deleteItems) } .onAppear(perform: { addItem() }) .navigationTitle("Clippings") .toolbar { - ToolbarItem(placement: .topBarTrailing) { - EditButton().padding(.trailing) - } - ToolbarItem { - Button(action: addItem) { - Label("Add Clipping", systemImage: "plus") + ToolbarItemGroup(placement: .topBarTrailing) { + if editMode?.wrappedValue.isEditing == true { + Button(role: .destructive, action: deleteSelection) { + Label("Delete Clippings", systemImage: "trash") + } + .tint(Color.red) } + EditButton().padding(.trailing) } } + .animation(nil, value: editMode?.wrappedValue) .toolbar { ToolbarItemGroup(placement: .bottomBar) { - Button("Paste", systemImage: "square.and.arrow.down", action: addItem) + Button("Paste", systemImage: "square.and.arrow.up", action: addItem) .accessibilityLabel("Add Clipping") Spacer() Spacer() @@ -110,8 +128,7 @@ struct CopiedItemList: View { ) do { try newItem.save(context: modelContext) - } - catch _ { + } catch _ { return } } @@ -124,6 +141,26 @@ struct CopiedItemList: View { } } } + private func deleteSelection() { + withAnimation { + for id in selection { + do { + try modelContext.delete(model: CopiedItem.self, where: #Predicate{ + $0.persistentModelID == id + }) + } catch { + print(selection) + } + } + } + self.editMode?.wrappedValue.toggle() + } + + private func deleteItem(_ item: CopiedItem) { + withAnimation { + modelContext.delete(item) + } + } } enum ContentTypeFilter: String { @@ -132,7 +169,7 @@ enum ContentTypeFilter: String { case image = "public.image" case file = "public.content" case any = "" - var id: String { return "\(self)" } + var id: String { "\(self)" } } struct CopiedItemListContainer: View { @@ -154,9 +191,52 @@ struct CopiedItemListContainer: View { } #Preview { - NavigationStack { - CopiedItemListContainer() + Group { + @State var exampleData: [CopiedItem] = [ + CopiedItem( + content: "Test Just Text. Alot of text. Like a LOOOOOOOOOOOOOOOOOOOOOO00000000000000000000000000000T", + type: .text, + timestamp: nil + ), + CopiedItem(content: "Empty title falls back to content", type: .text, title: "TITLE", timestamp: nil), + CopiedItem( + content: "Test Text With Title And Timestamp", + type: .text, + title: "TITLE", + timestamp: Date(timeIntervalSince1970: .zero) + ), + CopiedItem( + content: "iuweghcdiouwgcoewudgsddddddddddddddddddddddddddddddddddddddsdddddddddddddddddddddddddddddddddddddddddddchoewudchoewudchoecwidhcduwhcouwdhcoudwhcowudhcoh", + type: .text, + title: "", + timestamp: Date() + ), + CopiedItem( + content: URL(string: "https://google.com")!, + type: .url, + timestamp: Date(timeIntervalSinceNow: -10000) + ), + CopiedItem(content: URL(string: "file:///tmp/test/owufhcowhcouwehdcouhedouchweoduhouhdcwdeo")!, type: .url, timestamp: nil), + CopiedItem( + content: URL(string: "https://areally.long.url/?q=123idhwiue")!, + type: .url, + title: "URL with a title", + timestamp: Date(timeIntervalSince1970: .zero) + ), + ] + NavigationStack { + CopiedItemListContainer() + } + .pasteboardContext() + .modelContainer(for: CopiedItem.self, inMemory: true, onSetup: { mc in + do { + let ctx = try mc.get() + try exampleData.forEach { item in + try item.save(context: ctx.mainContext) + } + } catch { + return + } + }) } - .pasteboardContext() - .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift index 8f1181a..78323b4 100644 --- a/transcopied/CopiedItemList/CopiedItemListRow.swift +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -96,6 +96,7 @@ struct CopiedItemRow: View { .squareFrame() .maxWidth(50) .maxHeight(50) + .allowsHitTesting(false) } } } @@ -196,13 +197,13 @@ struct CopiedItemRow: View { Group { @State var exampleData: [CopiedItem] = [ CopiedItem( - content: UIImage(systemName: "person.text.rectangle.fill"), + content: UIImage(systemName: "person.text.rectangle.fill")!, type: .image, title: "", timestamp: Date(timeIntervalSince1970: .zero) ), CopiedItem( - content: UIImage(systemName: "clock"), + content: UIImage(systemName: "clock")!, type: .image, title: "Image with a title", timestamp: Date(timeIntervalSince1970: .zero)