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..b801885 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,14 @@ +--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 + +--disable redundantType 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-Info.plist b/Transcopied-Info.plist index ca9a074..634f4e4 100644 --- a/Transcopied-Info.plist +++ b/Transcopied-Info.plist @@ -4,6 +4,8 @@ UIBackgroundModes + fetch + processing remote-notification diff --git a/Transcopied.xcodeproj/project.pbxproj b/Transcopied.xcodeproj/project.pbxproj index 35f0376..a4e4b8b 100644 --- a/Transcopied.xcodeproj/project.pbxproj +++ b/Transcopied.xcodeproj/project.pbxproj @@ -7,20 +7,30 @@ 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 */; }; + 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 */; }; + 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 */; }; 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 */; }; 1DFE67962B13168E000E36DA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1DFE67952B13168E000E36DA /* Preview Assets.xcassets */; }; /* 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 = ""; }; + 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; }; @@ -35,7 +45,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 */ @@ -45,12 +55,39 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1D086B992B95340F004939CD /* SwiftUIX in Frameworks */, + 1D8B72D92B9EA6B30028D7D4 /* CompoundPredicate in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1D086B912B946169004939CD /* Migrations */ = { + isa = PBXGroup; + children = ( + 1D086B922B9461E8004939CD /* CopiedItemVersions.swift */, + ); + path = Migrations; + sourceTree = ""; + }; + 1D086B952B948394004939CD /* CopiedItemList */ = { + isa = PBXGroup; + children = ( + 1DFE678E2B13168D000E36DA /* CopiedItemList.swift */, + 1D086B962B9483F0004939CD /* CopiedItemListRow.swift */, + ); + path = CopiedItemList; + sourceTree = ""; + }; + 1DAB74952BAA6316003A591F /* Utils */ = { + isa = PBXGroup; + children = ( + 1DAB74962BAA634D003A591F /* Debugging.swift */, + ); + path = Utils; + sourceTree = ""; + }; 1DD331BA2B1828CE00708F46 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -88,8 +125,10 @@ 1DFE678B2B13168D000E36DA /* transcopied */ = { isa = PBXGroup; children = ( + 1DAB74952BAA6316003A591F /* Utils */, + 1D086B952B948394004939CD /* CopiedItemList */, + 1D086B912B946169004939CD /* Migrations */, 1DFE678C2B13168D000E36DA /* Transcopied.swift */, - 1DFE678E2B13168D000E36DA /* CopiedItemsList.swift */, 1DBC765E2B20BAB2004B1261 /* PBManager.swift */, 1DF36DD62B16EDC80037FA6A /* CopiedEditorView.swift */, 1DFE67922B13168E000E36DA /* Assets.xcassets */, @@ -97,6 +136,7 @@ 1D8346852B1415AB004ACF46 /* CopiedItem.swift */, 1DD331C82B19846400708F46 /* ModalMessage.swift */, 1D4BCFD62B1AD79C00E10186 /* AppDetails.swift */, + 1D17E8312B779DEC002FE8E7 /* Activitea.swift */, ); path = transcopied; sourceTree = ""; @@ -116,7 +156,7 @@ isa = PBXNativeTarget; buildConfigurationList = 1DFE67992B13168E000E36DA /* Build configuration list for PBXNativeTarget "Transcopied" */; buildPhases = ( - 1DD331C52B194D4F00708F46 /* ShellScript */, + 1D5046D52BA954FA00ED86BA /* ShellScript */, 1DFE67852B13168D000E36DA /* Sources */, 1DFE67862B13168D000E36DA /* Frameworks */, 1DFE67872B13168D000E36DA /* Resources */, @@ -126,6 +166,10 @@ dependencies = ( ); name = Transcopied; + packageProductDependencies = ( + 1D086B982B95340F004939CD /* SwiftUIX */, + 1D8B72D82B9EA6B30028D7D4 /* CompoundPredicate */, + ); productName = nipper; productReference = 1DFE67892B13168D000E36DA /* Transcopied.app */; productType = "com.apple.product-type.application"; @@ -136,9 +180,9 @@ 1DFE67812B13168D000E36DA /* Project object */ = { isa = PBXProject; attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1520; + LastUpgradeCheck = 1520; TargetAttributes = { 1DFE67882B13168D000E36DA = { CreatedOnToolsVersion = 15.0; @@ -155,6 +199,8 @@ ); mainGroup = 1DFE67802B13168D000E36DA; packageReferences = ( + 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */, + 1D8B72D72B9EA6B30028D7D4 /* XCRemoteSwiftPackageReference "CompoundPredicate" */, ); productRefGroup = 1DFE678A2B13168D000E36DA /* Products */; projectDirPath = ""; @@ -178,7 +224,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1DD331C52B194D4F00708F46 /* ShellScript */ = { + 1D5046D52BA954FA00ED86BA /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -194,8 +240,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"; - showEnvVarsInLog = 0; + 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 */ @@ -204,13 +249,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 1DFE678F2B13168D000E36DA /* CopiedItemsList.swift in Sources */, - 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, - 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, + 1D17E8322B779DEC002FE8E7 /* Activitea.swift in Sources */, 1D4BCFD72B1AD79C00E10186 /* AppDetails.swift in Sources */, - 1DD331C92B19846400708F46 /* ModalMessage.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 */, + 1DD331C92B19846400708F46 /* ModalMessage.swift in Sources */, + 1DBC765F2B20BAB2004B1261 /* PBManager.swift in Sources */, + 1DFE678D2B13168D000E36DA /* Transcopied.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -220,7 +269,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; @@ -254,7 +303,7 @@ 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; @@ -269,6 +318,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; @@ -276,6 +329,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; @@ -291,7 +345,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; @@ -325,7 +379,7 @@ 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; @@ -341,6 +395,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; @@ -380,7 +435,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "\"-Xfrontend -debug-time-function-bodies\""; + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.slyboots.transcopied; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -420,7 +475,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"; @@ -442,7 +497,7 @@ 1DFE67982B13168E000E36DA /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; 1DFE67992B13168E000E36DA /* Build configuration list for PBXNativeTarget "Transcopied" */ = { isa = XCConfigurationList; @@ -451,9 +506,41 @@ 1DFE679B2B13168E000E36DA /* Release */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Debug; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 1D243AF62B7676AC002560F9 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftUIX/SwiftUIX"; + requirement = { + kind = upToNextMajorVersion; + 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 */ + 1D086B982B95340F004939CD /* SwiftUIX */ = { + isa = XCSwiftPackageProductDependency; + 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 deleted file mode 100644 index cdae682..0000000 --- a/Transcopied.xcodeproj/xcshareddata/xcschemes/Transcopied.xcscheme +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/Activitea.swift b/transcopied/Activitea.swift new file mode 100644 index 0000000..9021f47 --- /dev/null +++ b/transcopied/Activitea.swift @@ -0,0 +1,18 @@ +// +// Activitea.swift +// Transcopied +// +// Created by Dakota Lorance on 2/10/24. +// + +import SwiftUI + +struct Activitea: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +#Preview { + Activitea() +} 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 532613a..a123c9b 100644 --- a/transcopied/CopiedEditorView.swift +++ b/transcopied/CopiedEditorView.swift @@ -7,44 +7,91 @@ import Foundation import SwiftData import SwiftUI +import SwiftUIX import UniformTypeIdentifiers struct CopiedEditorView: View { + enum EditorFocused { + case title + case content + } + @Environment(\.presentationMode) var presentationMode: Binding @Environment(\.modelContext) private var modelContext + @Environment(PBManager.self) private var pbm @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 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) + .padding(.top) Divider().padding(.vertical, 5).foregroundStyle(.primary) - HStack { - Text(item.type) + - Text(" - ") + - Text("\(item.content?.count ?? 0) characters") - } - .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) - .onChange(of: editorFocused) { - bottomBarPlacement = editorFocused ? .keyboard : .bottomBar - } + 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(75) + TextEditor(text: $item.content) + .foregroundStyle(.primary) + .focused($editorFocused, equals: .content) + .onChange(of: editorFocused) { + bottomBarPlacement = editorFocused != nil ? .keyboard : .bottomBar + } + } + case "public.image": + VStack { + Image(data: item.data)! + .resizable() + .scaledToFit() + Spacer() + } + + default: + Spacer() + EmptyView() + } } + .defaultFocus($editorFocused, EditorFocused.title) + .onAppear(perform: { + let _t = (item.title) + let _c = (item.text) + + if !_c.isEmpty { + editorFocused = .content + } + else if !_t.isEmpty, _c.isEmpty { + editorFocused = .title + } + else { + editorFocused = nil + } + + }) .accessibilityAction(.magicTap) { setClipboard() } .navigationTitle("Edit") .padding(.horizontal) @@ -54,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() @@ -97,13 +144,77 @@ struct CopiedEditorView: View { } private func setClipboard() { - if item.content != nil { - UIPasteboard.general.setValue(item.content as Any, 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) } } } -#Preview { - CopiedEditorView(item: CopiedItem(content: "Testing 123", title: "", timestamp: Date(), type: CopiedItemType.text)) - .modelContainer(for: CopiedItem.self, inMemory: true) +#Preview("Text Clip") { + CopiedEditorView( + item: CopiedItem( + content: "Text Content", + type: PasteboardContentType.text, + title: "Test Title", + timestamp: Date() + ) + ) + .pasteboardContext() + .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() + ) + ) + .pasteboardContext() + .modelContainer(for: CopiedItem.self, inMemory: true) +} + +#Preview("URL Clip no Title") { + CopiedEditorView( + item: CopiedItem( + content: URL(string: "https://www.reddit.com/") as Any, + type: PasteboardContentType.url, + title: "", + timestamp: Date() + ) + ) + .pasteboardContext() + .modelContainer(for: CopiedItem.self, inMemory: true) +} + +#Preview("Image Clip with Title") { + CopiedEditorView( + item: CopiedItem( + 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: UIImage(systemName: "clock")!, + type: PasteboardContentType.image, + title: "", + timestamp: Date() + ) + ) + .pasteboardContext() + .modelContainer(for: CopiedItem.self, inMemory: true) } diff --git a/transcopied/CopiedItem.swift b/transcopied/CopiedItem.swift index a40b441..00d2f3a 100644 --- a/transcopied/CopiedItem.swift +++ b/transcopied/CopiedItem.swift @@ -5,84 +5,149 @@ // 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" +enum PasteboardContentType: String, Codable, Identifiable, CaseIterable, Hashable { + 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 { + 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 CopiedItemKindScope: CaseIterable { - case txt - case url - case img - case file - case all +enum CopiedItemError: Error { + case alreadyExists } -struct CopiedItemSearchToken { - enum Kind: String, Identifiable, Hashable, CaseIterable { - case txt - case url - case img - case file - case all - var id: Self {self} +@Model +final class CopiedItem { + var uid: String = "00000000-0000-0000-0000-000000000000" + var title: String = "" + var type: String = "" + var timestamp = Date(timeIntervalSince1970: .zero) + var content: String = "" + @Attribute(.externalStorage) + var data = Data() + + @Transient + var text: String { + get { return content } + set { + content = newValue + } } - enum Scope: String, Identifiable, Hashable, CaseIterable { - case kind - var id: Self {self} + @Transient + var url: URL { + get { return URL(string: content)! } +// get { return type == PasteboardContentType.url.rawValue ? URL(string: String(data: content, encoding: .utf8)!) : nil } + set { + content = newValue.absoluteString.removingPercentEncoding! + } } - var kind: Kind = .all - var scope: Scope = .kind -} -//let copiedItemSearchTokens = [ -// CopiedItemTypeToken("txt"), -// CopiedItemTypeToken("url"), -// CopiedItemTypeToken("url"), -// CopiedItemTypeToken("file"), -//] + @Transient + var file: Data? { + get { return type == PasteboardContentType.file.rawValue ? data : nil } + set { data = Data(newValue!) } + } -@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 + @Transient + var image: UIImage? { + get { return type == PasteboardContentType.image.rawValue ? UIImage(data: data) : nil } + set { + data = Data(newValue!.pngData()!) + } } + 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.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 = 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 = S + self.title = title + case .file: + let D = (content as! Data) + self.uid = hashString(data: D) + self.type = PasteboardContentType.file.rawValue + self.data = Data(D) + self.title = title + } + if !self.content.isEmpty || !data.isEmpty { + self.timestamp = timestamp ?? Date(timeIntervalSinceNow: 0) + } + } - 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 - } + // 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 false + } + } + + func save(context: ModelContext) throws { + // find if the budget category with the same name already exists + if !exists(context: context, uid: uid) { + // save it + context.insert(self) + } + else { + // do something else + throw CopiedItemError.alreadyExists } } } +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: { @@ -96,8 +161,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 +179,31 @@ public extension Binding where Value: Equatable { else { source.wrappedValue = newValue } - }) + } + ) } } + +#Preview { + Group { + Text("Test") + } +} + +// #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 new file mode 100644 index 0000000..d2bde38 --- /dev/null +++ b/transcopied/CopiedItemList/CopiedItemList.swift @@ -0,0 +1,242 @@ +// +// CopiedItemList.swift +// Transcopied +// +// Created by Dakota Lorance on 11/26/23. +// +import CompoundPredicate +import SwiftData +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(selection: $selection) { + ForEach(items) { item in + NavigationLink { + CopiedEditorView(item: item) + } label: { + 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) + } + } + } + } + .onAppear(perform: { addItem() }) + .navigationTitle("Clippings") + .toolbar { + 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.up", action: addItem) + .accessibilityLabel("Add Clipping") + Spacer() + Spacer() + NavigationLink { + AppDetails() + } label: { + Image(systemName: "slider.horizontal.3") + .imageScale(.medium) + .foregroundStyle(.primary) + } + } + } + } + + 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 emptyScope = #Predicate { _ in + searchScope.isEmpty + } + let matchesScope = #Predicate { item in + item.type.localizedStandardContains(searchScope) + } + let emptySearch = #Predicate { _ in + searchText.isEmpty + } + let matchingTitle = #Predicate { item in + item.title.localizedStandardContains(searchText) + } + let matchingContent = #Predicate { item in + item.content.localizedStandardContains(searchText) + } + + let scopeFilter = [emptyScope, matchesScope].disjunction() + let queryFilter = [emptySearch, [matchingTitle, matchingContent].disjunction()].disjunction() + + _items = Query( + filter: [queryFilter, scopeFilter].conjunction(), + sort: \CopiedItem.timestamp, + order: .reverse + ) + } + + private func addItem() { + withAnimation { + let content = pbm.get() + guard !(content == nil) else { + return + } + + let pbtype = pbm.uti + + let newItem = CopiedItem( + content: content!, + type: PasteboardContentType[pbtype!]!, + title: "", + timestamp: Date() + ) + do { + try newItem.save(context: modelContext) + } catch _ { + return + } + } + } + + private func deleteItems(offsets: IndexSet) { + withAnimation { + for index in offsets { + modelContext.delete(items[index]) + } + } + } + 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 { + case text = "public.plain-text" + case url = "public.url" + case image = "public.image" + case file = "public.content" + case any = "" + var id: String { "\(self)" } +} + +struct CopiedItemListContainer: View { + @State private var searchText: String = "" + @State private var searchTokens = [PasteboardContentType]() + @State private var searchScope: ContentTypeFilter = .any + + 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) + } + } +} + +#Preview { + 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 + } + }) + } +} diff --git a/transcopied/CopiedItemList/CopiedItemListRow.swift b/transcopied/CopiedItemList/CopiedItemListRow.swift new file mode 100644 index 0000000..78323b4 --- /dev/null +++ b/transcopied/CopiedItemList/CopiedItemListRow.swift @@ -0,0 +1,223 @@ +// +// CopiedItemListRow.swift +// Transcopied +// +// Created by Dakota Lorance on 3/3/24. +// + +import SwiftUI +import SwiftUIX + +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 { + fallback + } + else if alt != nil { + main != nil ? String(main!.prefix(100)) : String(alt!.prefix(100)) + } + else { + // main should be safe to use + String(main!.prefix(100)) + } + } +} + +struct CopiedItemRow: View { + @Bindable var item: CopiedItem + private var iconName: String + private var iconColor: Color + + private var bytefmt = ByteCountFormatter() + + var body: some View { + 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) + .lineLimit(5) + .truncationMode(.tail) + } + if item.type == "public.image" { + Image(image: item.image!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxHeight:80) + .clipped() + } + Spacer() + HStack() { + if !item.text.isEmpty { + HStack(spacing:0) { + Image(systemName: "info.circle") + .symbolRenderingMode(.monochrome) + Text("\(item.text.count) characters") + } + } + if item.image != nil { + HStack { + Text("PNG "+item.image!.size.width.formatted()+"x"+item.image!.size.height.formatted()) + } + } + HStack(spacing:0) { + Image(systemName: "clock") + .symbolRenderingMode(.monochrome) + Text(relativeDateFmt(item.timestamp)) + } + } + .font(.caption) + .foregroundStyle(.tertiary) + } + if item.type == "public.url" { + // TODO: Move this to be under the URL with the URL font size set smaller + Spacer() + VStack { + LinkPresentationView(url: item.url) + .squareFrame() + .maxWidth(50) + .maxHeight(50) + .allowsHitTesting(false) + } + } + } + } + + 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 + } + } + + 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) + ), + CopiedItem( + content: "iuweghcdiouwgcoewudgsddddddddddddddddddddddddddddddddddddddsdddddddddddddddddddddddddddddddddddddddddddchoewudchoewudchoecwidhcduwhcouwdhcoudwhcowudhcoh", + type: .text, + title: "", + timestamp: Date() + ), + ] + 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) + .listStyle(.automatic) + .modelContainer(for: CopiedItem.self, inMemory: true) + } +} diff --git a/transcopied/CopiedItemsList.swift b/transcopied/CopiedItemsList.swift deleted file mode 100644 index b4d729e..0000000 --- a/transcopied/CopiedItemsList.swift +++ /dev/null @@ -1,202 +0,0 @@ -// -// CopiedItemList.swift -// Transcopied -// -// Created by Dakota Lorance on 11/26/23. -// -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.content, 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.content?.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 - @Query private var items: [CopiedItem] - - var body: some View { - List { - ForEach(items) { item in - NavigationLink { - CopiedEditorView(item: item, title: item.title) - } label: { - CopiedItemRow(item: item) - } - .foregroundStyle(.primary) - } - .onDelete(perform: deleteItems) - } - .navigationTitle("Clippings") - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - EditButton().padding(.trailing) - } - ToolbarItem { - Button(action: addItem) { - Label("Add Clipping", systemImage: "plus") - } - } - } - .toolbar { - ToolbarItemGroup(placement: .bottomBar) { - Button("Paste", systemImage: "square.and.arrow.down", action: addItem) - .accessibilityLabel("Add Clipping") - Spacer() - Spacer() - NavigationLink { - AppDetails() - } label: { - Image(systemName: "slider.horizontal.3") - .imageScale(.medium) - .foregroundStyle(.primary) - } - } - } - } - - init(searchText: 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 - } - }, - sort: \CopiedItem.timestamp, - order: .reverse - ) - } - - private func addItem() { - withAnimation { - let content = PBManager.getClipboard() - let newItem = CopiedItem(content: content, title: nil, timestamp: Date(), type: .text) - modelContext.insert(newItem) - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - for index in offsets { - modelContext.delete(items[index]) - } - } - } -} - -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 - @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 [] - } - } - - 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") - } - } - .searchScopes($searchScope, scopes: { - Text("Kind").tag(CopiedItemSearchToken.Scope.kind) - }) - } -} - -#Preview { - NavigationStack { - CopiedItemListContainer() -// CopiedItemsList(searchText: "") - } - .modelContainer(for: CopiedItem.self, inMemory: true) -} diff --git a/transcopied/Migrations/CopiedItemVersions.swift b/transcopied/Migrations/CopiedItemVersions.swift new file mode 100644 index 0000000..050f91d --- /dev/null +++ b/transcopied/Migrations/CopiedItemVersions.swift @@ -0,0 +1,237 @@ +// +// CopiedItemVersions.swift +// Transcopied +// +// Created by Dakota Lorance on 3/3/24. +// + +import CoreData +import Foundation +import SwiftData +import UIKit + +enum CopiedItemSchemaV1: VersionedSchema { + // initial structure + static var versionIdentifier: Schema.Version = .init(1, 0, 0) + static var models: [any PersistentModel.Type] { + [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] { + [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] { + [CopiedItem.self] + } + + @Model + final class CopiedItem { + var uid: String = "00000000-0000-0000-0000-000000000000" + var title: String = "" + var type: String = "" + var timestamp: Date = Date(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()! + case .file: + let D = (content as! Data) + self.uid = hashString(data: D) + self.type = PasteboardContentType.file.rawValue + 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 = S + case .url: + let U = (content as! URL) + self.uid = hashString(data: Data(U.absoluteString.utf8)) + self.type = PasteboardContentType.url.rawValue + 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] { + [CopiedItem.self] + } + + @Model + final class CopiedItem { + var uid: String = "00000000-0000-0000-0000-000000000000" + var title: String = "" + var type: String = "" + var timestamp: Date = Date(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)) + } + self.title = title + } + } +} + +enum CopiedItemsMigrationPlan: SchemaMigrationPlan { + static var schemas: [any VersionedSchema.Type] { + [CopiedItemSchemaV1.self, CopiedItemSchemaV2.self, CopiedItemSchemaV3.self] + } + + static var stages: [MigrationStage] { + [ + // V1__V2, +// V2__V3 + ] + } + + static let V1__V1_5 = MigrationStage.custom( + fromVersion: CopiedItemSchemaV1.self, + toVersion: CopiedItemSchemaV2.self, + willMigrate: { _ 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 + ) + 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 03baee3..e9ecfb2 100644 --- a/transcopied/PBManager.swift +++ b/transcopied/PBManager.swift @@ -5,14 +5,210 @@ // Created by Dakota Lorance on 12/6/23. // +import Combine import Foundation +import LinkPresentation import SwiftUI +import SwiftUIX +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? { + PasteType(rawValue: index) ?? PasteType.allCases.first(where: { index == "\($0)" })! + } +} + +@Observable +class PBManager { + var incomingBuffer: Any? + var changes: Int = 0 + private var board: UIPasteboard = UIPasteboard.general + + var canCopy: Bool { + let types = PasteType.allCases.map(\.rawValue) + return board.contains(pasteboardTypes: types) + } + + var uti: String? { + if board.hasImages { + return "public.image" + } + if board.hasURLs { + return "public.url" + } + if board.hasStrings { + if board.string!.isURL() { + return "public.url" + } + return "public.plain-text" + } + if board.value(forPasteboardType: "public.content") != nil { + return "public.content" + } + return nil + } + + func hashed(data: Any, type: PasteType) -> Int { + switch type { + case .image: + ((data as? Data)?.base64EncodedString().hashValue)! + case .url: + ((data as? URL)?.absoluteString.hashValue)! + case .text: + (data as? String)!.hashValue + default: + (data as? Data)!.hashValue + } + } + + func get() -> Any? { + if !canCopy { + return nil + } + + changes = board.changeCount + if let url = board.url { + return url + } + if let image = board.image { + return image + } + if let string = board.string { + if string.isURL() { + return URL(string: string) + } + return string + } + return board.value(forPasteboardType: "public.content") + } + + 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) + } + } +} + +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) {} + + 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 { + @State var pbm = PBManager() + 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() + } + } +} + +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 onPasteboardContent(perform action: @escaping () -> Void) -> some View { + modifier(ClipboardHasContentModifier(action: action)) + } + + func pasteboardContext() -> some View { + modifier(PasteboardContextModifier()) + } +} + +extension UIPasteboard { + var hasContent: Bool { + numberOfItems > 0 && contains(pasteboardTypes: PasteType.allCases.map(\.rawValue)) + } -final class PBManager { - static func getClipboard() -> String? { - let pasteboard = UIPasteboard.general - let data = pasteboard.string - return data + var hasContentPublisher: AnyPublisher { + 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 6394493..04fd5f6 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,20 +10,33 @@ import SwiftUI @main struct Transcopied: App { - @State var copiedItemSearch: String = "" + @State private var pbm = PBManager() var sharedModelContainer: ModelContainer = { let schema = Schema([ CopiedItem.self, ]) +#if DEBUG let modelConfiguration = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, allowsSave: true, - cloudKitDatabase: ModelConfiguration.CloudKitDatabase.private("iCloud.Transcopied") + 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, configurations: [modelConfiguration]) + return try ModelContainer( + for: schema, +// migrationPlan: CopiedItemsMigrationPlan.self, + configurations: [modelConfiguration] + ) } catch { fatalError("Could not create ModelContainer: \(error)") @@ -33,8 +46,24 @@ struct Transcopied: App { var body: some Scene { WindowGroup { NavigationStack { - CopiedItemsList(searchText: copiedItemSearch) - .searchable(text: $copiedItemSearch) + 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 + if pbm.canCopy { + // then save the data from the clipboard for use later + pbm.incomingBuffer = pbm.get() + } } } .modelContainer(sharedModelContainer) 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) + } +}