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)
+ }
+}