├── .clang-format ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── COPYING ├── Headers └── libSandy.h ├── Libraries ├── _roothide │ └── libsandy.tbd ├── _rootless │ └── libsandy.tbd └── libsandy.tbd ├── Makefile ├── Manager ├── PasteboardItem.h ├── PasteboardItem.m ├── PasteboardManager.h └── PasteboardManager.m ├── Preferences ├── Cells │ ├── LinkCell.h │ ├── LinkCell.m │ ├── SingleContactCell.h │ └── SingleContactCell.m ├── Controllers │ ├── KayokoCreditsListController.h │ ├── KayokoCreditsListController.m │ ├── KayokoListItemsController.h │ ├── KayokoListItemsController.m │ ├── KayokoRootListController.h │ └── KayokoRootListController.m ├── Makefile ├── NotificationKeys.h ├── PreferenceKeys.h ├── Resources │ ├── Copy.aiff │ ├── Credits.plist │ ├── HLS_iPad_Universal@3x.png │ ├── HLS_iPhone_Universal@3x.png │ ├── Icon.png │ ├── Icon@2x.png │ ├── Info.plist │ ├── Paste.aiff │ ├── Root.plist │ └── zh-Hans.lproj │ │ ├── Credits.strings │ │ ├── InfoPlist.strings │ │ ├── Root.strings │ │ └── Tweak.strings └── layout │ └── Library │ └── PreferenceLoader │ └── Preferences │ └── KayokoPreferences │ └── Root.plist ├── Preview.png ├── README.md ├── Tweak ├── Core │ ├── KayokoCore.h │ ├── KayokoCore.m │ ├── KayokoCore.plist │ ├── KayokoCoreLogos.xm │ ├── Makefile │ └── Views │ │ ├── KayokoFavoritesTableView.h │ │ ├── KayokoFavoritesTableView.m │ │ ├── KayokoHistoryTableView.h │ │ ├── KayokoHistoryTableView.m │ │ ├── KayokoPreviewView.h │ │ ├── KayokoPreviewView.m │ │ ├── KayokoTableView.h │ │ ├── KayokoTableView.m │ │ ├── KayokoTableViewCell.h │ │ ├── KayokoTableViewCell.m │ │ ├── KayokoView.h │ │ └── KayokoView.m └── Helper │ ├── KayokoHelper.h │ ├── KayokoHelper.m │ ├── KayokoHelper.plist │ ├── KayokoMenu.h │ ├── KayokoMenu.m │ ├── KeyokoHelperLogos.xm │ └── Makefile ├── Utils ├── AlertUtil.h ├── AlertUtil.m ├── ImageUtil.h ├── ImageUtil.m ├── StringUtil.h └── StringUtil.m ├── control ├── devkit ├── env.sh ├── roothide.sh ├── rootless.sh ├── sim-install.sh ├── sim-launch.sh └── simulator.sh ├── layout ├── DEBIAN │ ├── postinst │ └── postrm └── Library │ └── libSandy │ ├── Kayoko.plist │ └── Kayoko_RootHide.plist └── libroot └── dyn.c /.clang-format: -------------------------------------------------------------------------------- 1 | # clang-format 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | AccessModifierOffset: -4 5 | ContinuationIndentWidth: 4 6 | ColumnLimit: 120 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [kaethchen] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug in Kayoko. 4 | title: '' 5 | labels: bug 6 | assignees: kaethchen 7 | 8 | --- 9 | 10 | **Describe the bug:** 11 | 12 | **Steps to reproduce:** 13 | 14 | **Crash log (if applicable):** 15 | Upload your log file to Pastebin and paste the link here. 16 | 17 | **System information:** 18 | - iOS version: 19 | - Device: 20 | - Jailbreak: 21 | - Jailbreak Type: 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a feature for Kayoko. 4 | title: '' 5 | labels: enhancement 6 | assignees: kaethchen 7 | 8 | --- 9 | 10 | **Describe the requested feature:** 11 | 12 | **System information:** 13 | - iOS version: 14 | - Device: 15 | - Jailbreak: 16 | - Jailbreak Type: 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Package 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - release 8 | 9 | env: 10 | VERSION: 1.0 11 | FINALPACKAGE: 1 12 | HOMEBREW_NO_AUTO_UPDATE: 1 13 | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 14 | 15 | jobs: 16 | build: 17 | runs-on: macos-14 18 | 19 | steps: 20 | - name: Setup Xcode 21 | uses: maxim-lobanov/setup-xcode@v1 22 | with: 23 | xcode-version: '15.4' 24 | 25 | - name: Setup Environment 26 | run: | 27 | echo "THEOS=$HOME/theos" >> $GITHUB_ENV 28 | echo "THEOS_IGNORE_PARALLEL_BUILDING_NOTICE=yes" >> $HOME/.theosrc 29 | 30 | - name: Setup Theos 31 | run: | 32 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/OwnGoalStudio/theos/ci/auth-token/bin/install-theos)" 33 | $THEOS/bin/install-sdk iPhoneOS16.5 || true 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | 37 | - name: Setup Environment (RootHide) 38 | run: | 39 | echo "THEOS=$HOME/theos-roothide" >> $GITHUB_ENV 40 | 41 | - name: Setup Theos (RootHide) 42 | run: | 43 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/OwnGoalStudio/theos-roothide/ci/auth-token/bin/install-theos)" 44 | $THEOS/bin/install-sdk iPhoneOS16.5 || true 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | - name: Checkout 49 | uses: actions/checkout@v4 50 | 51 | - name: Build (arm) 52 | run: | 53 | source devkit/env.sh 54 | make package 55 | 56 | - name: Build (arm64) 57 | run: | 58 | source devkit/rootless.sh 59 | make package 60 | 61 | - name: Build (arm64e) 62 | run: | 63 | source devkit/roothide.sh 64 | make package 65 | 66 | - name: Collect dSYMs 67 | run: | 68 | find .theos/obj -name "*.dSYM" -exec cp -r {} ./packages/ \; 69 | VERSION=$(grep '^Version:' .theos/_/DEBIAN/control | awk '{print $2}') 70 | echo "VERSION=$VERSION" >> $GITHUB_ENV 71 | 72 | - name: Upload dSYMs 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: symbols-${{ env.VERSION }} 76 | path: | 77 | ./packages/*.dSYM 78 | 79 | - name: Upload Packages 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: packages-${{ env.VERSION }} 83 | path: | 84 | ./packages/*.deb 85 | 86 | - name: Release 87 | uses: softprops/action-gh-release@v2 88 | with: 89 | tag_name: v${{ env.VERSION }}+Lessica 90 | draft: true 91 | prerelease: false 92 | files: | 93 | ./packages/*.deb 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | .stamp 4 | .theos/ 5 | .vscode/ 6 | packages/ 7 | compile_commands.json 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,macos 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,macos 11 | 12 | ### macOS ### 13 | # General 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | 18 | # Icon must end with two \r 19 | Icon 20 | 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | ### macOS Patch ### 42 | # iCloud generated files 43 | *.icloud 44 | 45 | ### Swift ### 46 | # Xcode 47 | # 48 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 49 | 50 | ## User settings 51 | xcuserdata/ 52 | 53 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 54 | *.xcscmblueprint 55 | *.xccheckout 56 | 57 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 58 | build/ 59 | DerivedData/ 60 | *.moved-aside 61 | *.pbxuser 62 | !default.pbxuser 63 | *.mode1v3 64 | !default.mode1v3 65 | *.mode2v3 66 | !default.mode2v3 67 | *.perspectivev3 68 | !default.perspectivev3 69 | 70 | ## Obj-C/Swift specific 71 | *.hmap 72 | 73 | ## App packaging 74 | *.ipa 75 | *.dSYM.zip 76 | *.dSYM 77 | 78 | ## Playgrounds 79 | timeline.xctimeline 80 | playground.xcworkspace 81 | 82 | # Swift Package Manager 83 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 84 | # Packages/ 85 | # Package.pins 86 | # Package.resolved 87 | # *.xcodeproj 88 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 89 | # hence it is not needed unless you have added a package configuration file to your project 90 | # .swiftpm 91 | 92 | .build/ 93 | 94 | # CocoaPods 95 | # We recommend against adding the Pods directory to your .gitignore. However 96 | # you should judge for yourself, the pros and cons are mentioned at: 97 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 98 | # Pods/ 99 | # Add this line if you want to avoid checking in source code from the Xcode workspace 100 | # *.xcworkspace 101 | 102 | # Carthage 103 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 104 | # Carthage/Checkouts 105 | 106 | Carthage/Build/ 107 | 108 | # Accio dependency management 109 | Dependencies/ 110 | .accio/ 111 | 112 | # fastlane 113 | # It is recommended to not store the screenshots in the git repo. 114 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 115 | # For more information about the recommended setup visit: 116 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 117 | 118 | fastlane/report.xml 119 | fastlane/Preview.html 120 | fastlane/screenshots/**/*.png 121 | fastlane/test_output 122 | 123 | # Code Injection 124 | # After new code Injection tools there's a generated folder /iOSInjectionProject 125 | # https://github.com/johnno1962/injectionforxcode 126 | 127 | iOSInjectionProject/ 128 | 129 | ### Xcode ### 130 | 131 | ## Xcode 8 and earlier 132 | 133 | ### Xcode Patch ### 134 | *.xcodeproj/* 135 | !*.xcodeproj/project.pbxproj 136 | !*.xcodeproj/xcshareddata/ 137 | !*.xcodeproj/project.xcworkspace/ 138 | !*.xcworkspace/contents.xcworkspacedata 139 | /*.gcno 140 | **/xcshareddata/WorkspaceSettings.xcsettings 141 | 142 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,macos 143 | 144 | -------------------------------------------------------------------------------- /Headers/libSandy.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #if defined(__cplusplus) 4 | extern "C" { 5 | #endif 6 | 7 | #define kLibSandySuccess 0 8 | #define kLibSandyErrorXPCFailure 1 9 | #define kLibSandyErrorRestricted 2 10 | 11 | extern int libSandy_applyProfile(const char* profileName); 12 | extern bool libSandy_works(void); 13 | 14 | #if defined(__cplusplus) 15 | } 16 | #endif -------------------------------------------------------------------------------- /Libraries/_roothide/libsandy.tbd: -------------------------------------------------------------------------------- 1 | --- !tapi-tbd-v2 2 | archs: [ armv7, arm64, arm64e ] 3 | uuids: [ 'armv7: E8393BFB-05A6-3A91-AADC-6BB44C2785B9', 'arm64: 72E35021-8C45-3241-8798-711482EFCB25', 4 | 'arm64e: 0071493A-6940-36B2-BB35-118C4236A273' ] 5 | platform: ios 6 | flags: [ flat_namespace, not_app_extension_safe ] 7 | install-name: '@loader_path/.jbroot/usr/lib/libsandy.dylib' 8 | current-version: 0 9 | compatibility-version: 0 10 | objc-constraint: retain_release 11 | exports: 12 | - archs: [ armv7, arm64, arm64e ] 13 | symbols: [ _libSandy_applyProfile, _libSandy_works ] 14 | undefineds: 15 | - archs: [ armv7 ] 16 | symbols: [ __Unwind_SjLj_Register, __Unwind_SjLj_Resume, 17 | __Unwind_SjLj_Unregister ] 18 | - archs: [ armv7, arm64 ] 19 | symbols: [ dyld_stub_binder ] 20 | - archs: [ arm64, arm64 ] 21 | symbols: [ __Unwind_Resume ] 22 | - archs: [ armv7, arm64, arm64e ] 23 | symbols: [ __Block_object_assign, __Block_object_dispose, 24 | __NSConcreteGlobalBlock, __NSConcreteStackBlock, 25 | __NSGetArgv, ___CFConstantStringClassReference, 26 | ___objc_personality_v0, ___stack_chk_fail, 27 | ___stack_chk_guard, __xpc_type_array, 28 | __xpc_type_dictionary, __xpc_type_string, 29 | _dispatch_once, _objc_autoreleaseReturnValue, 30 | _objc_enumerationMutation, _objc_msgSend, 31 | _objc_release, _objc_retain, _objc_retainAutorelease, 32 | _objc_retainAutoreleasedReturnValue, 33 | _sandbox_extension_consume, _xpc_array_apply, 34 | _xpc_connection_create_mach_service, 35 | _xpc_connection_resume, 36 | _xpc_connection_send_message_with_reply_sync, 37 | _xpc_connection_set_event_handler, 38 | _xpc_dictionary_create, _xpc_dictionary_get_bool, 39 | _xpc_dictionary_get_value, _xpc_dictionary_set_int64, 40 | _xpc_dictionary_set_string, _xpc_get_type, 41 | _xpc_string_get_string_ptr ] 42 | objc-classes: [ _NSDictionary, _NSFileManager, _NSString ] 43 | ... 44 | -------------------------------------------------------------------------------- /Libraries/_rootless/libsandy.tbd: -------------------------------------------------------------------------------- 1 | --- !tapi-tbd-v2 2 | archs: [ armv7, arm64, arm64e ] 3 | uuids: [ 'armv7: E8393BFB-05A6-3A91-AADC-6BB44C2785B9', 'arm64: 72E35021-8C45-3241-8798-711482EFCB25', 4 | 'arm64e: 0071493A-6940-36B2-BB35-118C4236A273' ] 5 | platform: ios 6 | flags: [ flat_namespace, not_app_extension_safe ] 7 | install-name: '@rpath/libsandy.dylib' 8 | current-version: 0 9 | compatibility-version: 0 10 | objc-constraint: retain_release 11 | exports: 12 | - archs: [ armv7, arm64, arm64e ] 13 | symbols: [ _libSandy_applyProfile, _libSandy_works ] 14 | undefineds: 15 | - archs: [ armv7 ] 16 | symbols: [ __Unwind_SjLj_Register, __Unwind_SjLj_Resume, 17 | __Unwind_SjLj_Unregister ] 18 | - archs: [ armv7, arm64 ] 19 | symbols: [ dyld_stub_binder ] 20 | - archs: [ arm64, arm64 ] 21 | symbols: [ __Unwind_Resume ] 22 | - archs: [ armv7, arm64, arm64e ] 23 | symbols: [ __Block_object_assign, __Block_object_dispose, 24 | __NSConcreteGlobalBlock, __NSConcreteStackBlock, 25 | __NSGetArgv, ___CFConstantStringClassReference, 26 | ___objc_personality_v0, ___stack_chk_fail, 27 | ___stack_chk_guard, __xpc_type_array, 28 | __xpc_type_dictionary, __xpc_type_string, 29 | _dispatch_once, _objc_autoreleaseReturnValue, 30 | _objc_enumerationMutation, _objc_msgSend, 31 | _objc_release, _objc_retain, _objc_retainAutorelease, 32 | _objc_retainAutoreleasedReturnValue, 33 | _sandbox_extension_consume, _xpc_array_apply, 34 | _xpc_connection_create_mach_service, 35 | _xpc_connection_resume, 36 | _xpc_connection_send_message_with_reply_sync, 37 | _xpc_connection_set_event_handler, 38 | _xpc_dictionary_create, _xpc_dictionary_get_bool, 39 | _xpc_dictionary_get_value, _xpc_dictionary_set_int64, 40 | _xpc_dictionary_set_string, _xpc_get_type, 41 | _xpc_string_get_string_ptr ] 42 | objc-classes: [ _NSDictionary, _NSFileManager, _NSString ] 43 | ... 44 | -------------------------------------------------------------------------------- /Libraries/libsandy.tbd: -------------------------------------------------------------------------------- 1 | --- !tapi-tbd-v2 2 | archs: [ armv7, arm64, arm64e ] 3 | uuids: [ 'armv7: E8393BFB-05A6-3A91-AADC-6BB44C2785B9', 'arm64: 72E35021-8C45-3241-8798-711482EFCB25', 4 | 'arm64e: 0071493A-6940-36B2-BB35-118C4236A273' ] 5 | platform: ios 6 | flags: [ flat_namespace, not_app_extension_safe ] 7 | install-name: '/usr/lib/libsandy.dylib' 8 | current-version: 0 9 | compatibility-version: 0 10 | objc-constraint: retain_release 11 | exports: 12 | - archs: [ armv7, arm64, arm64e ] 13 | symbols: [ _libSandy_applyProfile, _libSandy_works ] 14 | undefineds: 15 | - archs: [ armv7 ] 16 | symbols: [ __Unwind_SjLj_Register, __Unwind_SjLj_Resume, 17 | __Unwind_SjLj_Unregister ] 18 | - archs: [ armv7, arm64 ] 19 | symbols: [ dyld_stub_binder ] 20 | - archs: [ arm64, arm64 ] 21 | symbols: [ __Unwind_Resume ] 22 | - archs: [ armv7, arm64, arm64e ] 23 | symbols: [ __Block_object_assign, __Block_object_dispose, 24 | __NSConcreteGlobalBlock, __NSConcreteStackBlock, 25 | __NSGetArgv, ___CFConstantStringClassReference, 26 | ___objc_personality_v0, ___stack_chk_fail, 27 | ___stack_chk_guard, __xpc_type_array, 28 | __xpc_type_dictionary, __xpc_type_string, 29 | _dispatch_once, _objc_autoreleaseReturnValue, 30 | _objc_enumerationMutation, _objc_msgSend, 31 | _objc_release, _objc_retain, _objc_retainAutorelease, 32 | _objc_retainAutoreleasedReturnValue, 33 | _sandbox_extension_consume, _xpc_array_apply, 34 | _xpc_connection_create_mach_service, 35 | _xpc_connection_resume, 36 | _xpc_connection_send_message_with_reply_sync, 37 | _xpc_connection_set_event_handler, 38 | _xpc_dictionary_create, _xpc_dictionary_get_bool, 39 | _xpc_dictionary_get_value, _xpc_dictionary_set_int64, 40 | _xpc_dictionary_set_string, _xpc_get_type, 41 | _xpc_string_get_string_ptr ] 42 | objc-classes: [ _NSDictionary, _NSFileManager, _NSString ] 43 | ... 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export PACKAGE_VERSION := 2.8 2 | export ARCHS := arm64 arm64e 3 | export TARGET := iphone:clang:16.5:14.0 4 | export GO_EASY_ON_ME := 1 5 | 6 | INSTALL_TARGET_PROCESSES := SpringBoard Preferences druid pasted 7 | 8 | SUBPROJECTS += Tweak/Core 9 | SUBPROJECTS += Tweak/Helper 10 | SUBPROJECTS += Preferences 11 | 12 | include $(THEOS)/makefiles/common.mk 13 | include $(THEOS_MAKE_PATH)/aggregate.mk -------------------------------------------------------------------------------- /Manager/PasteboardItem.h: -------------------------------------------------------------------------------- 1 | // 2 | // PasteboardItem.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | static NSString *const kItemKeyBundleIdentifier = @"bundle_identifier"; 11 | static NSString *const kItemKeyContent = @"content"; 12 | static NSString *const kItemKeyImageName = @"image_name"; 13 | static NSString *const kItemKeyHasLink = @"has_link"; 14 | 15 | @interface PasteboardItem : NSObject 16 | 17 | @property(nonatomic, copy) NSString *bundleIdentifier; 18 | @property(nonatomic, copy) NSString *displayName; 19 | @property(nonatomic, copy) NSString *content; 20 | @property(nonatomic, copy) NSString *imageName; 21 | @property(nonatomic, assign) BOOL hasLink; 22 | 23 | - (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier 24 | andContent:(NSString *)content 25 | withImageNamed:(NSString *)imageName; 26 | + (PasteboardItem *)itemFromDictionary:(NSDictionary *)dictionary; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Manager/PasteboardItem.m: -------------------------------------------------------------------------------- 1 | // 2 | // PasteboardItem.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "PasteboardItem.h" 9 | 10 | @implementation PasteboardItem 11 | 12 | /** 13 | * Initializes an item based on the given content. 14 | */ 15 | - (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier 16 | andContent:(NSString *)content 17 | withImageNamed:(NSString *)imageName { 18 | self = [super init]; 19 | 20 | if (self) { 21 | [self setBundleIdentifier:bundleIdentifier]; 22 | [self setContent:content]; 23 | [self setImageName:imageName]; 24 | [self setHasLink:[content hasPrefix:@"http://"] || [content hasPrefix:@"https://"]]; 25 | } 26 | 27 | return self; 28 | } 29 | 30 | /** 31 | * Creates an item from a dictionary. 32 | * 33 | * @param dictionary The dictionary to create the item from. 34 | * 35 | * @return The created item. 36 | */ 37 | + (PasteboardItem *)itemFromDictionary:(NSDictionary *)dictionary { 38 | NSString *bundleIdentifier = dictionary[kItemKeyBundleIdentifier]; 39 | NSString *content = dictionary[kItemKeyContent]; 40 | NSString *imageName = dictionary[kItemKeyImageName]; 41 | return [[PasteboardItem alloc] initWithBundleIdentifier:bundleIdentifier 42 | andContent:content 43 | withImageNamed:imageName]; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Manager/PasteboardManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // PasteboardManager.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @class PasteboardItem; 11 | 12 | static NSString *const kHistoryKeyHistory = @"history"; 13 | static NSString *const kHistoryKeyFavorites = @"favorites"; 14 | 15 | @interface PasteboardManager : NSObject { 16 | UIPasteboard *_pasteboard; 17 | NSUInteger _lastChangeCount; 18 | NSFileManager *_fileManager; 19 | } 20 | @property(nonatomic, assign) NSUInteger maximumHistoryAmount; 21 | @property(nonatomic, assign) BOOL saveText; 22 | @property(nonatomic, assign) BOOL saveImages; 23 | @property(nonatomic, assign) BOOL automaticallyPaste; 24 | 25 | + (instancetype)sharedInstance; 26 | - (instancetype)init NS_UNAVAILABLE; 27 | - (void)preparePasteboardQueue; 28 | 29 | + (NSString *)historyPath; 30 | + (NSString *)historyImagesPath; 31 | + (NSBundle *)localizationBundle; 32 | 33 | - (void)pullPasteboardChanges; 34 | - (void)addPasteboardItem:(PasteboardItem *)item toHistoryWithKey:(NSString *)historyKey; 35 | - (void)updatePasteboardWithItem:(PasteboardItem *)item 36 | fromHistoryWithKey:(NSString *)historyKey 37 | shouldAutoPaste:(BOOL)shouldAutoPaste; 38 | - (void)removePasteboardItem:(PasteboardItem *)item 39 | fromHistoryWithKey:(NSString *)historyKey 40 | shouldRemoveImage:(BOOL)shouldRemoveImage; 41 | 42 | - (NSMutableArray *)getItemsFromHistoryWithKey:(NSString *)historyKey; 43 | - (PasteboardItem *)getLatestHistoryItem; 44 | - (UIImage *)getImageForItem:(PasteboardItem *)item; 45 | 46 | @end 47 | 48 | @interface SBApplication : NSObject 49 | @property(nonatomic, copy, readonly) NSString *bundleIdentifier; 50 | @end 51 | 52 | @interface UIApplication (Private) 53 | - (SBApplication *)_accessibilityFrontMostApplication; 54 | @end 55 | -------------------------------------------------------------------------------- /Manager/PasteboardManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // PasteboardManager.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "PasteboardManager.h" 9 | #import "AlertUtil.h" 10 | #import "ImageUtil.h" 11 | #import "NotificationKeys.h" 12 | #import "PasteboardItem.h" 13 | #import "PreferenceKeys.h" 14 | #import "StringUtil.h" 15 | 16 | #import 17 | 18 | @implementation PasteboardManager { 19 | dispatch_queue_t _queue; 20 | } 21 | 22 | /** 23 | * Creates the shared instance. 24 | */ 25 | + (instancetype)sharedInstance { 26 | static PasteboardManager *sharedInstance; 27 | static dispatch_once_t onceToken; 28 | dispatch_once(&onceToken, ^{ 29 | sharedInstance = [[PasteboardManager alloc] init]; 30 | }); 31 | return sharedInstance; 32 | } 33 | 34 | + (NSString *)historyPath { 35 | static NSString *kHistoryPath = nil; 36 | static dispatch_once_t onceToken; 37 | dispatch_once(&onceToken, ^{ 38 | kHistoryPath = JBROOT_PATH_NSSTRING(@"/var/mobile/Library/codes.aurora.kayoko/history.json"); 39 | }); 40 | return kHistoryPath; 41 | } 42 | 43 | + (NSString *)historyImagesPath { 44 | static NSString *kHistoryImagesPath = nil; 45 | static dispatch_once_t onceToken; 46 | dispatch_once(&onceToken, ^{ 47 | kHistoryImagesPath = JBROOT_PATH_NSSTRING(@"/var/mobile/Library/codes.aurora.kayoko/images/"); 48 | }); 49 | return kHistoryImagesPath; 50 | } 51 | 52 | + (NSBundle *)localizationBundle { 53 | static NSBundle *kLocalizationBundle = nil; 54 | static dispatch_once_t onceToken; 55 | dispatch_once(&onceToken, ^{ 56 | kLocalizationBundle = 57 | [NSBundle bundleWithPath:JBROOT_PATH_NSSTRING(@"/Library/PreferenceBundles/KayokoPreferences.bundle")]; 58 | }); 59 | return kLocalizationBundle; 60 | } 61 | 62 | /** 63 | * Creates the manager using the shared instance. 64 | */ 65 | - (instancetype)init { 66 | self = [super init]; 67 | if (self) { 68 | _fileManager = [NSFileManager defaultManager]; 69 | if (@available(iOS 15, *)) { 70 | [self prepareGeneralPasteboard]; 71 | } else { 72 | dispatch_async(dispatch_get_main_queue(), ^{ 73 | [self prepareGeneralPasteboard]; 74 | }); 75 | } 76 | } 77 | return self; 78 | } 79 | 80 | - (void)prepareGeneralPasteboard { 81 | _pasteboard = [UIPasteboard generalPasteboard]; 82 | _lastChangeCount = [_pasteboard changeCount]; 83 | } 84 | 85 | - (void)preparePasteboardQueue { 86 | if (@available(iOS 16, *)) { 87 | _queue = dispatch_queue_create("codes.aurora.kayoko.queue.pasteboard", DISPATCH_QUEUE_SERIAL); 88 | } 89 | } 90 | 91 | - (void)pullPasteboardChanges { 92 | if (@available(iOS 16, *)) { 93 | dispatch_async(_queue, ^{ 94 | [self _reallyPullPasteboardChanges]; 95 | }); 96 | } else { 97 | [self _reallyPullPasteboardChanges]; 98 | } 99 | } 100 | 101 | /** 102 | * Pulls new changes from the pasteboard. 103 | */ 104 | - (void)_reallyPullPasteboardChanges { 105 | // Return if the pasteboard is empty. 106 | if ([_pasteboard changeCount] == _lastChangeCount || (![_pasteboard hasStrings] && ![_pasteboard hasImages])) { 107 | return; 108 | } 109 | 110 | [self ensureResourcesExist]; 111 | 112 | if ([self saveText]) { 113 | // Don't pull strings if the pasteboard contains images. 114 | // For example: When copying an image from the web we only want the image, without the string. 115 | if (!([_pasteboard hasStrings] && [_pasteboard hasImages])) { 116 | for (NSString *string in [_pasteboard strings]) { 117 | @autoreleasepool { 118 | // The core only runs on the SpringBoard process, thus we can't use mainbundle to get the process' 119 | // bundle identifier. However, we can get it by using UIApplication/SpringBoard 120 | // front-most-application. 121 | SBApplication *frontMostApplication = 122 | [[UIApplication sharedApplication] _accessibilityFrontMostApplication]; 123 | PasteboardItem *item = 124 | [[PasteboardItem alloc] initWithBundleIdentifier:[frontMostApplication bundleIdentifier] 125 | andContent:string 126 | withImageNamed:nil]; 127 | [self addPasteboardItem:item toHistoryWithKey:kHistoryKeyHistory]; 128 | } 129 | } 130 | } 131 | } 132 | 133 | if ([self saveImages]) { 134 | for (UIImage *image in [_pasteboard images]) { 135 | @autoreleasepool { 136 | NSString *imageName = [StringUtil getRandomStringWithLength:32]; 137 | 138 | // Only save as PNG if the image has an alpha channel to save storage space. 139 | if ([ImageUtil imageHasAlpha:image]) { 140 | imageName = [imageName stringByAppendingString:@".png"]; 141 | NSString *filePath = 142 | [NSString stringWithFormat:@"%@/%@", [PasteboardManager historyImagesPath], imageName]; 143 | [UIImagePNGRepresentation([ImageUtil getRotatedImageFromImage:image]) writeToFile:filePath 144 | atomically:YES]; 145 | } else { 146 | imageName = [imageName stringByAppendingString:@".jpg"]; 147 | NSString *filePath = 148 | [NSString stringWithFormat:@"%@/%@", [PasteboardManager historyImagesPath], imageName]; 149 | [UIImageJPEGRepresentation(image, 1) writeToFile:filePath atomically:YES]; 150 | } 151 | 152 | // See the above loop. 153 | SBApplication *frontMostApplication = 154 | [[UIApplication sharedApplication] _accessibilityFrontMostApplication]; 155 | PasteboardItem *item = 156 | [[PasteboardItem alloc] initWithBundleIdentifier:[frontMostApplication bundleIdentifier] 157 | andContent:imageName 158 | withImageNamed:imageName]; 159 | [self addPasteboardItem:item toHistoryWithKey:kHistoryKeyHistory]; 160 | } 161 | } 162 | } 163 | 164 | _lastChangeCount = [_pasteboard changeCount]; 165 | } 166 | 167 | /** 168 | * Adds an item to a specified history. 169 | * 170 | * @param item The item to save. 171 | * @param historyKey The key for the history which to save to. 172 | */ 173 | - (void)addPasteboardItem:(PasteboardItem *)item toHistoryWithKey:(NSString *)historyKey { 174 | if ([[item content] isEqualToString:@""]) { 175 | return; 176 | } 177 | 178 | // Remove duplicates. 179 | [self removePasteboardItem:item fromHistoryWithKey:historyKey shouldRemoveImage:NO]; 180 | 181 | NSMutableDictionary *json = [self getJson]; 182 | NSMutableArray *history = [self getItemsFromHistoryWithKey:historyKey]; 183 | 184 | [history insertObject:@{ 185 | kItemKeyBundleIdentifier : [item bundleIdentifier] ?: @"com.apple.springboard", 186 | kItemKeyContent : [item content] ?: @"", 187 | kItemKeyImageName : [item imageName] ?: @"", 188 | kItemKeyHasLink : @([item hasLink]) 189 | } 190 | atIndex:0]; 191 | 192 | // Truncate the history corresponding the set limit. 193 | while ([history count] > [self maximumHistoryAmount]) { 194 | [history removeLastObject]; 195 | } 196 | 197 | json[historyKey] = history; 198 | 199 | [self setJsonFromDictionary:json]; 200 | } 201 | 202 | /** 203 | * Removes an item from a specified history. 204 | * 205 | * @param item The item to remove. 206 | * @param historyKey The key for the history from which to remove from. 207 | * @param shouldRemoveImage Whether to remove the item's corresponding image or not. 208 | */ 209 | - (void)removePasteboardItem:(PasteboardItem *)item 210 | fromHistoryWithKey:(NSString *)historyKey 211 | shouldRemoveImage:(BOOL)shouldRemoveImage { 212 | NSMutableDictionary *json = [self getJson]; 213 | NSMutableArray *history = json[historyKey]; 214 | 215 | for (NSDictionary *dictionary in history) { 216 | @autoreleasepool { 217 | PasteboardItem *historyItem = [PasteboardItem itemFromDictionary:dictionary]; 218 | 219 | if ([[historyItem content] isEqualToString:[item content]]) { 220 | [history removeObject:dictionary]; 221 | 222 | if (![[item imageName] isEqualToString:@""] && shouldRemoveImage) { 223 | NSString *filePath = 224 | [NSString stringWithFormat:@"%@/%@", [PasteboardManager historyImagesPath], [item imageName]]; 225 | [_fileManager removeItemAtPath:filePath error:nil]; 226 | } 227 | 228 | break; 229 | } 230 | } 231 | } 232 | 233 | json[historyKey] = history; 234 | 235 | [self setJsonFromDictionary:json]; 236 | } 237 | 238 | - (void)updatePasteboardWithItem:(PasteboardItem *)item 239 | fromHistoryWithKey:(NSString *)historyKey 240 | shouldAutoPaste:(BOOL)shouldAutoPaste { 241 | if (@available(iOS 16, *)) { 242 | dispatch_async(_queue, ^{ 243 | [self _reallyUpdatePasteboardWithItem:item fromHistoryWithKey:historyKey shouldAutoPaste:shouldAutoPaste]; 244 | }); 245 | } else { 246 | [self _reallyUpdatePasteboardWithItem:item fromHistoryWithKey:historyKey shouldAutoPaste:shouldAutoPaste]; 247 | } 248 | } 249 | 250 | /** 251 | * Updates the pasteboard with an item's content. 252 | * 253 | * @param item The item from which to set the content from. 254 | * @param historyKey The key for the history which the item is from. 255 | * @param shouldAutoPaste Whether the helper should automatically paste the new content. 256 | */ 257 | - (void)_reallyUpdatePasteboardWithItem:(PasteboardItem *)item 258 | fromHistoryWithKey:(NSString *)historyKey 259 | shouldAutoPaste:(BOOL)shouldAutoPaste { 260 | [_pasteboard setString:@""]; 261 | 262 | if (![[item imageName] isEqualToString:@""]) { 263 | NSString *filePath = 264 | [NSString stringWithFormat:@"%@/%@", [PasteboardManager historyImagesPath], [item imageName]]; 265 | UIImage *image = [UIImage imageWithContentsOfFile:filePath]; 266 | [_pasteboard setImage:image]; 267 | } else { 268 | [_pasteboard setString:[item content]]; 269 | } 270 | 271 | // The pasteboard updates with the given item, which triggers an update event. 272 | // Therefore we remove the given item to prevent duplicates. 273 | [_pasteboard changeCount]; 274 | [self removePasteboardItem:item fromHistoryWithKey:historyKey shouldRemoveImage:YES]; 275 | 276 | // Automatic paste should not occur for asynchronous operations. 277 | if ([self automaticallyPaste] && shouldAutoPaste) { 278 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 279 | (CFStringRef)kNotificationKeyHelperPaste, nil, nil, NO); 280 | } 281 | } 282 | 283 | /** 284 | * Returns all items from a specified history. 285 | * 286 | * @param historyKey The key for the history from which to get the items from. 287 | * 288 | * @return The history's items. 289 | */ 290 | - (NSMutableArray *)getItemsFromHistoryWithKey:(NSString *)historyKey { 291 | NSDictionary *json = [self getJson]; 292 | return json[historyKey] ?: [[NSMutableArray alloc] init]; 293 | } 294 | 295 | /** 296 | * Returns the latest item from the default history. 297 | * 298 | * @return The item. 299 | */ 300 | - (PasteboardItem *)getLatestHistoryItem { 301 | NSArray *history = [self getItemsFromHistoryWithKey:kHistoryKeyHistory]; 302 | return [PasteboardItem itemFromDictionary:[history firstObject] ?: nil]; 303 | } 304 | 305 | /** 306 | * Returns the image for an item. 307 | * 308 | * @param item The item from which to get the image from. 309 | * 310 | * @return The image. 311 | */ 312 | - (UIImage *)getImageForItem:(PasteboardItem *)item { 313 | NSData *imageData = [_fileManager 314 | contentsAtPath:[NSString stringWithFormat:@"%@/%@", [PasteboardManager historyImagesPath], [item imageName]]]; 315 | return [UIImage imageWithData:imageData]; 316 | } 317 | 318 | /** 319 | * Creates and returns a dictionary from the json containing the histories. 320 | * 321 | * @return The dictionary. 322 | */ 323 | - (NSMutableDictionary *)getJson { 324 | [self ensureResourcesExist]; 325 | 326 | NSData *jsonData = [NSData dataWithContentsOfFile:[PasteboardManager historyPath]]; 327 | NSMutableDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData 328 | options:NSJSONReadingMutableContainers 329 | error:nil]; 330 | 331 | return json; 332 | } 333 | 334 | /** 335 | * Stores the contents from a dictionary to a json file. 336 | * 337 | * @param dictionary The dictionary from which to save the contents from. 338 | */ 339 | - (void)setJsonFromDictionary:(NSMutableDictionary *)dictionary { 340 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:nil]; 341 | [jsonData writeToFile:[PasteboardManager historyPath] atomically:YES]; 342 | 343 | // Tell the core to reload the history view. 344 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 345 | (CFStringRef)kNotificationKeyCoreReload, nil, nil, YES); 346 | } 347 | 348 | /** 349 | * Creates the json for the histories and path for the images. 350 | */ 351 | - (void)ensureResourcesExist { 352 | BOOL isDirectory; 353 | if (![_fileManager fileExistsAtPath:[PasteboardManager historyImagesPath] isDirectory:&isDirectory]) { 354 | [_fileManager createDirectoryAtPath:[PasteboardManager historyImagesPath] 355 | withIntermediateDirectories:YES 356 | attributes:nil 357 | error:nil]; 358 | } 359 | 360 | if (![_fileManager fileExistsAtPath:[PasteboardManager historyPath]]) { 361 | NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[[NSMutableDictionary alloc] init] 362 | options:NSJSONWritingPrettyPrinted 363 | error:nil]; 364 | [jsonData writeToFile:[PasteboardManager historyPath] options:NSDataWritingAtomic error:nil]; 365 | } 366 | } 367 | 368 | @end 369 | -------------------------------------------------------------------------------- /Preferences/Cells/LinkCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // LinkCell.h 3 | // Akarii Utils 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface LinkCell : PSTableCell 12 | @property(nonatomic, strong) UILabel *label; 13 | @property(nonatomic, strong) UILabel *subtitleLabel; 14 | @property(nonatomic, strong) UIImageView *indicatorImageView; 15 | @property(nonatomic, strong) UIView *tapRecognizerView; 16 | @property(nonatomic, strong) UITapGestureRecognizer *tap; 17 | @property(nonatomic, copy) NSString *title; 18 | @property(nonatomic, copy) NSString *subtitle; 19 | @property(nonatomic, copy) NSString *url; 20 | @end 21 | -------------------------------------------------------------------------------- /Preferences/Cells/LinkCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // LinkCell.m 3 | // Akarii Utils 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "LinkCell.h" 9 | 10 | @implementation LinkCell 11 | 12 | /** 13 | * Initializes the link cell. 14 | * 15 | * @param style 16 | * @param reuseIdentifier 17 | * @param specifier 18 | * 19 | * @return The cell. 20 | */ 21 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 22 | reuseIdentifier:(NSString *)reuseIdentifier 23 | specifier:(PSSpecifier *)specifier { 24 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 25 | 26 | if (self) { 27 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 28 | 29 | [self setTitle:[bundle localizedStringForKey:[specifier propertyForKey:@"label"] value:nil table:@"Root"]]; 30 | [self setSubtitle:[bundle localizedStringForKey:[specifier propertyForKey:@"subtitle"] value:nil table:@"Root"]]; 31 | [self setUrl:[specifier propertyForKey:@"url"]]; 32 | 33 | [self setIndicatorImageView:[[UIImageView alloc] init]]; 34 | [[self indicatorImageView] setImage:[UIImage systemImageNamed:@"safari"]]; 35 | [[self indicatorImageView] setTintColor:[UIColor systemGrayColor]]; 36 | [self addSubview:[self indicatorImageView]]; 37 | 38 | [[self indicatorImageView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 39 | [NSLayoutConstraint activateConstraints:@[ 40 | [[[self indicatorImageView] widthAnchor] constraintEqualToConstant:20], 41 | [[[self indicatorImageView] heightAnchor] constraintEqualToConstant:20], 42 | [[[self indicatorImageView] centerYAnchor] constraintEqualToAnchor:[self centerYAnchor]], 43 | [[[self indicatorImageView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor] constant:-16] 44 | ]]; 45 | 46 | [self setLabel:[[UILabel alloc] init]]; 47 | [[self label] setText:[self title]]; 48 | [[self label] setFont:[UIFont systemFontOfSize:17]]; 49 | [[self label] setTextColor:[UIColor systemBlueColor]]; 50 | [self addSubview:[self label]]; 51 | 52 | [[self label] setTranslatesAutoresizingMaskIntoConstraints:NO]; 53 | [NSLayoutConstraint activateConstraints:@[ 54 | [[[self label] centerYAnchor] constraintEqualToAnchor:[self centerYAnchor] constant:-10], 55 | [[[self label] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor] constant:16], 56 | [[[self label] trailingAnchor] constraintEqualToAnchor:[[self indicatorImageView] leadingAnchor] 57 | constant:16] 58 | ]]; 59 | 60 | [self setSubtitleLabel:[[UILabel alloc] init]]; 61 | [[self subtitleLabel] setText:[NSString stringWithFormat:@"%@", [self subtitle]]]; 62 | [[self subtitleLabel] setFont:[UIFont systemFontOfSize:11]]; 63 | [[self subtitleLabel] setTextColor:[[UIColor labelColor] colorWithAlphaComponent:0.6]]; 64 | [self addSubview:[self subtitleLabel]]; 65 | 66 | [[self subtitleLabel] setTranslatesAutoresizingMaskIntoConstraints:NO]; 67 | [NSLayoutConstraint activateConstraints:@[ 68 | [[[self subtitleLabel] centerYAnchor] constraintEqualToAnchor:[self centerYAnchor] constant:10], 69 | [[[self subtitleLabel] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor] constant:16], 70 | [[[self subtitleLabel] trailingAnchor] constraintEqualToAnchor:[[self indicatorImageView] leadingAnchor] 71 | constant:-16] 72 | ]]; 73 | 74 | [self setTapRecognizerView:[[UIView alloc] init]]; 75 | [self addSubview:[self tapRecognizerView]]; 76 | 77 | [[self tapRecognizerView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 78 | [NSLayoutConstraint activateConstraints:@[ 79 | [[[self tapRecognizerView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 80 | [[[self tapRecognizerView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 81 | [[[self tapRecognizerView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 82 | [[[self tapRecognizerView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 83 | ]]; 84 | 85 | [self setTap:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openUrl)]]; 86 | [[self tapRecognizerView] addGestureRecognizer:[self tap]]; 87 | } 88 | 89 | return self; 90 | } 91 | 92 | /** 93 | * Opens the specified url. 94 | */ 95 | - (void)openUrl { 96 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[self url]] options:@{} completionHandler:nil]; 97 | } 98 | 99 | @end 100 | -------------------------------------------------------------------------------- /Preferences/Cells/SingleContactCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // SingleContactCell.h 3 | // Akarii Utils 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | #import 10 | 11 | @interface SingleContactCell : PSTableCell 12 | @property(nonatomic, strong) UIImageView *avatarImageView; 13 | @property(nonatomic, strong) UILabel *displayNameLabel; 14 | @property(nonatomic, strong) UILabel *usernameLabel; 15 | @property(nonatomic, strong) UIView *tapRecognizerView; 16 | @property(nonatomic, strong) UITapGestureRecognizer *tap; 17 | @property(nonatomic, copy) NSString *displayName; 18 | @property(nonatomic, copy) NSString *username; 19 | @end 20 | -------------------------------------------------------------------------------- /Preferences/Cells/SingleContactCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // SingleContactCell.m 3 | // Akarii Utils 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "SingleContactCell.h" 9 | #import 10 | 11 | @implementation SingleContactCell 12 | 13 | /** 14 | * Initializes the single contact cell. 15 | * 16 | * @param style 17 | * @param reuseIdentifier 18 | * @param specifier 19 | * 20 | * @return The cell. 21 | */ 22 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 23 | reuseIdentifier:(NSString *)reuseIdentifier 24 | specifier:(PSSpecifier *)specifier { 25 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 26 | 27 | if (self) { 28 | [self setDisplayName:[specifier propertyForKey:@"displayName"]]; 29 | [self setUsername:[specifier propertyForKey:@"username"]]; 30 | 31 | [self setAvatarImageView:[[UIImageView alloc] init]]; 32 | 33 | [self fetchAvatarWithCompletion:^(UIImage *avatar) { 34 | [UIView transitionWithView:[self avatarImageView] 35 | duration:0.2 36 | options:UIViewAnimationOptionTransitionCrossDissolve 37 | animations:^{ 38 | [[self avatarImageView] setImage:avatar]; 39 | } 40 | completion:nil]; 41 | }]; 42 | 43 | [[self avatarImageView] setContentMode:UIViewContentModeScaleAspectFill]; 44 | [[self avatarImageView] setClipsToBounds:YES]; 45 | [[[self avatarImageView] layer] setCornerRadius:21.5]; 46 | [[[self avatarImageView] layer] setBorderWidth:2]; 47 | [[[self avatarImageView] layer] setBorderColor:[[[UIColor labelColor] colorWithAlphaComponent:0.1] CGColor]]; 48 | [self addSubview:[self avatarImageView]]; 49 | 50 | [[self avatarImageView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 51 | [NSLayoutConstraint activateConstraints:@[ 52 | [[[self avatarImageView] centerYAnchor] constraintEqualToAnchor:[self centerYAnchor]], 53 | [[[self avatarImageView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor] constant:16], 54 | [[[self avatarImageView] widthAnchor] constraintEqualToConstant:43], 55 | [[[self avatarImageView] heightAnchor] constraintEqualToConstant:43] 56 | ]]; 57 | 58 | [self setDisplayNameLabel:[[UILabel alloc] init]]; 59 | [[self displayNameLabel] setText:[self displayName]]; 60 | [[self displayNameLabel] setFont:[UIFont systemFontOfSize:17 weight:UIFontWeightSemibold]]; 61 | [[self displayNameLabel] setTextColor:[UIColor labelColor]]; 62 | [self addSubview:[self displayNameLabel]]; 63 | 64 | [[self displayNameLabel] setTranslatesAutoresizingMaskIntoConstraints:NO]; 65 | [NSLayoutConstraint activateConstraints:@[ 66 | [[[self displayNameLabel] topAnchor] constraintEqualToAnchor:[[self avatarImageView] topAnchor] constant:4], 67 | [[[self displayNameLabel] leadingAnchor] constraintEqualToAnchor:[[self avatarImageView] trailingAnchor] 68 | constant:8], 69 | [[[self displayNameLabel] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor] constant:-16] 70 | ]]; 71 | 72 | [self setUsernameLabel:[[UILabel alloc] init]]; 73 | [[self usernameLabel] setText:[NSString stringWithFormat:@"@%@", [self username]]]; 74 | [[self usernameLabel] setFont:[UIFont systemFontOfSize:11 weight:UIFontWeightRegular]]; 75 | [[self usernameLabel] setTextColor:[UIColor secondaryLabelColor]]; 76 | [self addSubview:[self usernameLabel]]; 77 | 78 | [[self usernameLabel] setTranslatesAutoresizingMaskIntoConstraints:NO]; 79 | [NSLayoutConstraint activateConstraints:@[ 80 | [[[self usernameLabel] leadingAnchor] constraintEqualToAnchor:[[self avatarImageView] trailingAnchor] 81 | constant:8], 82 | [[[self usernameLabel] trailingAnchor] constraintEqualToAnchor:[[self displayNameLabel] trailingAnchor]], 83 | [[[self usernameLabel] bottomAnchor] constraintEqualToAnchor:[[self avatarImageView] bottomAnchor] 84 | constant:-4] 85 | ]]; 86 | 87 | [self setTapRecognizerView:[[UIView alloc] init]]; 88 | [self addSubview:[self tapRecognizerView]]; 89 | 90 | [[self tapRecognizerView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 91 | [NSLayoutConstraint activateConstraints:@[ 92 | [[[self tapRecognizerView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 93 | [[[self tapRecognizerView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 94 | [[[self tapRecognizerView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 95 | [[[self tapRecognizerView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 96 | ]]; 97 | 98 | [self setTap:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(openUserProfile)]]; 99 | [[self tapRecognizerView] addGestureRecognizer:[self tap]]; 100 | } 101 | 102 | return self; 103 | } 104 | 105 | /** 106 | * Fetches the url for the user's avatar. 107 | */ 108 | - (void)fetchAvatarUrlWithCompletion:(void (^)(NSURL *avatarUrl))completion { 109 | NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://github.com/%@.png", [self username]]]; 110 | completion(url); 111 | } 112 | 113 | /** 114 | * Fetches the user's avatar. 115 | */ 116 | - (void)fetchAvatarWithCompletion:(void (^)(UIImage *avatar))completion { 117 | static NSMutableDictionary *cachedAvatars = nil; 118 | if (self.username && cachedAvatars[self.username]) { 119 | completion(cachedAvatars[self.username]); 120 | return; 121 | } 122 | if (!cachedAvatars) { 123 | cachedAvatars = [NSMutableDictionary dictionary]; 124 | } 125 | [self fetchAvatarUrlWithCompletion:^(NSURL *avatarUrl) { 126 | if (avatarUrl) { 127 | dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ 128 | NSString *cacheDir = 129 | [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0]; 130 | NSString *avatarPath = [cacheDir 131 | stringByAppendingPathComponent:[NSString stringWithFormat:@"%@_%@.png", NSStringFromClass([self class]), 132 | [self username]]]; 133 | UIImage *avatar = nil; 134 | if (!avatar) { 135 | avatar = [UIImage imageWithContentsOfFile:avatarPath]; 136 | } 137 | if (!avatar) { 138 | NSData *imageData = [NSData dataWithContentsOfURL:avatarUrl]; 139 | if (imageData) { 140 | [imageData writeToFile:avatarPath atomically:YES]; 141 | } 142 | avatar = [UIImage imageWithData:imageData]; 143 | } 144 | dispatch_async(dispatch_get_main_queue(), ^{ 145 | if (self.username && avatar) { 146 | cachedAvatars[self.username] = avatar; 147 | } 148 | completion(avatar); 149 | }); 150 | }); 151 | } else { 152 | completion(nil); 153 | } 154 | }]; 155 | } 156 | 157 | /** 158 | * Opens the user's profile. 159 | */ 160 | - (void)openUserProfile { 161 | NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://github.com/%@", [self username]]]; 162 | [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; 163 | } 164 | 165 | @end 166 | -------------------------------------------------------------------------------- /Preferences/Controllers/KayokoCreditsListController.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoCreditsListController.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface KayokoCreditsListController : PSListController 11 | @end 12 | -------------------------------------------------------------------------------- /Preferences/Controllers/KayokoCreditsListController.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoCreditsListController.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoCreditsListController.h" 9 | 10 | @implementation KayokoCreditsListController 11 | @end 12 | -------------------------------------------------------------------------------- /Preferences/Controllers/KayokoListItemsController.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoListItemsController.h 3 | // Kayoko 4 | // 5 | // Created by 82Flex 6 | // 7 | 8 | #import 9 | 10 | @interface KayokoListItemsController : PSListItemsController 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Preferences/Controllers/KayokoListItemsController.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoListItemsController.m 3 | // Kayoko 4 | // 5 | // Created by 82Flex 6 | // 7 | 8 | #import "KayokoListItemsController.h" 9 | #import "../NotificationKeys.h" 10 | #import "../PreferenceKeys.h" 11 | #import 12 | 13 | @implementation KayokoListItemsController { 14 | NSMutableSet *_selectedIndices; 15 | ActivationMethod _currentOptions; 16 | } 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | 21 | if (!_selectedIndices) { 22 | _selectedIndices = [NSMutableSet set]; 23 | } 24 | 25 | // Read current configuration value 26 | id value = [self readPreferenceValue:self.specifier]; 27 | _currentOptions = [value integerValue]; 28 | if (_currentOptions == 0) { 29 | _currentOptions = kPreferenceKeyActivationMethodDefaultValue; 30 | } 31 | 32 | // Initialize selected indices 33 | [_selectedIndices removeAllObjects]; 34 | NSArray *validValues = [self.specifier propertyForKey:@"validValues"]; 35 | for (NSUInteger i = 0; i < validValues.count; i++) { 36 | NSNumber *value = validValues[i]; 37 | if (_currentOptions & [value integerValue]) { 38 | [_selectedIndices addObject:@(i)]; 39 | } 40 | } 41 | } 42 | 43 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 44 | [tableView deselectRowAtIndexPath:indexPath animated:YES]; 45 | 46 | NSUInteger selectedIndex = indexPath.row; 47 | 48 | // Check if this option is already selected 49 | NSNumber *indexNumber = @(selectedIndex); 50 | if ([_selectedIndices containsObject:indexNumber]) { 51 | // If this is the last selected item, don't allow deselection 52 | if (_selectedIndices.count > 1) { 53 | [_selectedIndices removeObject:indexNumber]; 54 | } else { 55 | // If only one option is selected, keep it selected 56 | [tableView reloadData]; 57 | return; 58 | } 59 | } else { 60 | [_selectedIndices addObject:indexNumber]; 61 | } 62 | 63 | // Update options 64 | ActivationMethod newOptions = 0; 65 | NSArray *validValues = [self.specifier propertyForKey:@"validValues"]; 66 | for (NSNumber *index in _selectedIndices) { 67 | NSNumber *value = validValues[[index integerValue]]; 68 | newOptions |= [value integerValue]; 69 | } 70 | 71 | _currentOptions = newOptions; 72 | [self setPreferenceValue:@(newOptions) specifier:self.specifier]; 73 | [(PSListController *)self.parentController reloadSpecifiers]; 74 | 75 | // Post notification 76 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 77 | (CFStringRef)kNotificationKeyPreferencesReload, nil, nil, YES); 78 | 79 | [tableView reloadData]; 80 | } 81 | 82 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 83 | UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; 84 | 85 | // Set selection mark 86 | if ([_selectedIndices containsObject:@(indexPath.row)]) { 87 | cell.accessoryType = UITableViewCellAccessoryCheckmark; 88 | } else { 89 | cell.accessoryType = UITableViewCellAccessoryNone; 90 | } 91 | 92 | return cell; 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /Preferences/Controllers/KayokoRootListController.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoRootListController.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface NSConcreteNotification : NSNotification 11 | @end 12 | 13 | @interface PSListController (Private) 14 | - (void)_returnKeyPressed:(NSConcreteNotification *)notification; 15 | @end 16 | 17 | @interface KayokoRootListController : PSListController 18 | @end 19 | 20 | @interface NSTask : NSObject 21 | @property(nonatomic, copy) NSArray *arguments; 22 | @property(nonatomic, copy) NSString *launchPath; 23 | - (void)launch; 24 | @end 25 | -------------------------------------------------------------------------------- /Preferences/Controllers/KayokoRootListController.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoRootListController.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoRootListController.h" 9 | 10 | #import 11 | #import 12 | 13 | #import 14 | 15 | #import "../NotificationKeys.h" 16 | #import "../PreferenceKeys.h" 17 | #import "PasteboardManager.h" 18 | 19 | @implementation KayokoRootListController 20 | 21 | /** 22 | * Loads the root specifiers. 23 | * 24 | * @return The specifiers. 25 | */ 26 | - (NSArray *)specifiers { 27 | if (!_specifiers) { 28 | _specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self]; 29 | } 30 | 31 | return _specifiers; 32 | } 33 | 34 | /** 35 | * Handles preference changes. 36 | * 37 | * @param value The new value for the changed option. 38 | * @param specifier The specifier that was interacted with. 39 | */ 40 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { 41 | [super setPreferenceValue:value specifier:specifier]; 42 | 43 | // Prompt to respring for options that require one to apply changes. 44 | if ([[specifier propertyForKey:@"key"] isEqualToString:kPreferenceKeyEnabled] || 45 | [[specifier propertyForKey:@"key"] isEqualToString:kPreferenceKeyActivationMethod] || 46 | [[specifier propertyForKey:@"key"] isEqualToString:kPreferenceKeyAutomaticallyPaste]) { 47 | [self promptToRespring]; 48 | } 49 | } 50 | 51 | /** 52 | * Hides the keyboard when the "Return" key is pressed on focused text fields. 53 | * 54 | * @param notification The event notification. 55 | */ 56 | - (void)_returnKeyPressed:(NSConcreteNotification *)notification { 57 | [[self view] endEditing:YES]; 58 | [super _returnKeyPressed:notification]; 59 | } 60 | 61 | /** 62 | * Prompts the user to respring to apply changes. 63 | */ 64 | - (void)promptToRespring { 65 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 66 | 67 | UIAlertController *resetAlert = [UIAlertController 68 | alertControllerWithTitle:[bundle localizedStringForKey:@"Kayoko" value:nil table:@"Root"] 69 | message:[bundle localizedStringForKey: 70 | @"This option requires a respring to apply. Do you want to respring now?" 71 | value:nil 72 | table:@"Root"] 73 | preferredStyle:UIAlertControllerStyleAlert]; 74 | 75 | UIAlertAction *yesAction = [UIAlertAction actionWithTitle:[bundle localizedStringForKey:@"Yes" 76 | value:nil 77 | table:@"Root"] 78 | style:UIAlertActionStyleDestructive 79 | handler:^(UIAlertAction *action) { 80 | [self respring]; 81 | }]; 82 | 83 | UIAlertAction *noAction = [UIAlertAction actionWithTitle:[bundle localizedStringForKey:@"No" 84 | value:nil 85 | table:@"Root"] 86 | style:UIAlertActionStyleCancel 87 | handler:nil]; 88 | 89 | [resetAlert addAction:yesAction]; 90 | [resetAlert addAction:noAction]; 91 | 92 | [self presentViewController:resetAlert animated:YES completion:nil]; 93 | } 94 | 95 | /** 96 | * Resprings the device. 97 | */ 98 | - (void)respring { 99 | NSTask *task = [[NSTask alloc] init]; 100 | [task setLaunchPath:JBROOT_PATH_NSSTRING(@"/usr/bin/killall")]; 101 | [task setArguments:@[ @"backboardd" ]]; 102 | [task launch]; 103 | } 104 | 105 | /** 106 | * Prompts the user to reset their preferences. 107 | */ 108 | - (void)resetPrompt { 109 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 110 | 111 | UIAlertController *resetAlert = [UIAlertController 112 | alertControllerWithTitle:[bundle localizedStringForKey:@"Kayoko" value:nil table:@"Root"] 113 | message:[bundle localizedStringForKey:@"Are you sure you want to reset your preferences?" 114 | value:nil 115 | table:@"Root"] 116 | preferredStyle:UIAlertControllerStyleAlert]; 117 | 118 | UIAlertAction *yesAction = [UIAlertAction actionWithTitle:[bundle localizedStringForKey:@"Yes" 119 | value:nil 120 | table:@"Root"] 121 | style:UIAlertActionStyleDestructive 122 | handler:^(UIAlertAction *action) { 123 | [self resetPreferences]; 124 | }]; 125 | 126 | UIAlertAction *noAction = [UIAlertAction actionWithTitle:[bundle localizedStringForKey:@"No" 127 | value:nil 128 | table:@"Root"] 129 | style:UIAlertActionStyleCancel 130 | handler:nil]; 131 | 132 | [resetAlert addAction:yesAction]; 133 | [resetAlert addAction:noAction]; 134 | 135 | [self presentViewController:resetAlert animated:YES completion:nil]; 136 | } 137 | 138 | /** 139 | * Resets the preferences. 140 | */ 141 | - (void)resetPreferences { 142 | NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kPreferencesIdentifier]; 143 | for (NSString *key in [userDefaults dictionaryRepresentation]) { 144 | [userDefaults removeObjectForKey:key]; 145 | } 146 | 147 | [self reloadSpecifiers]; 148 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 149 | (CFStringRef)kNotificationKeyPreferencesReload, nil, nil, YES); 150 | } 151 | 152 | - (UISlider *_Nullable)findSliderInView:(UIView *)view { 153 | if ([view isKindOfClass:[UISlider class]]) { 154 | return (UISlider *)view; 155 | } 156 | for (UIView *subview in view.subviews) { 157 | UISlider *slider = [self findSliderInView:subview]; 158 | if (slider) { 159 | return slider; 160 | } 161 | } 162 | return nil; 163 | } 164 | 165 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 166 | PSSpecifier *specifier = [self specifierAtIndexPath:indexPath]; 167 | NSString *key = [specifier propertyForKey:@"cell"]; 168 | if ([key isEqualToString:@"PSButtonCell"]) { 169 | UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; 170 | NSNumber *isDestructiveValue = [specifier propertyForKey:@"isDestructive"]; 171 | BOOL isDestructive = [isDestructiveValue boolValue]; 172 | cell.textLabel.textColor = isDestructive ? [UIColor systemRedColor] : [UIColor systemBlueColor]; 173 | cell.textLabel.highlightedTextColor = isDestructive ? [UIColor systemRedColor] : [UIColor systemBlueColor]; 174 | return cell; 175 | } 176 | if ([key isEqualToString:@"PSSliderCell"]) { 177 | UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; 178 | NSNumber *isContinuousValue = [specifier propertyForKey:@"isContinuous"]; 179 | BOOL isContinuous = [isContinuousValue boolValue]; 180 | UISlider *slider = [self findSliderInView:cell]; 181 | if (slider) { 182 | slider.continuous = isContinuous; 183 | } 184 | return cell; 185 | } 186 | if ([key isEqualToString:@"PSLinkListCell"]) { 187 | NSString *detail = [specifier propertyForKey:@"detail"]; 188 | if ([detail isEqualToString:@"KayokoListItemsController"]) { 189 | UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath]; 190 | NSBundle *bundle = [NSBundle bundleForClass:[self class]]; 191 | 192 | // Get the current activation methods 193 | NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName:kPreferencesIdentifier]; 194 | ActivationMethod currentOptions = [userDefaults integerForKey:kPreferenceKeyActivationMethod]; 195 | if (currentOptions == 0) { 196 | currentOptions = kPreferenceKeyActivationMethodDefaultValue; 197 | } 198 | 199 | // Get valid values and titles 200 | NSArray *validValues = [specifier propertyForKey:@"validValues"]; 201 | NSArray *validTitles = [specifier propertyForKey:@"validTitles"]; 202 | 203 | // Find selected options 204 | NSMutableArray *selectedTitles = [NSMutableArray array]; 205 | for (NSUInteger i = 0; i < validValues.count; i++) { 206 | NSNumber *value = validValues[i]; 207 | if (currentOptions & [value integerValue]) { 208 | [selectedTitles addObject:[bundle localizedStringForKey:validTitles[i] value:nil table:@"Root"]]; 209 | } 210 | } 211 | 212 | // Format the detail text based on the number of selected options 213 | NSString *detailText; 214 | if (selectedTitles.count == 1) { 215 | // Only one option - display its name 216 | detailText = selectedTitles[0]; 217 | } else if (selectedTitles.count == 2) { 218 | // Two options - display "Option A and Option B" 219 | NSString *format = [bundle localizedStringForKey:@"%@ and %@" value:nil table:@"Root"]; 220 | detailText = [NSString stringWithFormat:format, selectedTitles[0], selectedTitles[1]]; 221 | } else if (selectedTitles.count > 2) { 222 | // Three or more options - display "Option A and X others" 223 | NSString *format = [bundle localizedStringForKey:@"%@ and %d others" value:nil table:@"Root"]; 224 | detailText = [NSString stringWithFormat:format, selectedTitles[0], (int)selectedTitles.count - 1]; 225 | } else { 226 | // No options (shouldn't happen) 227 | detailText = @""; 228 | } 229 | 230 | cell.detailTextLabel.text = detailText; 231 | return cell; 232 | } 233 | } 234 | return [super tableView:tableView cellForRowAtIndexPath:indexPath]; 235 | } 236 | 237 | @end 238 | -------------------------------------------------------------------------------- /Preferences/Makefile: -------------------------------------------------------------------------------- 1 | BUNDLE_NAME += KayokoPreferences 2 | 3 | KayokoPreferences_FILES += $(wildcard Controllers/*.m Cells/*.m ../Manager/*.m) 4 | KayokoPreferences_FILES += $(wildcard ../Utils/*.m) 5 | 6 | ifeq ($(THEOS_PACKAGE_SCHEME),roothide) 7 | KayokoPreferences_FILES += ../libroot/dyn.c 8 | endif 9 | 10 | KayokoPreferences_CFLAGS += -fobjc-arc 11 | KayokoPreferences_CFLAGS += -I../Manager 12 | KayokoPreferences_CFLAGS += -I../Preferences 13 | KayokoPreferences_CFLAGS += -I../Utils 14 | 15 | KayokoPreferences_FRAMEWORKS += UIKit 16 | KayokoPreferences_PRIVATE_FRAMEWORKS += Preferences 17 | KayokoPreferences_INSTALL_PATH := /Library/PreferenceBundles 18 | 19 | include $(THEOS)/makefiles/common.mk 20 | include $(THEOS_MAKE_PATH)/bundle.mk -------------------------------------------------------------------------------- /Preferences/NotificationKeys.h: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationKeys.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | static NSString *const kNotificationKeyCoreShow = @"codes.aurora.kayoko.core.show"; 9 | static NSString *const kNotificationKeyCoreHide = @"codes.aurora.kayoko.core.hide"; 10 | static NSString *const kNotificationKeyCoreReload = @"codes.aurora.kayoko.core.reload"; 11 | static NSString *const kNotificationKeyHelperPaste = @"codes.aurora.kayoko.helper.paste"; 12 | static NSString *const kNotificationKeyPreferencesReload = @"codes.aurora.kayoko.preferences.reload"; 13 | static NSString *const kNotificationKeyPasteWillStart = @"codes.aurora.kayoko.paste.willstart"; 14 | -------------------------------------------------------------------------------- /Preferences/PreferenceKeys.h: -------------------------------------------------------------------------------- 1 | // 2 | // PreferenceKeys.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | typedef NS_OPTIONS(NSUInteger, ActivationMethod) { 11 | kActivationMethodPredictionBar = 1 << 0, 12 | kActivationMethodDictationKey = 1 << 1, 13 | kActivationMethodInputSwitcher = 1 << 2, 14 | kActivationMethodCalloutBar = 1 << 3 15 | }; 16 | 17 | static NSString *const kPreferencesIdentifier = @"codes.aurora.kayoko.preferences"; 18 | 19 | static NSString *const kPreferenceKeyEnabled = @"Enabled"; 20 | static NSString *const kPreferenceKeyMaximumHistoryAmount = @"MaximumHistoryAmount"; 21 | static NSString *const kPreferenceKeySaveText = @"SaveText"; 22 | static NSString *const kPreferenceKeySaveImages = @"SaveImages"; 23 | static NSString *const kPreferenceKeyActivationMethod = @"ActivationMethod"; 24 | static NSString *const kPreferenceKeyAutomaticallyPaste = @"AutomaticallyPaste"; 25 | static NSString *const kPreferenceKeyDisablePasteTips = @"DisablePasteTips"; 26 | static NSString *const kPreferenceKeyPlaySoundEffects = @"PlaySoundEffects"; 27 | static NSString *const kPreferenceKeyPlayHapticFeedback = @"PlayHapticFeedback"; 28 | static NSString *const kPreferenceKeyHeightInPoints = @"HeightInPoints"; 29 | 30 | static BOOL const kPreferenceKeyEnabledDefaultValue = YES; 31 | static NSUInteger const kPreferenceKeyMaximumHistoryAmountDefaultValue = 200; 32 | static BOOL const kPreferenceKeySaveTextDefaultValue = YES; 33 | static BOOL const kPreferenceKeySaveImagesDefaultValue = YES; 34 | static ActivationMethod const kPreferenceKeyActivationMethodDefaultValue = kActivationMethodPredictionBar; 35 | static BOOL const kPreferenceKeyAutomaticallyPasteDefaultValue = YES; 36 | static BOOL const kPreferenceKeyDisablePasteTipsDefaultValue = NO; 37 | static BOOL const kPreferenceKeyPlaySoundEffectsDefaultValue = YES; 38 | static BOOL const kPreferenceKeyPlayHapticFeedbackDefaultValue = YES; 39 | static CGFloat const kPreferenceKeyHeightInPointsDefaultValue = 420; 40 | -------------------------------------------------------------------------------- /Preferences/Resources/Copy.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preferences/Resources/Copy.aiff -------------------------------------------------------------------------------- /Preferences/Resources/Credits.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | footerText 11 | Direct and indirect Daemon assistance - uroboro and absidue 12 | 13 | 14 | cell 15 | PSGroupCell 16 | footerText 17 | Bug fixes and iOS 16 support - Lessica 18 | 19 | 20 | cell 21 | PSGroupCell 22 | footerText 23 | Testing - 0xilis, denial, flower and MrGcGamer 24 | 25 | 26 | title 27 | Credits 28 | 29 | 30 | -------------------------------------------------------------------------------- /Preferences/Resources/HLS_iPad_Universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preferences/Resources/HLS_iPad_Universal@3x.png -------------------------------------------------------------------------------- /Preferences/Resources/HLS_iPhone_Universal@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preferences/Resources/HLS_iPhone_Universal@3x.png -------------------------------------------------------------------------------- /Preferences/Resources/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preferences/Resources/Icon.png -------------------------------------------------------------------------------- /Preferences/Resources/Icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preferences/Resources/Icon@2x.png -------------------------------------------------------------------------------- /Preferences/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | KayokoPreferences 9 | CFBundleIdentifier 10 | codes.aurora.kayoko.preferences 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1.0 21 | NSPrincipalClass 22 | KayokoRootListController 23 | 24 | 25 | -------------------------------------------------------------------------------- /Preferences/Resources/Paste.aiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preferences/Resources/Paste.aiff -------------------------------------------------------------------------------- /Preferences/Resources/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | 11 | 12 | cellClass 13 | SingleContactCell 14 | displayName 15 | Alexandra 16 | username 17 | kaethchen 18 | height 19 | 58 20 | 21 | 22 | cellClass 23 | SingleContactCell 24 | displayName 25 | 82Flex 26 | username 27 | Lessica 28 | height 29 | 58 30 | 31 | 32 | cell 33 | PSSwitchCell 34 | default 35 | 36 | defaults 37 | codes.aurora.kayoko.preferences 38 | key 39 | Enabled 40 | label 41 | Enable Kayoko 42 | PostNotification 43 | codes.aurora.kayoko.preferences.reload 44 | 45 | 46 | 47 | cell 48 | PSGroupCell 49 | label 50 | History 51 | 52 | 53 | cell 54 | PSStaticTextCell 55 | label 56 | Maximum Amount 57 | 58 | 59 | cell 60 | PSSliderCell 61 | default 62 | 200 63 | defaults 64 | codes.aurora.kayoko.preferences 65 | key 66 | MaximumHistoryAmount 67 | min 68 | 50 69 | max 70 | 500 71 | showValue 72 | 73 | isSegmented 74 | 75 | segmentCount 76 | 9 77 | PostNotification 78 | codes.aurora.kayoko.preferences.reload 79 | 80 | 81 | cell 82 | PSSwitchCell 83 | default 84 | 85 | defaults 86 | codes.aurora.kayoko.preferences 87 | key 88 | SaveText 89 | label 90 | Save Text 91 | PostNotification 92 | codes.aurora.kayoko.preferences.reload 93 | 94 | 95 | cell 96 | PSSwitchCell 97 | default 98 | 99 | defaults 100 | codes.aurora.kayoko.preferences 101 | key 102 | SaveImages 103 | label 104 | Save Images 105 | PostNotification 106 | codes.aurora.kayoko.preferences.reload 107 | 108 | 109 | 110 | cell 111 | PSGroupCell 112 | label 113 | Behaviors 114 | footerText 115 | The “Prediction Bar” method displays custom suggestions on the “More Keyboard Keyplane” (123). 116 | 117 | 118 | cell 119 | PSLinkListCell 120 | default 121 | 1 122 | detail 123 | KayokoListItemsController 124 | defaults 125 | codes.aurora.kayoko.preferences 126 | label 127 | Activation Methods 128 | key 129 | ActivationMethod 130 | validValues 131 | 132 | 1 133 | 2 134 | 4 135 | 8 136 | 137 | validTitles 138 | 139 | Prediction Bar 140 | Dictation Key 141 | Input Switcher 142 | Edit Menu 143 | 144 | 145 | 146 | cell 147 | PSSwitchCell 148 | default 149 | 150 | defaults 151 | codes.aurora.kayoko.preferences 152 | key 153 | AutomaticallyPaste 154 | label 155 | Automatically Paste 156 | PostNotification 157 | codes.aurora.kayoko.preferences.reload 158 | 159 | 160 | cell 161 | PSSwitchCell 162 | default 163 | 164 | defaults 165 | codes.aurora.kayoko.preferences 166 | key 167 | DisablePasteTips 168 | label 169 | Disable Paste Tips 170 | PostNotification 171 | codes.aurora.kayoko.preferences.reload 172 | 173 | 174 | 175 | cell 176 | PSGroupCell 177 | label 178 | Feedbacks 179 | 180 | 181 | cell 182 | PSSwitchCell 183 | default 184 | 185 | defaults 186 | codes.aurora.kayoko.preferences 187 | key 188 | PlaySoundEffects 189 | label 190 | Play Sound Effects 191 | PostNotification 192 | codes.aurora.kayoko.preferences.reload 193 | 194 | 195 | cell 196 | PSSwitchCell 197 | default 198 | 199 | defaults 200 | codes.aurora.kayoko.preferences 201 | key 202 | PlayHapticFeedback 203 | label 204 | Play Haptic Feedback 205 | PostNotification 206 | codes.aurora.kayoko.preferences.reload 207 | 208 | 209 | 210 | cell 211 | PSGroupCell 212 | label 213 | Appearance 214 | 215 | 216 | cell 217 | PSStaticTextCell 218 | label 219 | Height (Points) 220 | 221 | 222 | cell 223 | PSSliderCell 224 | default 225 | 420 226 | defaults 227 | codes.aurora.kayoko.preferences 228 | key 229 | HeightInPoints 230 | min 231 | 200 232 | max 233 | 600 234 | showValue 235 | 236 | isContinuous 237 | 238 | isSegmented 239 | 240 | PostNotification 241 | codes.aurora.kayoko.preferences.reload 242 | 243 | 244 | 245 | cell 246 | PSGroupCell 247 | footerText 248 | Try out your changes by typing, copying and pasting text. 249 | 250 | 251 | cell 252 | PSEditTextCell 253 | placeholder 254 | Wishing on a star… 255 | 256 | 257 | 258 | cell 259 | PSGroupCell 260 | label 261 | Tools 262 | 263 | 264 | cell 265 | PSButtonCell 266 | label 267 | Respring 268 | action 269 | respring 270 | isDestructive 271 | 272 | 273 | 274 | cell 275 | PSButtonCell 276 | label 277 | Reset Preferences 278 | action 279 | resetPrompt 280 | isDestructive 281 | 282 | 283 | 284 | 285 | cell 286 | PSGroupCell 287 | footerText 288 | Thanks to you and everyone who has helped in any way. 289 | 290 | 291 | cell 292 | PSLinkCell 293 | label 294 | Credits 295 | detail 296 | KayokoCreditsListController 297 | 298 | 299 | 300 | cell 301 | PSGroupCell 302 | label 303 | Maintainer 304 | footerText 305 | Latest maintainer of Kayoko. 306 | 307 | 308 | cellClass 309 | LinkCell 310 | label 311 | Made with ♥ by OwnGoal Studio 312 | subtitle 313 | Please support our paid works, thank you! 314 | url 315 | https://havoc.app/search/82Flex 316 | height 317 | 54 318 | 319 | 320 | 321 | cell 322 | PSGroupCell 323 | label 324 | Original Author 325 | footerText 326 | Original author of Kayoko. 327 | 328 | 329 | cellClass 330 | LinkCell 331 | label 332 | Source Code 333 | subtitle 334 | Find it on my GitHub. 335 | url 336 | https://github.com/kaethchen/Kayoko 337 | height 338 | 54 339 | 340 | 341 | title 342 | Kayoko 343 | 344 | 345 | -------------------------------------------------------------------------------- /Preferences/Resources/zh-Hans.lproj/Credits.strings: -------------------------------------------------------------------------------- 1 | "Direct and indirect Daemon assistance - uroboro and absidue" = "直接和间接的守护进程协助 - uroboro 和 absidue"; 2 | "Bug fixes and iOS 16 support - Lessica" = "错误修复及 iOS 16 支持 - Lessica"; 3 | "Testing - 0xilis, denial, flower and MrGcGamer" = "测试 - 0xilis, denial, flower 和 MrGcGamer"; 4 | "Credits" = "致谢"; 5 | -------------------------------------------------------------------------------- /Preferences/Resources/zh-Hans.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | "CFBundleDisplayName" = "Kayoko"; 2 | -------------------------------------------------------------------------------- /Preferences/Resources/zh-Hans.lproj/Root.strings: -------------------------------------------------------------------------------- 1 | "Kayoko" = "Kayoko"; 2 | "Enable Kayoko" = "启用 Kayoko"; 3 | "Maximum Amount" = "最大数量"; 4 | "History" = "历史记录"; 5 | "Save Text" = "保存文本"; 6 | "Save Images" = "保存图像"; 7 | "Behaviors" = "行为"; 8 | "The “Prediction Bar” method displays custom suggestions on the “More Keyboard Keyplane” (123)." = "“预测栏” 方法在 “更多键盘 (123)” 上显示自定义建议。"; 9 | "Activation Method" = "激活方法"; 10 | "Activation Methods" = "激活方法"; 11 | "Prediction Bar" = "预测栏"; 12 | "Dictation Key" = "听写键"; 13 | "Input Switcher" = "输入法菜单"; 14 | "Edit Menu" = "编辑菜单"; 15 | "Automatically Paste" = "自动粘贴"; 16 | "Disable Paste Tips" = "禁用粘贴提示"; 17 | "Feedbacks" = "反馈"; 18 | "Play Sound Effects" = "播放声音效果"; 19 | "Play Haptic Feedback" = "播放触觉反馈"; 20 | "Appearance" = "外观"; 21 | "Height (Points)" = "高度"; 22 | "Try out your changes by typing, copying and pasting text." = "尝试输入、拷贝和粘贴文本以查看效果。"; 23 | "Wishing on a star…" = "在星星上许个愿…"; 24 | "Tools" = "工具"; 25 | "Respring" = "妙手回春"; 26 | "Reset Preferences" = "还原设置"; 27 | "Thanks to you and everyone who has helped in any way." = "感谢你和所有以任何方式提供帮助的人。"; 28 | "Credits" = "致谢"; 29 | "My Tweaks" = "我的插件"; 30 | "Check out my other work." = "查看我的其他作品。"; 31 | "Source Code" = "源代码"; 32 | "Find it on my GitHub." = "在我的 GitHub 上找到它。"; 33 | "Donate" = "捐赠"; 34 | "Let me know that you like what I do." = "让我知道你喜欢我所做的事情。"; 35 | "This option requires a respring to apply. Do you want to respring now?" = "此选项需要重新启动才能生效。你现在要重新启动吗?"; 36 | "Are you sure you want to reset your preferences?" = "你确定要还原设置吗?"; 37 | "Yes" = "是"; 38 | "No" = "否"; 39 | "Maintainer" = "维护者"; 40 | "Latest maintainer of Kayoko." = "Kayoko 的最新维护者。"; 41 | "Made with ♥ by OwnGoal Studio" = "「乌龙工作室」倾情献制"; 42 | "Please support our paid works, thank you!" = "请支持我们的其他付费作品,谢谢!"; 43 | "Original Author" = "原作者"; 44 | "Original author of Kayoko." = "Kayoko 的原作者。"; 45 | "%@ and %@" = "%@和%@"; 46 | "%@ and %d others" = "%@和其他 %d 个"; 47 | -------------------------------------------------------------------------------- /Preferences/Resources/zh-Hans.lproj/Tweak.strings: -------------------------------------------------------------------------------- 1 | "Kayoko" = "Kayoko"; 2 | "History" = "历史记录"; 3 | "history" = "历史记录"; 4 | "Favorites" = "收藏夹"; 5 | "favorites" = "收藏夹"; 6 | "Preview" = "预览"; 7 | "This will clear your %@." = "这将清除你的 %@。"; 8 | "Yes" = "是"; 9 | "No" = "否"; 10 | "Copy" = "拷贝"; 11 | "Paste" = "粘贴"; 12 | "SpringBoard" = "主屏幕"; 13 | -------------------------------------------------------------------------------- /Preferences/layout/Library/PreferenceLoader/Preferences/KayokoPreferences/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | entry 6 | 7 | bundle 8 | KayokoPreferences 9 | cell 10 | PSLinkCell 11 | detail 12 | KayokoRootListController 13 | icon 14 | Icon.png 15 | isController 16 | 17 | label 18 | Kayoko 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OwnGoalStudio/Kayoko/611c935791de9111efd1db2eaf1cddfb2041dd61/Preview.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kayoko (iOS 16) 2 | 3 | Feature rich clipboard manager for iOS. 4 | For iPhone running iOS 15/16 with Dopamine jailbreak. 5 | 6 | ## Preview 7 | 8 | Preview 9 | 10 | ## Installation 11 | 12 | 1. Download the latest `deb` from the [releases](https://github.com/Lessica/Kayoko/releases). 13 | 2. Install the `deb` using your preferred method. 14 | 15 | ## License 16 | 17 | [GPLv3](https://github.com/Lessica/Kayoko/blob/main/COPYING) 18 | -------------------------------------------------------------------------------- /Tweak/Core/KayokoCore.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoCore.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @class KayokoView; 11 | 12 | OBJC_EXTERN KayokoView *kayokoView; 13 | 14 | OBJC_EXTERN NSUserDefaults *kayokoPreferences; 15 | OBJC_EXTERN BOOL kayokoPrefsEnabled; 16 | OBJC_EXTERN NSUInteger kayokoHelperPrefsActivationMethod; 17 | 18 | OBJC_EXTERN NSUInteger kayokoPrefsMaximumHistoryAmount; 19 | OBJC_EXTERN BOOL kayokoPrefsSaveText; 20 | OBJC_EXTERN BOOL kayokoPrefsSaveImages; 21 | OBJC_EXTERN BOOL kayokoPrefsAutomaticallyPaste; 22 | OBJC_EXTERN BOOL kayokoPrefsDisablePasteTips; 23 | OBJC_EXTERN BOOL kayokoPrefsPlaySoundEffects; 24 | OBJC_EXTERN BOOL kayokoPrefsPlayHapticFeedback; 25 | OBJC_EXTERN CGFloat kayokoPrefsHeightInPoints; 26 | 27 | OBJC_EXTERN void EnableKayokoDisablePasteTips(void); 28 | 29 | @interface UIStatusBarWindow : UIWindow 30 | @end 31 | -------------------------------------------------------------------------------- /Tweak/Core/KayokoCore.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoCore.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoCore.h" 9 | 10 | #import 11 | #import 12 | #import 13 | 14 | #import 15 | #import 16 | #import 17 | 18 | #import "NotificationKeys.h" 19 | #import "PasteboardManager.h" 20 | #import "PreferenceKeys.h" 21 | #import "Views/KayokoView.h" 22 | 23 | #define kMinimumFeedbackInterval 0.6 24 | 25 | KayokoView *kayokoView = nil; 26 | 27 | NSUserDefaults *kayokoPreferences = nil; 28 | BOOL kayokoPrefsEnabled = NO; 29 | NSUInteger kayokoHelperPrefsActivationMethod = 0; 30 | 31 | NSUInteger kayokoPrefsMaximumHistoryAmount = 0; 32 | BOOL kayokoPrefsSaveText = NO; 33 | BOOL kayokoPrefsSaveImages = NO; 34 | BOOL kayokoPrefsAutomaticallyPaste = NO; 35 | BOOL kayokoPrefsDisablePasteTips = NO; 36 | BOOL kayokoPrefsPlaySoundEffects = NO; 37 | BOOL kayokoPrefsPlayHapticFeedback = NO; 38 | 39 | CGFloat kayokoPrefsHeightInPoints = 420; 40 | 41 | static BOOL isInPasteProgress = NO; 42 | 43 | static NSTimeInterval lastPasteFeedbackOccurred = 0; 44 | static NSTimeInterval lastCopyFeedbackOccurred = 0; 45 | 46 | @interface UIStatusBarStyleRequest : NSObject 47 | @property(nonatomic, assign, readonly) long long style; 48 | @end 49 | 50 | @interface SBStatusBarManager : NSObject 51 | + (instancetype)sharedInstance; 52 | - (UIStatusBarStyleRequest *)frontmostStatusBarStyleRequest; 53 | @end 54 | 55 | @interface SBWindowSceneStatusBarManager : NSObject 56 | + (instancetype)windowSceneStatusBarManagerForEmbeddedDisplay; 57 | - (UIStatusBarStyleRequest *)frontmostStatusBarStyleRequest; 58 | @end 59 | 60 | #pragma mark - UIStatusBarWindow class hooks 61 | 62 | /** 63 | * Sets up the history view. 64 | * 65 | * Using the status bar's window is hacky, yet it's present on SpringBoard and in apps. 66 | * It's important to note that it runs on the SpringBoard process too, which gives us file system read/write. 67 | * 68 | * @param frame 69 | */ 70 | static void (*orig_UIStatusBarWindow_initWithFrame)(UIStatusBarWindow *self, SEL _cmd, CGRect frame); 71 | static void override_UIStatusBarWindow_initWithFrame(UIStatusBarWindow *self, SEL _cmd, CGRect frame) { 72 | orig_UIStatusBarWindow_initWithFrame(self, _cmd, frame); 73 | 74 | if (!kayokoView) { 75 | CGRect bounds = [[UIScreen mainScreen] bounds]; 76 | kayokoView = [[KayokoView alloc] initWithFrame:CGRectMake(0, bounds.size.height - kayokoPrefsHeightInPoints, 77 | bounds.size.width, kayokoPrefsHeightInPoints)]; 78 | [kayokoView setAutomaticallyPaste:kayokoPrefsAutomaticallyPaste]; 79 | [kayokoView setHidden:YES]; 80 | [self addSubview:kayokoView]; 81 | } 82 | } 83 | 84 | #pragma mark - Notification callbacks 85 | 86 | static void kayokoPasteWillStart() { isInPasteProgress = YES; } 87 | 88 | /** 89 | * Receives the notification that the pasteboard changed from the daemon and pulls the new changes. 90 | */ 91 | static void _kayokoCopy() { 92 | [[PasteboardManager sharedInstance] pullPasteboardChanges]; 93 | if (isInPasteProgress) { 94 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 95 | isInPasteProgress = NO; 96 | }); 97 | return; 98 | } 99 | NSTimeInterval now = CACurrentMediaTime(); 100 | if (fabs(now - lastCopyFeedbackOccurred) < kMinimumFeedbackInterval) { 101 | return; 102 | } 103 | lastCopyFeedbackOccurred = now; 104 | if (kayokoPrefsPlaySoundEffects) { 105 | static dispatch_once_t onceToken; 106 | static SystemSoundID soundID; 107 | dispatch_once(&onceToken, ^{ 108 | AudioServicesCreateSystemSoundID( 109 | (__bridge CFURLRef) 110 | [NSURL fileURLWithPath:JBROOT_PATH_NSSTRING( 111 | @"/Library/PreferenceBundles/KayokoPreferences.bundle/Copy.aiff")], 112 | &soundID); 113 | }); 114 | AudioServicesPlaySystemSound(soundID); 115 | } 116 | if (kayokoPrefsPlayHapticFeedback) { 117 | AudioServicesPlaySystemSound(1519); 118 | } 119 | } 120 | 121 | static void kayokoCopy() { 122 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 123 | _kayokoCopy(); 124 | }); 125 | } 126 | 127 | /** 128 | * Shows the history. 129 | */ 130 | static void show() { 131 | if ([kayokoView isHidden]) { 132 | 133 | [kayokoView setOverrideUserInterfaceStyle:UIUserInterfaceStyleUnspecified]; 134 | 135 | /* iOS 15 */ 136 | SBStatusBarManager *statusBarManager = [objc_getClass("SBStatusBarManager") sharedInstance]; 137 | if (statusBarManager) { 138 | UIStatusBarStyleRequest *styleRequest = [statusBarManager frontmostStatusBarStyleRequest]; 139 | if (styleRequest) { 140 | long long style = [styleRequest style]; 141 | BOOL isKindOfDark = style == 1; 142 | if (isKindOfDark) { 143 | [kayokoView setOverrideUserInterfaceStyle:UIUserInterfaceStyleDark]; 144 | } else { 145 | [kayokoView setOverrideUserInterfaceStyle:UIUserInterfaceStyleLight]; 146 | } 147 | } 148 | } 149 | 150 | /* iOS 16 */ 151 | SBWindowSceneStatusBarManager *windowSceneStatusBarManager = 152 | [objc_getClass("SBWindowSceneStatusBarManager") windowSceneStatusBarManagerForEmbeddedDisplay]; 153 | if (windowSceneStatusBarManager) { 154 | UIStatusBarStyleRequest *styleRequest = [windowSceneStatusBarManager frontmostStatusBarStyleRequest]; 155 | if (styleRequest) { 156 | long long style = [styleRequest style]; 157 | BOOL isKindOfDark = style == 1; 158 | if (isKindOfDark) { 159 | [kayokoView setOverrideUserInterfaceStyle:UIUserInterfaceStyleDark]; 160 | } else { 161 | [kayokoView setOverrideUserInterfaceStyle:UIUserInterfaceStyleLight]; 162 | } 163 | } 164 | } 165 | 166 | [kayokoView show]; 167 | 168 | if (kayokoPrefsPlayHapticFeedback && (kayokoHelperPrefsActivationMethod & kActivationMethodDictationKey)) { 169 | AudioServicesPlaySystemSound(1519); 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * Hides the history. 176 | */ 177 | static void hide() { 178 | if (![kayokoView isHidden]) { 179 | [kayokoView hide]; 180 | } 181 | } 182 | 183 | /** 184 | * Reloads the history. 185 | */ 186 | static void reload() { 187 | if (![kayokoView isHidden]) { 188 | [kayokoView reload]; 189 | } 190 | } 191 | 192 | #pragma mark - Preferences 193 | 194 | /** 195 | * Loads the user's preferences. 196 | */ 197 | static void load_preferences() { 198 | kayokoPreferences = [[NSUserDefaults alloc] initWithSuiteName:kPreferencesIdentifier]; 199 | 200 | [kayokoPreferences registerDefaults:@{ 201 | kPreferenceKeyEnabled : @(kPreferenceKeyEnabledDefaultValue), 202 | kPreferenceKeyActivationMethod : @(kPreferenceKeyActivationMethodDefaultValue), 203 | kPreferenceKeyMaximumHistoryAmount : @(kPreferenceKeyMaximumHistoryAmountDefaultValue), 204 | kPreferenceKeySaveText : @(kPreferenceKeySaveTextDefaultValue), 205 | kPreferenceKeySaveImages : @(kPreferenceKeySaveImagesDefaultValue), 206 | kPreferenceKeyAutomaticallyPaste : @(kPreferenceKeyAutomaticallyPasteDefaultValue), 207 | kPreferenceKeyDisablePasteTips : @(kPreferenceKeyDisablePasteTipsDefaultValue), 208 | kPreferenceKeyPlaySoundEffects : @(kPreferenceKeyPlaySoundEffectsDefaultValue), 209 | kPreferenceKeyPlayHapticFeedback : @(kPreferenceKeyPlayHapticFeedbackDefaultValue), 210 | kPreferenceKeyHeightInPoints : @(kPreferenceKeyHeightInPointsDefaultValue), 211 | }]; 212 | 213 | kayokoPrefsEnabled = [[kayokoPreferences objectForKey:kPreferenceKeyEnabled] boolValue]; 214 | kayokoHelperPrefsActivationMethod = 215 | [[kayokoPreferences objectForKey:kPreferenceKeyActivationMethod] unsignedIntegerValue]; 216 | kayokoPrefsMaximumHistoryAmount = 217 | [[kayokoPreferences objectForKey:kPreferenceKeyMaximumHistoryAmount] unsignedIntegerValue]; 218 | kayokoPrefsSaveText = [[kayokoPreferences objectForKey:kPreferenceKeySaveText] boolValue]; 219 | kayokoPrefsSaveImages = [[kayokoPreferences objectForKey:kPreferenceKeySaveImages] boolValue]; 220 | kayokoPrefsAutomaticallyPaste = [[kayokoPreferences objectForKey:kPreferenceKeyAutomaticallyPaste] boolValue]; 221 | kayokoPrefsDisablePasteTips = [[kayokoPreferences objectForKey:kPreferenceKeyDisablePasteTips] boolValue]; 222 | kayokoPrefsPlaySoundEffects = [[kayokoPreferences objectForKey:kPreferenceKeyPlaySoundEffects] boolValue]; 223 | kayokoPrefsPlayHapticFeedback = [[kayokoPreferences objectForKey:kPreferenceKeyPlayHapticFeedback] boolValue]; 224 | kayokoPrefsHeightInPoints = [[kayokoPreferences objectForKey:kPreferenceKeyHeightInPoints] doubleValue]; 225 | 226 | [[PasteboardManager sharedInstance] preparePasteboardQueue]; 227 | [[PasteboardManager sharedInstance] setMaximumHistoryAmount:kayokoPrefsMaximumHistoryAmount]; 228 | [[PasteboardManager sharedInstance] setSaveText:kayokoPrefsSaveText]; 229 | [[PasteboardManager sharedInstance] setSaveImages:kayokoPrefsSaveImages]; 230 | [[PasteboardManager sharedInstance] setAutomaticallyPaste:kayokoPrefsAutomaticallyPaste]; 231 | 232 | if (kayokoView) { 233 | [kayokoView setAutomaticallyPaste:kayokoPrefsAutomaticallyPaste]; 234 | [kayokoView setShouldPlayFeedback:kayokoPrefsPlayHapticFeedback]; 235 | CGRect bounds = [[UIScreen mainScreen] bounds]; 236 | CGRect newFrame = 237 | CGRectMake(0, bounds.size.height - kayokoPrefsHeightInPoints, bounds.size.width, kayokoPrefsHeightInPoints); 238 | [kayokoView setFrame:newFrame]; 239 | } 240 | } 241 | 242 | #pragma mark - Sound effects 243 | 244 | static void kayokoPaste() { 245 | NSTimeInterval now = CACurrentMediaTime(); 246 | if (fabs(now - lastPasteFeedbackOccurred) < kMinimumFeedbackInterval) { 247 | return; 248 | } 249 | lastPasteFeedbackOccurred = now; 250 | if (kayokoPrefsPlaySoundEffects) { 251 | static dispatch_once_t onceToken; 252 | static SystemSoundID soundID; 253 | dispatch_once(&onceToken, ^{ 254 | AudioServicesCreateSystemSoundID( 255 | (__bridge CFURLRef) 256 | [NSURL fileURLWithPath:JBROOT_PATH_NSSTRING( 257 | @"/Library/PreferenceBundles/KayokoPreferences.bundle/Paste.aiff")], 258 | &soundID); 259 | }); 260 | AudioServicesPlaySystemSound(soundID); 261 | } 262 | if (kayokoPrefsPlayHapticFeedback) { 263 | AudioServicesPlaySystemSound(1519); 264 | } 265 | } 266 | 267 | #pragma mark - Constructor 268 | 269 | /** 270 | * Initializes the core. 271 | * 272 | * First it loads the preferences and continues if Kayoko is enabled. 273 | * Secondly it sets up the hooks. 274 | * Finally it registers the notification callbacks. 275 | */ 276 | __attribute((constructor)) static void initialize() { 277 | NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; 278 | BOOL isSpringBoard = [bundleIdentifier isEqualToString:@"com.apple.springboard"]; 279 | if (isSpringBoard) { 280 | load_preferences(); 281 | 282 | if (!kayokoPrefsEnabled) { 283 | return; 284 | } 285 | 286 | EnableKayokoDisablePasteTips(); 287 | 288 | MSHookMessageEx(objc_getClass("UIStatusBarWindow"), @selector(initWithFrame:), 289 | (IMP)&override_UIStatusBarWindow_initWithFrame, (IMP *)&orig_UIStatusBarWindow_initWithFrame); 290 | 291 | CFNotificationCenterAddObserver( 292 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)kayokoCopy, 293 | CFSTR("com.apple.pasteboard.notify.changed"), NULL, 294 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 295 | CFNotificationCenterAddObserver( 296 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)show, 297 | (CFStringRef)kNotificationKeyCoreShow, NULL, 298 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 299 | CFNotificationCenterAddObserver( 300 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)hide, 301 | (CFStringRef)kNotificationKeyCoreHide, NULL, 302 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 303 | CFNotificationCenterAddObserver( 304 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)reload, 305 | (CFStringRef)kNotificationKeyCoreReload, NULL, 306 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 307 | CFNotificationCenterAddObserver( 308 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)load_preferences, 309 | (CFStringRef)kNotificationKeyPreferencesReload, NULL, 310 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 311 | CFNotificationCenterAddObserver( 312 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)kayokoPaste, 313 | (CFStringRef)kNotificationKeyHelperPaste, NULL, 314 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 315 | CFNotificationCenterAddObserver( 316 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)kayokoPasteWillStart, 317 | (CFStringRef)kNotificationKeyPasteWillStart, NULL, 318 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 319 | 320 | return; 321 | } 322 | 323 | NSArray *args = [[NSProcessInfo processInfo] arguments]; 324 | NSString *processName = [[NSProcessInfo processInfo] processName]; 325 | NSString *executablePath = [args firstObject]; 326 | BOOL isDruidOrPasted = 327 | ([executablePath hasPrefix:@"/System/Library/"] || [executablePath hasPrefix:@"/usr/libexec/"]) && 328 | ([processName isEqualToString:@"druid"] || [processName isEqualToString:@"pasted"]); 329 | if (isDruidOrPasted) { 330 | load_preferences(); 331 | 332 | if (!kayokoPrefsEnabled) { 333 | return; 334 | } 335 | 336 | EnableKayokoDisablePasteTips(); 337 | CFNotificationCenterAddObserver( 338 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)load_preferences, 339 | (CFStringRef)kNotificationKeyPreferencesReload, NULL, 340 | (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDeliverImmediately); 341 | 342 | return; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Tweak/Core/KayokoCore.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.springboard" ); Executables = ( "druid", "pasted" ); }; } 2 | -------------------------------------------------------------------------------- /Tweak/Core/KayokoCoreLogos.xm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @import Foundation; 5 | @import UIKit; 6 | 7 | #import "KayokoCore.h" 8 | #import "NotificationKeys.h" 9 | 10 | @interface PBCFUserNotificationPasteAnnouncer : NSObject 11 | - (void)authorizationDidCompleteWithPasteAllowed:(BOOL)arg1; 12 | - (void)requestAuthorizationForPaste:(id)arg1 replyHandler:(id)arg2; 13 | - (void)announcePaste:(id)arg1 replyHandler:(id)arg2; 14 | @end 15 | 16 | @interface SBUserNotificationAlert : NSObject 17 | - (void)_setActivated:(BOOL)activated; 18 | - (void)_sendResponseAndCleanUp:(BOOL)cleanup; 19 | @end 20 | 21 | %group DruidUI 22 | 23 | %hook DRPasteAnnouncer 24 | 25 | - (void)announceDeniedPaste { 26 | if (kayokoPrefsDisablePasteTips) return; 27 | %orig; 28 | } 29 | 30 | - (void)announcePaste:(id)arg1 { 31 | if (kayokoPrefsDisablePasteTips) return; 32 | %orig; 33 | } 34 | 35 | %end 36 | 37 | %end // DruidUI 38 | 39 | %group Pasteboard 40 | 41 | %hook PBDruidRemotePasteAnnouncer 42 | 43 | + (void)announceDeniedPaste { 44 | if (kayokoPrefsDisablePasteTips) return; 45 | %orig; 46 | } 47 | 48 | + (void)announcePaste:(id)arg1 { 49 | if (kayokoPrefsDisablePasteTips) return; 50 | %orig; 51 | } 52 | 53 | %end 54 | 55 | %hook PBCFUserNotificationPasteAnnouncer 56 | 57 | + (void)announceDeniedPaste { 58 | if (kayokoPrefsDisablePasteTips) return; 59 | %orig; 60 | } 61 | 62 | + (void)announcePaste:(id)arg1 { 63 | if (kayokoPrefsDisablePasteTips) return; 64 | %orig; 65 | } 66 | 67 | - (void)requestAuthorizationForPaste:(id)arg1 replyHandler:(void(^)(BOOL))reply { 68 | reply(YES); 69 | [self authorizationDidCompleteWithPasteAllowed:YES]; 70 | } 71 | 72 | - (void)announcePaste:(id)arg1 replyHandler:(void(^)(BOOL))reply { 73 | reply(YES); 74 | [self authorizationDidCompleteWithPasteAllowed:YES]; 75 | } 76 | 77 | %end 78 | 79 | %end // Pasteboard 80 | 81 | %group NoPasteAlerts16 82 | 83 | %hook SBAlertItem 84 | 85 | + (void)activateAlertItem:(id)arg1 { 86 | id alertItem = arg1; 87 | if ([alertItem isKindOfClass:NSClassFromString(@"SBUserNotificationAlert")]) { 88 | NSString *str = MSHookIvar(alertItem, "_alertSource"); 89 | if ([str isEqualToString:@"pasted"]) { 90 | [alertItem _setActivated:NO]; 91 | if ([alertItem respondsToSelector:@selector(_sendResponseAndCleanUp:)]) { 92 | [alertItem _sendResponseAndCleanUp:YES]; 93 | } 94 | return; 95 | } 96 | } 97 | %orig(alertItem); 98 | } 99 | 100 | %end 101 | 102 | %end // NoPasteAlerts16 103 | 104 | void EnableKayokoDisablePasteTips(void) { 105 | %init(DruidUI); 106 | if (@available(iOS 16, *)) { 107 | %init(Pasteboard); 108 | %init(NoPasteAlerts16); 109 | } 110 | } -------------------------------------------------------------------------------- /Tweak/Core/Makefile: -------------------------------------------------------------------------------- 1 | TWEAK_NAME := KayokoCore 2 | 3 | KayokoCore_FILES += KayokoCore.m 4 | KayokoCore_FILES += KayokoCoreLogos.xm 5 | KayokoCore_FILES += $(wildcard ../../Manager/*.m Views/*.m ../../Utils/*.m) 6 | 7 | ifeq ($(THEOS_PACKAGE_SCHEME),roothide) 8 | KayokoCore_FILES += ../../libroot/dyn.c 9 | endif 10 | 11 | KayokoCore_CFLAGS += -fobjc-arc 12 | KayokoCore_CFLAGS += -I../../Manager 13 | KayokoCore_CFLAGS += -I../../Preferences 14 | KayokoCore_CFLAGS += -I../../Utils 15 | 16 | KayokoCore_FRAMEWORKS += UIKit AudioToolbox 17 | 18 | include $(THEOS)/makefiles/common.mk 19 | include $(THEOS_MAKE_PATH)/aggregate.mk 20 | include $(THEOS_MAKE_PATH)/tweak.mk 21 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoFavoritesTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoFavoritesTableView.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoTableView.h" 9 | 10 | @interface KayokoFavoritesTableView : KayokoTableView 11 | @end 12 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoFavoritesTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoFavoritesTableView.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoFavoritesTableView.h" 9 | #import "PasteboardItem.h" 10 | #import "PasteboardManager.h" 11 | 12 | @implementation KayokoFavoritesTableView 13 | /** 14 | * Sets up the swipe actions on the left. 15 | * 16 | * @param tableView 17 | * @param indexPath 18 | */ 19 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView 20 | leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { 21 | NSMutableArray *actions = [[[super tableView:tableView 22 | leadingSwipeActionsConfigurationForRowAtIndexPath:indexPath] actions] mutableCopy]; 23 | PasteboardItem *item = [PasteboardItem itemFromDictionary:[self items][[indexPath row]]]; 24 | 25 | UIContextualAction *unfavoriteAction = [UIContextualAction 26 | contextualActionWithStyle:UIContextualActionStyleNormal 27 | title:@"" 28 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 29 | void (^_Nonnull completionHandler)(BOOL)) { 30 | [self 31 | performBatchUpdates:^{ 32 | [self deleteRowsAtIndexPaths:@[ indexPath ] 33 | withRowAnimation:UITableViewRowAnimationRight]; 34 | NSMutableArray *items = [[self items] mutableCopy]; 35 | [items removeObjectAtIndex:[indexPath row]]; 36 | [self setItems:items]; 37 | } 38 | completion:^(BOOL finished) { 39 | [[PasteboardManager sharedInstance] addPasteboardItem:item 40 | toHistoryWithKey:kHistoryKeyHistory]; 41 | [[PasteboardManager sharedInstance] removePasteboardItem:item 42 | fromHistoryWithKey:kHistoryKeyFavorites 43 | shouldRemoveImage:NO]; 44 | completionHandler(YES); 45 | }]; 46 | }]; 47 | [unfavoriteAction setImage:[UIImage systemImageNamed:@"heart.slash.fill"]]; 48 | [unfavoriteAction setBackgroundColor:[UIColor systemPinkColor]]; 49 | [actions insertObject:unfavoriteAction atIndex:0]; 50 | 51 | return [UISwipeActionsConfiguration configurationWithActions:actions]; 52 | } 53 | 54 | /** 55 | * Sets up the swipe actions on the right. 56 | * 57 | * @param tableView 58 | * @param indexPath 59 | */ 60 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView 61 | trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { 62 | NSMutableArray *actions = [[NSMutableArray alloc] init]; 63 | PasteboardItem *item = [PasteboardItem itemFromDictionary:[self items][[indexPath row]]]; 64 | 65 | UIContextualAction *deleteAction = [UIContextualAction 66 | contextualActionWithStyle:UIContextualActionStyleNormal 67 | title:@"" 68 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 69 | void (^_Nonnull completionHandler)(BOOL)) { 70 | [self 71 | performBatchUpdates:^{ 72 | [self deleteRowsAtIndexPaths:@[ indexPath ] 73 | withRowAnimation:UITableViewRowAnimationLeft]; 74 | NSMutableArray *items = [[self items] mutableCopy]; 75 | [items removeObjectAtIndex:[indexPath row]]; 76 | [self setItems:items]; 77 | } 78 | completion:^(BOOL finished) { 79 | [[PasteboardManager sharedInstance] removePasteboardItem:item 80 | fromHistoryWithKey:kHistoryKeyFavorites 81 | shouldRemoveImage:YES]; 82 | completionHandler(YES); 83 | }]; 84 | }]; 85 | [deleteAction setImage:[UIImage systemImageNamed:@"trash.fill"]]; 86 | [deleteAction setBackgroundColor:[UIColor systemRedColor]]; 87 | [actions addObject:deleteAction]; 88 | 89 | return [UISwipeActionsConfiguration configurationWithActions:actions]; 90 | } 91 | @end 92 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoHistoryTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoHistoryTableView.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoTableView.h" 9 | 10 | @interface KayokoHistoryTableView : KayokoTableView 11 | @end 12 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoHistoryTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoHistoryTableView.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoHistoryTableView.h" 9 | #import "PasteboardItem.h" 10 | #import "PasteboardManager.h" 11 | 12 | @implementation KayokoHistoryTableView 13 | 14 | /** 15 | * Sets up the swipe actions on the left. 16 | * 17 | * @param tableView 18 | * @param indexPath 19 | */ 20 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView 21 | leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { 22 | NSMutableArray *actions = [[[super tableView:tableView 23 | leadingSwipeActionsConfigurationForRowAtIndexPath:indexPath] actions] mutableCopy]; 24 | PasteboardItem *item = [PasteboardItem itemFromDictionary:[self items][[indexPath row]]]; 25 | 26 | UIContextualAction *favoriteAction = [UIContextualAction 27 | contextualActionWithStyle:UIContextualActionStyleNormal 28 | title:@"" 29 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 30 | void (^_Nonnull completionHandler)(BOOL)) { 31 | [self 32 | performBatchUpdates:^{ 33 | [self deleteRowsAtIndexPaths:@[ indexPath ] 34 | withRowAnimation:UITableViewRowAnimationRight]; 35 | NSMutableArray *items = [[self items] mutableCopy]; 36 | [items removeObjectAtIndex:[indexPath row]]; 37 | [self setItems:items]; 38 | } 39 | completion:^(BOOL finished) { 40 | [[PasteboardManager sharedInstance] addPasteboardItem:item 41 | toHistoryWithKey:kHistoryKeyFavorites]; 42 | [[PasteboardManager sharedInstance] removePasteboardItem:item 43 | fromHistoryWithKey:kHistoryKeyHistory 44 | shouldRemoveImage:NO]; 45 | completionHandler(YES); 46 | }]; 47 | }]; 48 | [favoriteAction setImage:[UIImage systemImageNamed:@"heart.fill"]]; 49 | [favoriteAction setBackgroundColor:[UIColor systemPinkColor]]; 50 | [actions insertObject:favoriteAction atIndex:0]; 51 | 52 | return [UISwipeActionsConfiguration configurationWithActions:actions]; 53 | } 54 | 55 | /** 56 | * Sets up the swipe actions on the right. 57 | * 58 | * @param tableView 59 | * @param indexPath 60 | */ 61 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView 62 | trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { 63 | NSMutableArray *actions = [[NSMutableArray alloc] init]; 64 | PasteboardItem *item = [PasteboardItem itemFromDictionary:[self items][[indexPath row]]]; 65 | 66 | UIContextualAction *deleteAction = [UIContextualAction 67 | contextualActionWithStyle:UIContextualActionStyleNormal 68 | title:@"" 69 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 70 | void (^_Nonnull completionHandler)(BOOL)) { 71 | [self 72 | performBatchUpdates:^{ 73 | [self deleteRowsAtIndexPaths:@[ indexPath ] 74 | withRowAnimation:UITableViewRowAnimationLeft]; 75 | NSMutableArray *items = [[self items] mutableCopy]; 76 | [items removeObjectAtIndex:[indexPath row]]; 77 | [self setItems:items]; 78 | } 79 | completion:^(BOOL finished) { 80 | [[PasteboardManager sharedInstance] removePasteboardItem:item 81 | fromHistoryWithKey:kHistoryKeyHistory 82 | shouldRemoveImage:YES]; 83 | completionHandler(YES); 84 | }]; 85 | }]; 86 | [deleteAction setImage:[UIImage systemImageNamed:@"trash.fill"]]; 87 | [deleteAction setBackgroundColor:[UIColor systemRedColor]]; 88 | [actions addObject:deleteAction]; 89 | 90 | return [UISwipeActionsConfiguration configurationWithActions:actions]; 91 | } 92 | 93 | @end 94 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoPreviewView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoPreviewView.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface KayokoPreviewView : UIView 11 | @property(nonatomic, strong) UITextView *textView; 12 | @property(nonatomic, strong) UIImageView *imageView; 13 | @property(nonatomic, strong) WKWebView *webView; 14 | @property(nonatomic, copy) NSString *name; 15 | - (instancetype)initWithName:(NSString *)name; 16 | - (void)reset; 17 | @end 18 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoPreviewView.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoPreviewView.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoPreviewView.h" 9 | 10 | @implementation KayokoPreviewView 11 | 12 | /** 13 | * Initializes the preview view. 14 | */ 15 | - (instancetype)initWithName:(NSString *)name { 16 | self = [super init]; 17 | 18 | if (self) { 19 | [self setName:name]; 20 | 21 | [self setTextView:[[UITextView alloc] init]]; 22 | [[self textView] setBackgroundColor:[UIColor clearColor]]; 23 | [[self textView] setFont:[UIFont systemFontOfSize:14]]; 24 | [[self textView] setEditable:NO]; 25 | [[self textView] setSelectable:NO]; 26 | [[self textView] setHidden:YES]; 27 | [self addSubview:[self textView]]; 28 | 29 | [[self textView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 30 | [NSLayoutConstraint activateConstraints:@[ 31 | [[[self textView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 32 | [[[self textView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor] constant:16], 33 | [[[self textView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor] constant:-16], 34 | [[[self textView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 35 | ]]; 36 | 37 | [self setImageView:[[UIImageView alloc] init]]; 38 | [[self imageView] setContentMode:UIViewContentModeScaleAspectFit]; 39 | [[self imageView] setHidden:YES]; 40 | [self addSubview:[self imageView]]; 41 | 42 | [[self imageView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 43 | [NSLayoutConstraint activateConstraints:@[ 44 | [[[self imageView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 45 | [[[self imageView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 46 | [[[self imageView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 47 | [[[self imageView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 48 | ]]; 49 | 50 | [self setWebView:[[WKWebView alloc] init]]; 51 | [[self webView] setNavigationDelegate:self]; 52 | [[self webView] setAllowsBackForwardNavigationGestures:YES]; 53 | [[self webView] setHidden:YES]; 54 | [self addSubview:[self webView]]; 55 | 56 | [[self webView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 57 | [NSLayoutConstraint activateConstraints:@[ 58 | [[[self webView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 59 | [[[self webView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 60 | [[[self webView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 61 | [[[self webView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 62 | ]]; 63 | } 64 | 65 | return self; 66 | } 67 | 68 | /** 69 | * Resets the preview view. 70 | * 71 | * Hides the view as well as removes any text, image or web content. 72 | */ 73 | - (void)reset { 74 | [[self textView] setHidden:YES]; 75 | [[self textView] setText:@""]; 76 | [[self imageView] setHidden:YES]; 77 | [[self imageView] setImage:nil]; 78 | [[self webView] setHidden:YES]; 79 | [[self webView] loadHTMLString:@"" baseURL:nil]; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoTableView.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface KayokoTableView : UITableView 11 | @property(nonatomic, copy) NSString *name; 12 | @property(nonatomic, strong) NSArray *items; 13 | @property(nonatomic, assign) BOOL automaticallyPaste; 14 | - (instancetype)initWithName:(NSString *)name; 15 | - (void)reloadDataWithItems:(NSArray *)items; 16 | @end 17 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoTableView.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoTableView.h" 9 | #import "KayokoTableViewCell.h" 10 | #import "PasteboardItem.h" 11 | #import "PasteboardManager.h" 12 | 13 | @implementation KayokoTableView 14 | 15 | /** 16 | * Initializes the table view. 17 | * 18 | * @param name The associated name with the table view that's displayed on the main view. 19 | */ 20 | - (instancetype)initWithName:(NSString *)name { 21 | self = [super init]; 22 | 23 | if (self) { 24 | [self setName:name]; 25 | [self setDelegate:self]; 26 | [self setDataSource:self]; 27 | [self setBackgroundColor:[UIColor clearColor]]; 28 | [self setRowHeight:65]; 29 | } 30 | 31 | return self; 32 | } 33 | 34 | /** 35 | * Defines how many rows are in the table view. 36 | * 37 | * @param tableView 38 | * @param section 39 | */ 40 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 41 | return [[self items] count] ?: 0; 42 | } 43 | 44 | /** 45 | * Styles the table view cells. 46 | * 47 | * @param tableView 48 | * @param indexPath 49 | */ 50 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 51 | NSDictionary *dictionary = [self items][[indexPath row]]; 52 | PasteboardItem *item = [PasteboardItem itemFromDictionary:dictionary]; 53 | 54 | KayokoTableViewCell *cell = [[KayokoTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault 55 | andItem:item 56 | reuseIdentifier:@"KayokoTableViewCell"]; 57 | 58 | // Add long press gesture recognizer to preview the cell's content. 59 | UILongPressGestureRecognizer *gesture = 60 | [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGestureRecognizer:)]; 61 | [cell addGestureRecognizer:gesture]; 62 | 63 | return cell; 64 | } 65 | 66 | /** 67 | * Handles table view cell selection. 68 | * 69 | * it creates a dictionary from the cell's row index. 70 | * Then it creates a pasteboard item from the dictionary and updates the pasteboard with it. 71 | * 72 | * @param tableView 73 | * @param indexPath 74 | */ 75 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 76 | [[tableView cellForRowAtIndexPath:indexPath] setSelected:NO animated:YES]; 77 | 78 | NSDictionary *dictionary = [self items][[indexPath row]]; 79 | PasteboardItem *item = [PasteboardItem itemFromDictionary:dictionary]; 80 | [[PasteboardManager sharedInstance] updatePasteboardWithItem:item 81 | fromHistoryWithKey:kHistoryKeyHistory 82 | shouldAutoPaste:YES]; 83 | 84 | [[self superview] performSelector:@selector(hide)]; 85 | } 86 | 87 | /** 88 | * Sets up the swipe actions on the left. 89 | * 90 | * @param tableView 91 | * @param indexPath 92 | */ 93 | - (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView 94 | leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath { 95 | NSMutableArray *actions = [[NSMutableArray alloc] init]; 96 | PasteboardItem *item = [PasteboardItem itemFromDictionary:[self items][[indexPath row]]]; 97 | 98 | // If automatic paste is enabled and the item has text, add an option to only copy the contents without pasting. 99 | // If the item has an image we want to instead add an option to save the image to the photo library. 100 | if ([self automaticallyPaste] || ![[item imageName] isEqualToString:@""]) { 101 | UIContextualAction *saveAction; 102 | 103 | if ([[item imageName] isEqualToString:@""]) { 104 | saveAction = [UIContextualAction 105 | contextualActionWithStyle:UIContextualActionStyleNormal 106 | title:@"" 107 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 108 | void (^_Nonnull completionHandler)(BOOL)) { 109 | [[UIPasteboard generalPasteboard] setString:[item content]]; 110 | completionHandler(YES); 111 | }]; 112 | [saveAction setImage:[UIImage systemImageNamed:@"doc.on.doc.fill"]]; 113 | } else { 114 | saveAction = [UIContextualAction 115 | contextualActionWithStyle:UIContextualActionStyleNormal 116 | title:@"" 117 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 118 | void (^_Nonnull completionHandler)(BOOL)) { 119 | UIImageWriteToSavedPhotosAlbum( 120 | [[PasteboardManager sharedInstance] getImageForItem:item], nil, nil, nil); 121 | completionHandler(YES); 122 | }]; 123 | [saveAction setImage:[UIImage systemImageNamed:@"square.and.arrow.down.fill"]]; 124 | } 125 | 126 | [saveAction setBackgroundColor:[UIColor systemOrangeColor]]; 127 | [actions addObject:saveAction]; 128 | } 129 | 130 | if ([item hasLink]) { 131 | UIContextualAction *linkAction = [UIContextualAction 132 | contextualActionWithStyle:UIContextualActionStyleNormal 133 | title:@"" 134 | handler:^(UIContextualAction *_Nonnull action, __kindof UIView *_Nonnull sourceView, 135 | void (^_Nonnull completionHandler)(BOOL)) { 136 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[item content]] 137 | options:@{} 138 | completionHandler:nil]; 139 | completionHandler(YES); 140 | }]; 141 | [linkAction setImage:[UIImage systemImageNamed:@"arrow.up"]]; 142 | [linkAction setBackgroundColor:[UIColor systemGreenColor]]; 143 | [actions addObject:linkAction]; 144 | } 145 | 146 | return [UISwipeActionsConfiguration configurationWithActions:actions]; 147 | } 148 | 149 | /** 150 | * Handles the long press gesture for the preview. 151 | * 152 | * It creates a dictionary from the cell's content and sends it to the main view to preview it. 153 | * 154 | * @param recognizer The long press gesture recognizer. 155 | */ 156 | - (void)handleLongPressGestureRecognizer:(UILongPressGestureRecognizer *)recognizer { 157 | if ([recognizer state] == UIGestureRecognizerStateBegan) { 158 | KayokoTableViewCell *cell = (KayokoTableViewCell *)[recognizer view]; 159 | NSIndexPath *indexPath = [self indexPathForCell:cell]; 160 | 161 | NSDictionary *dictionary = [self items][[indexPath row]]; 162 | PasteboardItem *item = [PasteboardItem itemFromDictionary:dictionary]; 163 | 164 | [[self superview] performSelector:@selector(showPreviewWithItem:) withObject:item]; 165 | } 166 | } 167 | 168 | /** 169 | * Reloads the table view with new items. 170 | * 171 | * @param items The new items to laod. 172 | */ 173 | - (void)reloadDataWithItems:(NSArray *)items { 174 | [self setItems:items]; 175 | [self reloadData]; 176 | } 177 | 178 | @end 179 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoTableViewCell.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @class PasteboardItem; 11 | 12 | @interface KayokoTableViewCell : UITableViewCell 13 | @property(nonatomic) UIImageView *iconImageView; 14 | @property(nonatomic) UILabel *headerLabel; 15 | @property(nonatomic) UILabel *contentLabel; 16 | @property(nonatomic) UIImageView *contentImageView; 17 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 18 | andItem:(PasteboardItem *)item 19 | reuseIdentifier:(NSString *)reuseIdentifier; 20 | @end 21 | 22 | @interface UIImage (Private) 23 | + (instancetype)_applicationIconImageForBundleIdentifier:(NSString *)bundleIdentifier 24 | format:(int)format 25 | scale:(CGFloat)scale; 26 | @end 27 | 28 | @interface SBApplicationController : NSObject 29 | + (id)sharedInstance; 30 | - (id)applicationWithBundleIdentifier:(id)arg1; 31 | @end 32 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoTableViewCell.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoTableViewCell.h" 9 | #import "ImageUtil.h" 10 | #import "PasteboardItem.h" 11 | #import "PasteboardManager.h" 12 | #import 13 | 14 | @implementation KayokoTableViewCell 15 | 16 | /** 17 | * Initializes the table view cell. 18 | * 19 | * @param style 20 | * @param item 21 | * @param reuseIdentifier 22 | */ 23 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 24 | andItem:(PasteboardItem *)item 25 | reuseIdentifier:(NSString *)reuseIdentifier { 26 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 27 | 28 | if (self) { 29 | [self setBackgroundColor:[UIColor clearColor]]; 30 | 31 | [self setIconImageView:[[UIImageView alloc] init]]; 32 | 33 | UIImage *icon = nil; 34 | if ([[item bundleIdentifier] isEqualToString:@"com.apple.springboard"]) { 35 | BOOL isPad = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad; 36 | icon = [UIImage imageNamed:isPad ? @"HLS_iPad_Universal" : @"HLS_iPhone_Universal" 37 | inBundle:[PasteboardManager localizationBundle] 38 | compatibleWithTraitCollection:nil]; 39 | } else { 40 | icon = [UIImage _applicationIconImageForBundleIdentifier:[item bundleIdentifier] 41 | format:2 42 | scale:[[UIScreen mainScreen] scale]]; 43 | } 44 | // Use the default app icon if no icon exists for the item's bundle identifier. 45 | if (!icon) { 46 | icon = [UIImage _applicationIconImageForBundleIdentifier:@"com.apple.WebSheet" 47 | format:2 48 | scale:[[UIScreen mainScreen] scale]]; 49 | } 50 | [[self iconImageView] setImage:icon]; 51 | 52 | [[self iconImageView] setContentMode:UIViewContentModeScaleAspectFit]; 53 | [[self iconImageView] setClipsToBounds:YES]; 54 | [[[self iconImageView] layer] setCornerRadius:10]; 55 | [self addSubview:[self iconImageView]]; 56 | 57 | [[self iconImageView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 58 | [NSLayoutConstraint activateConstraints:@[ 59 | [[[self iconImageView] widthAnchor] constraintEqualToConstant:40], 60 | [[[self iconImageView] heightAnchor] constraintEqualToConstant:40], 61 | [[[self iconImageView] centerYAnchor] constraintEqualToAnchor:[self centerYAnchor]], 62 | [[[self iconImageView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor] constant:24] 63 | ]]; 64 | 65 | if (![[item imageName] isEqualToString:@""]) { 66 | [self setContentImageView:[[UIImageView alloc] init]]; 67 | 68 | UIImage *originalImage = [[PasteboardManager sharedInstance] getImageForItem:item]; 69 | // Save memory by scaling the image down in the history view. 70 | UIImage *scaledImage = 71 | [ImageUtil getImageWithImage:originalImage 72 | scaledToSize:CGSizeMake(originalImage.size.width / 4, originalImage.size.height / 4)]; 73 | [[self contentImageView] setImage:scaledImage]; 74 | 75 | [[self contentImageView] setContentMode:UIViewContentModeScaleAspectFill]; 76 | [[self contentImageView] setClipsToBounds:YES]; 77 | [[[self contentImageView] layer] setCornerRadius:4]; 78 | [self addSubview:[self contentImageView]]; 79 | 80 | [[self contentImageView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 81 | [NSLayoutConstraint activateConstraints:@[ 82 | [[[self contentImageView] widthAnchor] constraintEqualToConstant:70], 83 | [[[self contentImageView] heightAnchor] constraintEqualToConstant:40], 84 | [[[self contentImageView] centerYAnchor] constraintEqualToAnchor:[self centerYAnchor]], 85 | [[[self contentImageView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor] constant:-24] 86 | ]]; 87 | } 88 | 89 | [self setHeaderLabel:[[UILabel alloc] init]]; 90 | NSString *displayName = [[[objc_getClass("SBApplicationController") sharedInstance] 91 | applicationWithBundleIdentifier:[item bundleIdentifier]] displayName] 92 | ?: [[PasteboardManager localizationBundle] localizedStringForKey:@"SpringBoard" 93 | value:nil 94 | table:@"Tweak"]; 95 | [[self headerLabel] setText:displayName]; 96 | [[self headerLabel] setFont:[UIFont systemFontOfSize:16 weight:UIFontWeightMedium]]; 97 | [[self headerLabel] setTextColor:[UIColor labelColor]]; 98 | [self addSubview:[self headerLabel]]; 99 | 100 | [[self headerLabel] setTranslatesAutoresizingMaskIntoConstraints:NO]; 101 | [NSLayoutConstraint activateConstraints:@[ 102 | [[[self headerLabel] topAnchor] constraintEqualToAnchor:[[self iconImageView] topAnchor] constant:1], 103 | [[[self headerLabel] leadingAnchor] constraintEqualToAnchor:[[self iconImageView] trailingAnchor] 104 | constant:16] 105 | ]]; 106 | 107 | if ([self contentImageView]) { 108 | [NSLayoutConstraint activateConstraints:@[ [[[self headerLabel] trailingAnchor] 109 | constraintEqualToAnchor:[[self contentImageView] leadingAnchor] 110 | constant:-16] ]]; 111 | } else { 112 | [NSLayoutConstraint activateConstraints:@[ [[[self headerLabel] trailingAnchor] 113 | constraintEqualToAnchor:[self trailingAnchor] 114 | constant:-24] ]]; 115 | } 116 | 117 | [self setContentLabel:[[UILabel alloc] init]]; 118 | [[self contentLabel] setText:[item content]]; 119 | [[self contentLabel] setFont:[UIFont systemFontOfSize:14]]; 120 | [[self contentLabel] setTextColor:[[UIColor labelColor] colorWithAlphaComponent:0.8]]; 121 | [[self contentLabel] setLineBreakMode:NSLineBreakByTruncatingTail]; 122 | [self addSubview:[self contentLabel]]; 123 | 124 | [[self contentLabel] setTranslatesAutoresizingMaskIntoConstraints:NO]; 125 | [NSLayoutConstraint activateConstraints:@[ 126 | [[[self contentLabel] bottomAnchor] constraintEqualToAnchor:[[self iconImageView] bottomAnchor] 127 | constant:-1], 128 | [[[self contentLabel] leadingAnchor] constraintEqualToAnchor:[[self headerLabel] leadingAnchor]], 129 | [[[self contentLabel] trailingAnchor] constraintEqualToAnchor:[[self headerLabel] trailingAnchor]] 130 | ]]; 131 | } 132 | 133 | return self; 134 | } 135 | 136 | @end 137 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoView.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoView.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @class KayokoTableView; 11 | @class KayokoHistoryTableView; 12 | @class KayokoFavoritesTableView; 13 | @class KayokoPreviewView; 14 | @class PasteboardItem; 15 | 16 | static NSUInteger const kFavoritesButtonImageSize = 24; 17 | static NSUInteger const kClearButtonImageSize = 20; 18 | static NSUInteger const kBackButtonImageSize = 20; 19 | 20 | @interface _UIGrabber : UIControl 21 | @end 22 | 23 | @interface KayokoView : UIView { 24 | KayokoTableView *_previewSourceTableView; 25 | BOOL _isAnimating; 26 | } 27 | @property(nonatomic, strong) UIBlurEffect *blurEffect; 28 | @property(nonatomic, strong) UIVisualEffectView *blurEffectView; 29 | @property(nonatomic, strong) UIView *headerView; 30 | @property(nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer; 31 | @property(nonatomic, strong) _UIGrabber *grabber; 32 | @property(nonatomic, strong) UILabel *titleLabel; 33 | @property(nonatomic, strong) UIButton *clearButton; 34 | @property(nonatomic, strong) UIButton *backButton; 35 | @property(nonatomic, strong) UIButton *favoritesButton; 36 | @property(nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer; 37 | @property(nonatomic, strong) KayokoHistoryTableView *historyTableView; 38 | @property(nonatomic, strong) KayokoFavoritesTableView *favoritesTableView; 39 | @property(nonatomic, strong) KayokoPreviewView *previewView; 40 | @property(nonatomic, strong) UIImpactFeedbackGenerator *feedbackGenerator; 41 | @property(nonatomic, assign) BOOL automaticallyPaste; 42 | @property(nonatomic, assign) BOOL shouldPlayFeedback; 43 | - (void)showPreviewWithItem:(PasteboardItem *)item; 44 | - (void)show; 45 | - (void)hide; 46 | - (void)reload; 47 | @end 48 | -------------------------------------------------------------------------------- /Tweak/Core/Views/KayokoView.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoView.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoView.h" 9 | #import "KayokoFavoritesTableView.h" 10 | #import "KayokoHistoryTableView.h" 11 | #import "KayokoPreviewView.h" 12 | #import "PasteboardItem.h" 13 | #import "PasteboardManager.h" 14 | #import 15 | 16 | @implementation KayokoView 17 | 18 | /** 19 | * Initializes the main view. 20 | * 21 | * @param frame 22 | */ 23 | - (instancetype)initWithFrame:(CGRect)frame { 24 | self = [super initWithFrame:frame]; 25 | 26 | if (self) { 27 | [self hide]; 28 | 29 | [[self layer] setShadowColor:[[UIColor blackColor] CGColor]]; 30 | [[self layer] setShadowOffset:CGSizeZero]; 31 | [[self layer] setShadowRadius:10]; 32 | [[self layer] setShadowOpacity:0.5]; 33 | 34 | [self setBlurEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleRegular]]; 35 | [self setBlurEffectView:[[UIVisualEffectView alloc] initWithEffect:[self blurEffect]]]; 36 | [self addSubview:[self blurEffectView]]; 37 | 38 | [[self blurEffectView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 39 | [NSLayoutConstraint activateConstraints:@[ 40 | [[[self blurEffectView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 41 | [[[self blurEffectView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 42 | [[[self blurEffectView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 43 | [[[self blurEffectView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 44 | ]]; 45 | 46 | [self setHeaderView:[[UIView alloc] init]]; 47 | [self addSubview:[self headerView]]; 48 | 49 | [[self headerView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 50 | [NSLayoutConstraint activateConstraints:@[ 51 | [[[self headerView] heightAnchor] constraintEqualToConstant:60], 52 | [[[self headerView] topAnchor] constraintEqualToAnchor:[self topAnchor]], 53 | [[[self headerView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 54 | [[[self headerView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]] 55 | ]]; 56 | 57 | [self setPanGestureRecognizer:[[UIPanGestureRecognizer alloc] 58 | initWithTarget:self 59 | action:@selector(handlePanGestureRecognizer:)]]; 60 | [[self headerView] addGestureRecognizer:[self panGestureRecognizer]]; 61 | 62 | [self setTapGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self 63 | action:@selector(hidePreview)]]; 64 | [[self headerView] addGestureRecognizer:[self tapGestureRecognizer]]; 65 | 66 | [self setGrabber:[[_UIGrabber alloc] init]]; 67 | [[self headerView] addSubview:[self grabber]]; 68 | 69 | [[self grabber] setTranslatesAutoresizingMaskIntoConstraints:NO]; 70 | [NSLayoutConstraint activateConstraints:@[ 71 | [[[self grabber] topAnchor] constraintEqualToAnchor:[[self headerView] topAnchor] constant:12], 72 | [[[self grabber] centerXAnchor] constraintEqualToAnchor:[[self headerView] centerXAnchor]] 73 | ]]; 74 | 75 | [self setFavoritesButton:[[UIButton alloc] init]]; 76 | [[self favoritesButton] addTarget:self 77 | action:@selector(handleFavoritesButtonPressed) 78 | forControlEvents:UIControlEventTouchUpInside]; 79 | [self updateStyleForHeaderButton:[self favoritesButton] 80 | withImageName:@"heart" 81 | andImageSize:kFavoritesButtonImageSize 82 | andTintColor:[UIColor labelColor]]; 83 | [[self headerView] addSubview:[self favoritesButton]]; 84 | 85 | [[self favoritesButton] setTranslatesAutoresizingMaskIntoConstraints:NO]; 86 | [NSLayoutConstraint activateConstraints:@[ 87 | [[[self favoritesButton] bottomAnchor] constraintEqualToAnchor:[[self headerView] bottomAnchor] 88 | constant:-2], 89 | [[[self favoritesButton] leadingAnchor] constraintEqualToAnchor:[[self headerView] leadingAnchor] 90 | constant:24] 91 | ]]; 92 | 93 | [self setTitleLabel:[[UILabel alloc] init]]; 94 | [[self titleLabel] setText:[[PasteboardManager localizationBundle] localizedStringForKey:@"History" 95 | value:nil 96 | table:@"Tweak"]]; 97 | [[self titleLabel] setFont:[UIFont systemFontOfSize:26 weight:UIFontWeightSemibold]]; 98 | [[self titleLabel] setTextColor:[UIColor labelColor]]; 99 | [[self headerView] addSubview:[self titleLabel]]; 100 | 101 | [[self titleLabel] setTranslatesAutoresizingMaskIntoConstraints:NO]; 102 | [NSLayoutConstraint activateConstraints:@[ 103 | [[[self titleLabel] centerYAnchor] constraintEqualToAnchor:[[self favoritesButton] centerYAnchor]], 104 | [[[self titleLabel] leadingAnchor] constraintEqualToAnchor:[[self favoritesButton] trailingAnchor] 105 | constant:12] 106 | ]]; 107 | 108 | [self setClearButton:[[UIButton alloc] init]]; 109 | [[self clearButton] addTarget:self 110 | action:@selector(handleClearButtonPressed) 111 | forControlEvents:UIControlEventTouchUpInside]; 112 | [self updateStyleForHeaderButton:[self clearButton] 113 | withImageName:@"trash" 114 | andImageSize:kClearButtonImageSize 115 | andTintColor:[UIColor labelColor]]; 116 | [[self headerView] addSubview:[self clearButton]]; 117 | [[self clearButton] setHidden:NO]; 118 | 119 | [[self clearButton] setTranslatesAutoresizingMaskIntoConstraints:NO]; 120 | [NSLayoutConstraint activateConstraints:@[ 121 | [[[self clearButton] centerYAnchor] constraintEqualToAnchor:[[self favoritesButton] centerYAnchor]], 122 | [[[self clearButton] trailingAnchor] constraintEqualToAnchor:[[self headerView] trailingAnchor] 123 | constant:-24] 124 | ]]; 125 | 126 | [self setBackButton:[[UIButton alloc] init]]; 127 | [[self backButton] addTarget:self action:@selector(hidePreview) forControlEvents:UIControlEventTouchUpInside]; 128 | [self updateStyleForHeaderButton:[self backButton] 129 | withImageName:@"arrowshape.turn.up.backward" 130 | andImageSize:kBackButtonImageSize 131 | andTintColor:[UIColor labelColor]]; 132 | [[self headerView] addSubview:[self backButton]]; 133 | [[self backButton] setHidden:YES]; 134 | 135 | [[self backButton] setTranslatesAutoresizingMaskIntoConstraints:NO]; 136 | [NSLayoutConstraint activateConstraints:@[ 137 | [[[self backButton] centerYAnchor] constraintEqualToAnchor:[[self favoritesButton] centerYAnchor]], 138 | [[[self backButton] trailingAnchor] constraintEqualToAnchor:[[self headerView] trailingAnchor] constant:-24] 139 | ]]; 140 | 141 | [self setHistoryTableView:[[KayokoHistoryTableView alloc] initWithName:[[PasteboardManager localizationBundle] 142 | localizedStringForKey:@"History" 143 | value:nil 144 | table:@"Tweak"]]]; 145 | [self addSubview:[self historyTableView]]; 146 | 147 | [[self historyTableView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 148 | [NSLayoutConstraint activateConstraints:@[ 149 | [[[self historyTableView] topAnchor] constraintEqualToAnchor:[[self headerView] bottomAnchor] constant:8], 150 | [[[self historyTableView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 151 | [[[self historyTableView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 152 | [[[self historyTableView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 153 | ]]; 154 | 155 | [self 156 | setFavoritesTableView:[[KayokoFavoritesTableView alloc] initWithName:[[PasteboardManager localizationBundle] 157 | localizedStringForKey:@"Favorites" 158 | value:nil 159 | table:@"Tweak"]]]; 160 | [[self favoritesTableView] setHidden:YES]; 161 | [self addSubview:[self favoritesTableView]]; 162 | 163 | [[self favoritesTableView] reloadDataWithItems:nil]; 164 | 165 | [[self favoritesTableView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 166 | [NSLayoutConstraint activateConstraints:@[ 167 | [[[self favoritesTableView] topAnchor] constraintEqualToAnchor:[[self headerView] bottomAnchor] constant:8], 168 | [[[self favoritesTableView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 169 | [[[self favoritesTableView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 170 | [[[self favoritesTableView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 171 | ]]; 172 | 173 | [self setPreviewView:[[KayokoPreviewView alloc] 174 | initWithName:[[PasteboardManager localizationBundle] localizedStringForKey:@"Preview" 175 | value:nil 176 | table:@"Tweak"]]]; 177 | [[self previewView] setHidden:YES]; 178 | [self addSubview:[self previewView]]; 179 | 180 | [[self previewView] setTranslatesAutoresizingMaskIntoConstraints:NO]; 181 | [NSLayoutConstraint activateConstraints:@[ 182 | [[[self previewView] topAnchor] constraintEqualToAnchor:[[self headerView] bottomAnchor] constant:8], 183 | [[[self previewView] leadingAnchor] constraintEqualToAnchor:[self leadingAnchor]], 184 | [[[self previewView] trailingAnchor] constraintEqualToAnchor:[self trailingAnchor]], 185 | [[[self previewView] bottomAnchor] constraintEqualToAnchor:[self bottomAnchor]] 186 | ]]; 187 | } 188 | 189 | return self; 190 | } 191 | 192 | /** 193 | * Handles the drag on the top of the main view to close it. 194 | * 195 | * @param recognizer The pan gesture recognizer. 196 | */ 197 | - (void)handlePanGestureRecognizer:(UIPanGestureRecognizer *)recognizer { 198 | CGPoint translation = CGPointMake(0, 0); 199 | NSUInteger const kMaxTranslation = 100; 200 | 201 | if ([recognizer state] == UIGestureRecognizerStateChanged) { 202 | translation = [recognizer translationInView:self]; 203 | 204 | if (translation.y < 0) { 205 | return; 206 | } 207 | 208 | CGFloat alpha = fabs(translation.y / kMaxTranslation); 209 | [UIView animateWithDuration:0.1 210 | delay:0 211 | usingSpringWithDamping:0.7 212 | initialSpringVelocity:0 213 | options:UIViewAnimationOptionCurveEaseOut 214 | animations:^{ 215 | [self setTransform:CGAffineTransformMakeTranslation(0, translation.y)]; 216 | [self setAlpha:1 - alpha]; 217 | } 218 | completion:nil]; 219 | 220 | if (translation.y >= kMaxTranslation) { 221 | [self hide]; 222 | return; 223 | } 224 | } else if ([recognizer state] == UIGestureRecognizerStateEnded) { 225 | if (translation.y < kMaxTranslation) { 226 | [UIView animateWithDuration:0.4 227 | delay:0 228 | usingSpringWithDamping:1 229 | initialSpringVelocity:0 230 | options:UIViewAnimationOptionCurveEaseOut 231 | animations:^{ 232 | [self setTransform:CGAffineTransformIdentity]; 233 | [self setAlpha:1]; 234 | } 235 | completion:nil]; 236 | } 237 | } 238 | } 239 | 240 | /** 241 | * Updates the style of the a header button. 242 | * 243 | * @param button The button to update. 244 | * @param imageName The name of the system image to use on the button. 245 | * @param color The color to use for the image. 246 | */ 247 | - (void)updateStyleForHeaderButton:(UIButton *)button 248 | withImageName:(NSString *)imageName 249 | andImageSize:(NSUInteger)imageSize 250 | andTintColor:(UIColor *)color { 251 | UIImageSymbolConfiguration *configuration = 252 | [UIImageSymbolConfiguration configurationWithPointSize:imageSize weight:UIImageSymbolWeightMedium]; 253 | [button setImage:[[UIImage systemImageNamed:imageName] imageWithConfiguration:configuration] 254 | forState:UIControlStateNormal]; 255 | [button setTintColor:color]; 256 | } 257 | 258 | /** 259 | * Handles the press of the favorites button. 260 | * 261 | * It either shows the history or favorites view or hides the preview view again. 262 | */ 263 | - (void)handleFavoritesButtonPressed { 264 | if (_isAnimating) { 265 | return; 266 | } 267 | 268 | if (![[self previewView] isHidden]) { 269 | [self hidePreview]; 270 | } 271 | 272 | if ([[self historyTableView] isHidden]) { 273 | NSArray *items = [[PasteboardManager sharedInstance] getItemsFromHistoryWithKey:kHistoryKeyHistory]; 274 | [[self historyTableView] reloadDataWithItems:items]; 275 | 276 | [self showContentView:[self historyTableView] andHideContentView:[self favoritesTableView] reverse:YES]; 277 | 278 | [self updateStyleForHeaderButton:[self favoritesButton] 279 | withImageName:@"heart" 280 | andImageSize:kFavoritesButtonImageSize 281 | andTintColor:[UIColor labelColor]]; 282 | } else { 283 | NSArray *items = [[PasteboardManager sharedInstance] getItemsFromHistoryWithKey:kHistoryKeyFavorites]; 284 | [[self favoritesTableView] reloadDataWithItems:items]; 285 | 286 | [self showContentView:[self favoritesTableView] andHideContentView:[self historyTableView] reverse:NO]; 287 | 288 | [self updateStyleForHeaderButton:[self favoritesButton] 289 | withImageName:@"heart.fill" 290 | andImageSize:kFavoritesButtonImageSize 291 | andTintColor:[UIColor systemPinkColor]]; 292 | } 293 | 294 | [self triggerHapticFeedbackWithStyle:UIImpactFeedbackStyleSoft]; 295 | } 296 | 297 | - (void)handleClearButtonPressed { 298 | [self hide]; 299 | 300 | NSString *key = [[self historyTableView] isHidden] ? kHistoryKeyFavorites : kHistoryKeyHistory; 301 | UIAlertController *clearAlert = [UIAlertController 302 | alertControllerWithTitle:[[PasteboardManager localizationBundle] localizedStringForKey:@"Kayoko" 303 | value:nil 304 | table:@"Tweak"] 305 | message:[NSString stringWithFormat:[[PasteboardManager localizationBundle] 306 | localizedStringForKey:@"This will clear your %@." 307 | value:nil 308 | table:@"Tweak"], 309 | [[PasteboardManager localizationBundle] 310 | localizedStringForKey:key 311 | value:nil 312 | table:@"Tweak"]] 313 | preferredStyle:UIAlertControllerStyleAlert]; 314 | 315 | UIAlertAction *yesAction = [UIAlertAction 316 | actionWithTitle:[[PasteboardManager localizationBundle] localizedStringForKey:@"Yes" value:nil table:@"Tweak"] 317 | style:UIAlertActionStyleDestructive 318 | handler:^(UIAlertAction *action) { 319 | NSArray *items = [[PasteboardManager sharedInstance] getItemsFromHistoryWithKey:key]; 320 | for (NSDictionary *dictionary in items) { 321 | PasteboardItem *item = [PasteboardItem itemFromDictionary:dictionary]; 322 | [[PasteboardManager sharedInstance] removePasteboardItem:item 323 | fromHistoryWithKey:key 324 | shouldRemoveImage:YES]; 325 | } 326 | 327 | [self show]; 328 | }]; 329 | 330 | UIAlertAction *noAction = [UIAlertAction 331 | actionWithTitle:[[PasteboardManager localizationBundle] localizedStringForKey:@"No" value:nil table:@"Tweak"] 332 | style:UIAlertActionStyleCancel 333 | handler:^(UIAlertAction *action) { 334 | [self show]; 335 | }]; 336 | 337 | [clearAlert addAction:yesAction]; 338 | [clearAlert addAction:noAction]; 339 | 340 | #pragma clang diagnostic push 341 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 342 | [[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:clearAlert 343 | animated:YES 344 | completion:nil]; 345 | #pragma clang diagnostic pop 346 | 347 | [self triggerHapticFeedbackWithStyle:UIImpactFeedbackStyleHeavy]; 348 | } 349 | 350 | /** 351 | * Shows the preview view with a given item's contents. 352 | * 353 | * @param item The item to preview. 354 | */ 355 | - (void)showPreviewWithItem:(PasteboardItem *)item { 356 | if ([item hasLink]) { 357 | [[[self previewView] webView] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[item content]]]]; 358 | [[[self previewView] webView] setHidden:NO]; 359 | } else if (![[item imageName] isEqualToString:@""]) { 360 | NSData *imageData = [[NSFileManager defaultManager] 361 | contentsAtPath:[NSString 362 | stringWithFormat:@"%@/%@", [PasteboardManager historyImagesPath], [item imageName]]]; 363 | [[[self previewView] imageView] setImage:[UIImage imageWithData:imageData]]; 364 | [[[self previewView] imageView] setHidden:NO]; 365 | } else { 366 | [[[self previewView] textView] setText:[item content]]; 367 | [[[self previewView] textView] setHidden:NO]; 368 | } 369 | 370 | _previewSourceTableView = [[self historyTableView] isHidden] ? [self favoritesTableView] : [self historyTableView]; 371 | [self showContentView:[self previewView] andHideContentView:_previewSourceTableView reverse:NO]; 372 | 373 | [[self clearButton] setHidden:YES]; 374 | [[self backButton] setHidden:NO]; 375 | 376 | [self triggerHapticFeedbackWithStyle:UIImpactFeedbackStyleMedium]; 377 | } 378 | 379 | /** 380 | * Hides the preview view. 381 | */ 382 | - (void)hidePreview { 383 | if ([[self previewView] isHidden] || _isAnimating) { 384 | return; 385 | } 386 | 387 | [self showContentView:_previewSourceTableView andHideContentView:[self previewView] reverse:YES]; 388 | 389 | [[self clearButton] setHidden:NO]; 390 | [[self backButton] setHidden:YES]; 391 | 392 | [[self previewView] reset]; 393 | } 394 | 395 | /** 396 | * Animates a view in and out. 397 | * 398 | * For example when switching between the history and favorites view. 399 | * 400 | * @param viewToShow The view that's to be shown. 401 | * @param viewToHide The view that's to be hidden. 402 | * @param reverse Whether the animation should play reversed for a mirrored effect. 403 | */ 404 | - (void)showContentView:(UIView *)viewToShow andHideContentView:(UIView *)viewToHide reverse:(BOOL)reverse { 405 | [UIView transitionWithView:[self titleLabel] 406 | duration:0.1 407 | options:UIViewAnimationOptionTransitionCrossDissolve 408 | animations:^{ 409 | [[self titleLabel] setText:[viewToShow valueForKey:@"_name"]]; 410 | } 411 | completion:nil]; 412 | 413 | CGFloat viewToShowTransform = reverse ? 10 : -10; 414 | [viewToShow setTransform:CGAffineTransformTranslate(viewToShow.transform, 0, viewToShowTransform)]; 415 | [viewToShow setAlpha:0]; 416 | [viewToShow setHidden:NO]; 417 | 418 | _isAnimating = YES; 419 | [UIView animateWithDuration:0.3 420 | delay:0 421 | usingSpringWithDamping:1 422 | initialSpringVelocity:0 423 | options:UIViewAnimationOptionCurveEaseOut 424 | animations:^{ 425 | [viewToShow setTransform:CGAffineTransformIdentity]; 426 | [viewToShow setAlpha:1]; 427 | 428 | CGFloat viewToHideTransform = reverse ? -10 : 10; 429 | [viewToHide setTransform:CGAffineTransformTranslate(viewToShow.transform, 0, viewToHideTransform)]; 430 | [viewToHide setAlpha:0]; 431 | } 432 | completion:^(BOOL finished) { 433 | [viewToHide setHidden:YES]; 434 | _isAnimating = NO; 435 | }]; 436 | } 437 | 438 | /** 439 | * Triggers haptic feedback. 440 | * 441 | * @param style The feedback type/strength to use. 442 | */ 443 | - (void)triggerHapticFeedbackWithStyle:(UIImpactFeedbackStyle)style { 444 | if (!self.shouldPlayFeedback) { 445 | return; 446 | } 447 | [self setFeedbackGenerator:[[UIImpactFeedbackGenerator alloc] initWithStyle:style]]; 448 | [[self feedbackGenerator] prepare]; 449 | [[self feedbackGenerator] impactOccurred]; 450 | [self setFeedbackGenerator:nil]; 451 | } 452 | 453 | /** 454 | * Reloads the active history view. 455 | */ 456 | - (void)reload { 457 | if (![[self historyTableView] isHidden]) { 458 | NSArray *items = [[PasteboardManager sharedInstance] getItemsFromHistoryWithKey:kHistoryKeyHistory]; 459 | [[self historyTableView] reloadDataWithItems:items]; 460 | } else { 461 | NSArray *items = [[PasteboardManager sharedInstance] getItemsFromHistoryWithKey:kHistoryKeyFavorites]; 462 | [[self favoritesTableView] reloadDataWithItems:items]; 463 | } 464 | } 465 | 466 | /** 467 | * Shows the main view. 468 | */ 469 | - (void)show { 470 | if (_isAnimating) { 471 | return; 472 | } 473 | 474 | [[self historyTableView] setAutomaticallyPaste:[self automaticallyPaste]]; 475 | [[self favoritesTableView] setAutomaticallyPaste:[self automaticallyPaste]]; 476 | 477 | [self reload]; 478 | 479 | [self setTransform:CGAffineTransformMakeTranslation(0, [self bounds].size.height / 3)]; 480 | [self setAlpha:0]; 481 | [self setHidden:NO]; 482 | 483 | _isAnimating = YES; 484 | [UIView animateWithDuration:0.33 485 | delay:0 486 | usingSpringWithDamping:1 487 | initialSpringVelocity:0 488 | options:UIViewAnimationOptionCurveEaseOut 489 | animations:^{ 490 | [self setTransform:CGAffineTransformIdentity]; 491 | [self setAlpha:1]; 492 | } 493 | completion:^(BOOL finished) { 494 | _isAnimating = NO; 495 | }]; 496 | } 497 | 498 | /** 499 | * Hides the main view. 500 | */ 501 | - (void)hide { 502 | if (_isAnimating) { 503 | return; 504 | } 505 | 506 | _isAnimating = YES; 507 | [UIView animateWithDuration:0.33 508 | delay:0 509 | usingSpringWithDamping:1 510 | initialSpringVelocity:0 511 | options:UIViewAnimationOptionCurveEaseOut 512 | animations:^{ 513 | [self setAlpha:0]; 514 | } 515 | completion:^(BOOL finished) { 516 | [self setHidden:YES]; 517 | _isAnimating = NO; 518 | }]; 519 | } 520 | 521 | @end 522 | -------------------------------------------------------------------------------- /Tweak/Helper/KayokoHelper.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoHelper.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | OBJC_EXTERN NSUserDefaults *kayokoHelperPreferences; 11 | OBJC_EXTERN BOOL kayokoHelperPrefsEnabled; 12 | OBJC_EXTERN NSUInteger kayokoHelperPrefsActivationMethod; 13 | OBJC_EXTERN BOOL kayokoHelperPrefsAutomaticallyPaste; 14 | 15 | OBJC_EXTERN NSString *const kayokoMenuName; 16 | OBJC_EXTERN NSString *const kayokoSelectorName; 17 | OBJC_EXTERN NSString *const kayokoSelectorSignature; 18 | 19 | OBJC_EXTERN void EnableKayokoActivationGlobe(void); 20 | OBJC_EXTERN void EnableKayokoActivationDictation(void); 21 | 22 | @interface TIKeyboardCandidate : NSObject 23 | @end 24 | 25 | @interface TIAutocorrectionList : NSObject 26 | + (TIAutocorrectionList *)listWithAutocorrection:(TIKeyboardCandidate *)arg1 27 | predictions:(NSArray *)predictions 28 | emojiList:(NSArray *)emojiList; 29 | @end 30 | 31 | @interface UIKeyboardAutocorrectionController : NSObject 32 | - (void)setTextSuggestionList:(TIAutocorrectionList *)textSuggestionList; 33 | - (void)setAutocorrectionList:(TIAutocorrectionList *)textSuggestionList; 34 | @end 35 | 36 | @interface TUIPredictionView : UIView 37 | @end 38 | 39 | @interface TIKeyboardCandidateSingle : TIKeyboardCandidate 40 | @property(nonatomic, copy) NSString *candidate; 41 | @property(nonatomic, copy) NSString *input; 42 | @end 43 | 44 | @interface TIZephyrCandidate : TIKeyboardCandidateSingle 45 | @property(nonatomic, copy) NSString *label; 46 | @property(nonatomic, copy) NSString *fromBundleId; 47 | @end 48 | 49 | @interface UIPredictionViewController : UIViewController 50 | @end 51 | 52 | @interface UIKeyboardLayout : UIView 53 | @end 54 | 55 | @interface UIKeyboardLayoutStar : UIKeyboardLayout 56 | @end 57 | 58 | @interface UISystemKeyboardDockController : NSObject 59 | @end 60 | 61 | @interface UIKBInputDelegateManager : NSObject 62 | - (UITextRange *)selectedTextRange; 63 | - (NSString *)textInRange:(UITextRange *)range; 64 | - (void)insertText:(NSString *)text; 65 | @end 66 | 67 | @interface UIKeyboardImpl : UIView 68 | @property(nonatomic, strong, readonly) UIKeyboardAutocorrectionController *autocorrectionController; 69 | @property(nonatomic, strong) UIKBInputDelegateManager *inputDelegateManager; 70 | @property(nonatomic, strong, readonly) UIResponder *inputDelegate; 71 | + (instancetype)activeInstance; 72 | - (void)insertText:(NSString *)text; 73 | @end 74 | 75 | @interface UIKBTree : NSObject 76 | @property(nonatomic, copy) NSString *name; 77 | @property(nonatomic, strong) NSMutableDictionary *properties; 78 | @end 79 | 80 | @interface UIMenu (Kayoko) 81 | - (UIMenu *)menuByReplacingChildren:(NSArray *)children; 82 | @end 83 | 84 | @interface _UICalloutBarSystemButtonDescription : NSObject 85 | @property (nonatomic, readonly) SEL action; 86 | + (instancetype)buttonDescriptionWithTitle:(NSString *)arg1 action:(SEL)arg2 type:(int)arg3 ; 87 | @end 88 | 89 | @interface UICalloutBar : UIView 90 | @end 91 | -------------------------------------------------------------------------------- /Tweak/Helper/KayokoHelper.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoHelper.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "KayokoHelper.h" 9 | #import "KayokoMenu.h" 10 | #import "NotificationKeys.h" 11 | #import "PasteboardItem.h" 12 | #import "PasteboardManager.h" 13 | #import "PreferenceKeys.h" 14 | 15 | #import 16 | #import 17 | #import 18 | #import 19 | 20 | NSUserDefaults *kayokoHelperPreferences = nil; 21 | 22 | BOOL kayokoHelperPrefsEnabled = NO; 23 | NSUInteger kayokoHelperPrefsActivationMethod = 0; 24 | BOOL kayokoHelperPrefsAutomaticallyPaste = NO; 25 | 26 | NSString *const kayokoMenuName = @"Kayoko"; 27 | NSString *const kayokoSelectorName = @"_Kayoko_OpenTools_ab2e39c7"; 28 | NSString *const kayokoSelectorSignature = @"v@:"; 29 | 30 | static BOOL shouldShowCustomSuggestions = NO; 31 | static BOOL applicationIsInForeground = YES; 32 | 33 | static TIAutocorrectionList *kayokoCreateAutocorrectionList(void); 34 | static void kayokoPaste(void); 35 | 36 | #pragma mark - UIKeyboardAutocorrectionController class hooks 37 | 38 | /** 39 | * Updates the prediction bar with the original or custom items. 40 | * 41 | * This method usage only works on iOS 14 or lower. 42 | * 43 | * @param textSuggestionList The list that is used to update the prediciton bar with. 44 | */ 45 | static void (*orig_UIKeyboardAutocorrectionController_setTextSuggestionList)(UIKeyboardAutocorrectionController *self, 46 | SEL _cmd, 47 | TIAutocorrectionList *textSuggestionList); 48 | static void 49 | override_UIKeyboardAutocorrectionController_setTextSuggestionList(UIKeyboardAutocorrectionController *self, SEL _cmd, 50 | TIAutocorrectionList *textSuggestionList) { 51 | if (shouldShowCustomSuggestions) { 52 | orig_UIKeyboardAutocorrectionController_setTextSuggestionList(self, _cmd, kayokoCreateAutocorrectionList()); 53 | } else { 54 | orig_UIKeyboardAutocorrectionController_setTextSuggestionList(self, _cmd, textSuggestionList); 55 | } 56 | } 57 | 58 | /** 59 | * Updates the prediction bar with the original or custom items. 60 | * 61 | * This method usage only works on iOS 15 or above. 62 | * 63 | * @param autoCorrectionList The list that is used to update the prediciton bar with. 64 | */ 65 | static void (*orig_UIKeyboardAutocorrectionController_setAutocorrectionList)(UIKeyboardAutocorrectionController *self, 66 | SEL _cmd, 67 | TIAutocorrectionList *autoCorrectionList); 68 | static void 69 | override_UIKeyboardAutocorrectionController_setAutocorrectionList(UIKeyboardAutocorrectionController *self, SEL _cmd, 70 | TIAutocorrectionList *autoCorrectionList) { 71 | if (shouldShowCustomSuggestions) { 72 | orig_UIKeyboardAutocorrectionController_setAutocorrectionList(self, _cmd, kayokoCreateAutocorrectionList()); 73 | } else { 74 | orig_UIKeyboardAutocorrectionController_setAutocorrectionList(self, _cmd, autoCorrectionList); 75 | } 76 | } 77 | 78 | /** 79 | * Creates a list with custom prediction bar items. 80 | * 81 | * Each item has Kayoko's package id set as its bundle identifier to identify them later on. 82 | * 83 | * @return The list of custom items. 84 | */ 85 | static TIAutocorrectionList *kayokoCreateAutocorrectionList() { 86 | NSArray *labels = @[ @"History", @"Copy", @"Paste" ]; 87 | NSMutableArray *candidates = [[NSMutableArray alloc] init]; 88 | for (NSString *label in labels) { 89 | TIZephyrCandidate *candidate = [[objc_getClass("TIZephyrCandidate") alloc] init]; 90 | [candidate setLabel:[[PasteboardManager localizationBundle] localizedStringForKey:label 91 | value:nil 92 | table:@"Tweak"]]; 93 | [candidate setCandidate:[NSString stringWithFormat:@"{kayoko-%@}", label]]; 94 | [candidate setFromBundleId:@"codes.aurora.kayoko"]; 95 | [candidates addObject:candidate]; 96 | } 97 | 98 | return [objc_getClass("TIAutocorrectionList") listWithAutocorrection:nil predictions:candidates emojiList:nil]; 99 | } 100 | 101 | #pragma mark - UIPredictionViewController class hooks 102 | 103 | /** 104 | * Handles the selection of a prediction bar item. 105 | * 106 | * @see kayokoCreateAutocorrectionList to learn how the items are identified. 107 | * 108 | * @param predictionView The prediction bar on which the item was selected. 109 | * @param candidate The item that was selected. 110 | */ 111 | static void (*orig_UIPredictionViewController_predictionView_didSelectCandidate)(UIPredictionViewController *self, 112 | SEL _cmd, 113 | TUIPredictionView *predictionView, 114 | TIZephyrCandidate *candidate); 115 | static void override_UIPredictionViewController_predictionView_didSelectCandidate(UIPredictionViewController *self, 116 | SEL _cmd, 117 | TUIPredictionView *predictionView, 118 | TIZephyrCandidate *candidate) { 119 | if ([candidate respondsToSelector:@selector(fromBundleId)] && 120 | [[candidate fromBundleId] isEqualToString:@"codes.aurora.kayoko"]) { 121 | if ([[candidate candidate] isEqualToString:@"{kayoko-History}"]) { 122 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 123 | (CFStringRef)kNotificationKeyCoreShow, nil, nil, YES); 124 | } else if ([[candidate candidate] isEqualToString:@"{kayoko-Copy}"]) { 125 | if (@available(iOS 15.0, *)) { 126 | UIKBInputDelegateManager *delegateManager = 127 | [[objc_getClass("UIKeyboardImpl") activeInstance] inputDelegateManager]; 128 | UITextRange *range = [delegateManager selectedTextRange]; 129 | NSString *text = [delegateManager textInRange:range]; 130 | 131 | if (![text isEqualToString:@""]) { 132 | [[UIPasteboard generalPasteboard] setString:text]; 133 | } 134 | } else { 135 | id delegate = [[objc_getClass("UIKeyboardImpl") activeInstance] inputDelegate]; 136 | UITextRange *range = [delegate selectedTextRange]; 137 | NSString *text = [delegate textInRange:range]; 138 | 139 | if (![text isEqualToString:@""]) { 140 | [[UIPasteboard generalPasteboard] setString:text]; 141 | } 142 | } 143 | } else if ([[candidate candidate] isEqualToString:@"{kayoko-Paste}"]) { 144 | kayokoPaste(); 145 | } 146 | } else { 147 | orig_UIPredictionViewController_predictionView_didSelectCandidate(self, _cmd, predictionView, candidate); 148 | } 149 | } 150 | 151 | /** 152 | * Makes the prediction bar always visible. 153 | * 154 | * @param delegate 155 | * @param inputViews 156 | * 157 | * @return Whether the prediction bar should be visible or not. 158 | */ 159 | static BOOL override_UIPredictionViewController_isVisibleForInputDelegate_inputViews(UIPredictionViewController *self, 160 | SEL _cmd, id delegate, 161 | id inputViews) { 162 | return YES; 163 | } 164 | 165 | #pragma mark - UIKeyboardLayoutStar class hooks 166 | 167 | /** 168 | * Updates the prediction bar with the custom items once the user entered the numeric keyboard. 169 | * 170 | * @param name The name of the keyplane that was switched to. 171 | */ 172 | static void (*orig_UIKeyboardLayoutStar_setKeyplaneName)(UIKeyboardLayoutStar *self, SEL _cmd, NSString *name); 173 | static void override_UIKeyboardLayoutStar_setKeyplaneName(UIKeyboardLayoutStar *self, SEL _cmd, NSString *name) { 174 | orig_UIKeyboardLayoutStar_setKeyplaneName(self, _cmd, name); 175 | 176 | shouldShowCustomSuggestions = [name isEqualToString:@"numbers-and-punctuation"] || 177 | [name isEqualToString:@"numbers-and-punctuation-alternate"]; 178 | 179 | if (@available(iOS 15.0, *)) { 180 | [[[objc_getClass("UIKeyboardImpl") activeInstance] autocorrectionController] setAutocorrectionList:nil]; 181 | } else { 182 | [[[objc_getClass("UIKeyboardImpl") activeInstance] autocorrectionController] setTextSuggestionList:nil]; 183 | } 184 | } 185 | 186 | /** 187 | * Shows the history with the dictation button. 188 | * 189 | * This method usage only works on devices with the old keyboard. 190 | * The modern keyboard has a specific dictation icon on the so-called "Keyboard Dock". 191 | * 192 | * @param point 193 | * 194 | * @return The key that was pressed. 195 | */ 196 | static UIKBTree *(*orig_UIKeyboardLayoutStar_keyHitTest)(UIKeyboardLayoutStar *self, SEL _cmd, CGPoint point); 197 | static UIKBTree *override_UIKeyboardLayoutStar_keyHitTest(UIKeyboardLayoutStar *self, SEL _cmd, CGPoint point) { 198 | UIKBTree *orig = orig_UIKeyboardLayoutStar_keyHitTest(self, _cmd, point); 199 | 200 | // Unset the original action and tell the core to show the history. 201 | if ([[orig name] isEqualToString:@"Dictation-Key"]) { 202 | [[orig properties] setValue:@(0) forKey:@"KBinteractionType"]; 203 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 204 | (CFStringRef)kNotificationKeyCoreShow, nil, nil, YES); 205 | } 206 | 207 | return orig; 208 | } 209 | 210 | /** 211 | * Hides the history when they keyboard was dismissed, if the history is already visible. 212 | */ 213 | static void (*orig_UIKeyboardLayoutStar_didMoveToWindow)(UIKeyboardLayoutStar *self, SEL _cmd); 214 | static void override_UIKeyboardLayoutStar_didMoveToWindow(UIKeyboardLayoutStar *self, SEL _cmd) { 215 | orig_UIKeyboardLayoutStar_didMoveToWindow(self, _cmd); 216 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 217 | (CFStringRef)kNotificationKeyCoreHide, nil, nil, YES); 218 | } 219 | 220 | #pragma mark - UIKeyboardImpl class hooks 221 | 222 | /** 223 | * Makes the dictation key always show. 224 | * 225 | * This applies to devices using the modern keyboard. 226 | * @see keyHitTest for a more in-depth explanation. 227 | * 228 | * @return Whether the dictation key should be shown. 229 | */ 230 | static BOOL override_UIKeyboardImpl_shouldShowDictationKey(UIKeyboardImpl *self, SEL _cmd) { return YES; } 231 | 232 | /** 233 | * Notes that the app became active. 234 | * 235 | * Knowing that, we can prevent pasting from happening in apps that became inactive. 236 | */ 237 | static void (*orig_UIKeyboardImpl_applicationDidBecomeActive)(UIKeyboardImpl *self, SEL _cmd, BOOL didBecomeActive); 238 | static void override_UIKeyboardImpl_applicationDidBecomeActive(UIKeyboardImpl *self, SEL _cmd, BOOL didBecomeActive) { 239 | orig_UIKeyboardImpl_applicationDidBecomeActive(self, _cmd, didBecomeActive); 240 | applicationIsInForeground = YES; 241 | } 242 | 243 | /** 244 | * Notes that the app became inactive. 245 | * 246 | * @see applicationDidBecomeActive why to save the state of an app. 247 | */ 248 | static void (*orig_UIKeyboardImpl_applicationWillResignActive)(UIKeyboardImpl *self, SEL _cmd, BOOL willResignActive); 249 | static void override_UIKeyboardImpl_applicationWillResignActive(UIKeyboardImpl *self, SEL _cmd, BOOL willResignActive) { 250 | orig_UIKeyboardImpl_applicationWillResignActive(self, _cmd, willResignActive); 251 | applicationIsInForeground = NO; 252 | } 253 | 254 | #pragma mark - UISystemKeyboardDockController class hooks 255 | 256 | /** 257 | * Shows the history with the dictation button. 258 | * 259 | * This method usage only works on devices with the modern keyboard. 260 | * @see keyHitTest for a more in-depth explanation. 261 | * 262 | * @param event 263 | */ 264 | static void 265 | override_UISystemKeyboardDockController_dictationItemButtonWasPressed_withEvent(UISystemKeyboardDockController *self, 266 | SEL _cmd, UIEvent *event) { 267 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 268 | (CFStringRef)kNotificationKeyCoreShow, nil, nil, YES); 269 | } 270 | 271 | #pragma mark - _UIEditMenuPresentation class hooks (iOS 16+) 272 | 273 | static void (*orig__UIEditMenuPresentation_displayMenu_configuration_)(id, SEL, UIMenu *, id); 274 | static void override__UIEditMenuPresentation_displayMenu_configuration_(id self, SEL _cmd, UIMenu *menu, 275 | id configuration) { 276 | NSMutableArray *build = [NSMutableArray new]; 277 | for (id item in [menu children]) { 278 | if (KayokoMenuItemIsWritingTool(item)) { 279 | continue; 280 | } 281 | if (![item isKindOfClass:[UIMenu class]]) { 282 | [build addObject:item]; 283 | continue; 284 | } 285 | UIMenu *submenu = item; 286 | if (![submenu.identifier isEqualToString:KayokoAppleMenuIdentifier()]) { 287 | [build addObject:submenu]; 288 | continue; 289 | } 290 | NSMutableArray *rebuildAppleEditMenu = [submenu.children mutableCopy]; 291 | [rebuildAppleEditMenu addObject:KayokoMenuItemUICommand()]; 292 | UIMenu *rebuildAppleMenu = [submenu menuByReplacingChildren:rebuildAppleEditMenu]; 293 | [build addObject:rebuildAppleMenu]; 294 | } 295 | UIMenu *newMenu = [menu menuByReplacingChildren:build]; 296 | orig__UIEditMenuPresentation_displayMenu_configuration_(self, _cmd, newMenu, configuration); 297 | } 298 | 299 | #pragma mark - UICalloutBar class hooks (iOS 15) 300 | 301 | static void (*orig_UICalloutBar_setExtraItems_)(UICalloutBar *, SEL, NSArray *); 302 | static void override_UICalloutBar_setExtraItems_(UICalloutBar *self, SEL _cmd, NSArray *items) { 303 | NSMutableArray *newItems = [NSMutableArray arrayWithCapacity:items.count]; 304 | for (UIMenuItem *item in items) { 305 | NSString *selectorName = NSStringFromSelector(item.action); 306 | if ([selectorName isEqualToString:kayokoSelectorName]) { 307 | item.action = NSSelectorFromString(@"__kayoko_dummy__"); 308 | } 309 | [newItems addObject:item]; 310 | } 311 | orig_UICalloutBar_setExtraItems_(self, _cmd, [newItems copy]); 312 | } 313 | 314 | static void (*orig_UICalloutBar_updateAvailableButtons)(UICalloutBar *, SEL); 315 | static void override_UICalloutBar_updateAvailableButtons(UICalloutBar *self, SEL _cmd) { 316 | Class cbsbdCls = NSClassFromString(@"_UICalloutBarSystemButtonDescription"); 317 | if (!cbsbdCls || ![cbsbdCls respondsToSelector:@selector(buttonDescriptionWithTitle:action:type:)]) { 318 | return orig_UICalloutBar_updateAvailableButtons(self, _cmd); 319 | } 320 | 321 | UIMenuItem *kayokoNowItem = KayokoMenuItem(); 322 | _UICalloutBarSystemButtonDescription *buttonDescription = 323 | [cbsbdCls buttonDescriptionWithTitle:kayokoNowItem.title 324 | action:NSSelectorFromString(kayokoSelectorName) 325 | type:1]; 326 | 327 | if (!buttonDescription) { 328 | return orig_UICalloutBar_updateAvailableButtons(self, _cmd); 329 | } 330 | 331 | Ivar msbd = class_getInstanceVariable(object_getClass(self), "m_systemButtonDescriptions"); 332 | if (!msbd) { 333 | return orig_UICalloutBar_updateAvailableButtons(self, _cmd); 334 | } 335 | 336 | NSMutableArray *buttonDescriptions = object_getIvar(self, msbd); 337 | for (_UICalloutBarSystemButtonDescription *description in buttonDescriptions) { 338 | if (!description.action) { 339 | continue; 340 | } 341 | NSString *selectorName = NSStringFromSelector(description.action); 342 | if ([selectorName isEqualToString:NSStringFromSelector(buttonDescription.action)]) { 343 | return orig_UICalloutBar_updateAvailableButtons(self, _cmd); 344 | } 345 | } 346 | 347 | NSInteger insertIndex = NSNotFound; 348 | NSInteger currentIndex = 0; 349 | for (_UICalloutBarSystemButtonDescription *description in buttonDescriptions) { 350 | if (!description.action) { 351 | continue; 352 | } 353 | if ([NSStringFromSelector(description.action) hasPrefix:@"_"]) { 354 | insertIndex = currentIndex; 355 | break; 356 | } 357 | currentIndex++; 358 | } 359 | 360 | if (insertIndex == 0) { 361 | return orig_UICalloutBar_updateAvailableButtons(self, _cmd); 362 | } 363 | 364 | if (insertIndex == NSNotFound) { 365 | [buttonDescriptions addObject:buttonDescription]; 366 | } else { 367 | [buttonDescriptions insertObject:buttonDescription atIndex:insertIndex]; 368 | } 369 | 370 | return orig_UICalloutBar_updateAvailableButtons(self, _cmd); 371 | } 372 | 373 | #pragma mark - UIResponder additions 374 | 375 | static void addon_UIResponder_openKayoko(id self, SEL _cmd) { 376 | dispatch_async(dispatch_get_main_queue(), ^{ 377 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 378 | (CFStringRef)kNotificationKeyCoreShow, nil, nil, YES); 379 | }); 380 | } 381 | 382 | #pragma mark - Notification callbacks 383 | 384 | /** 385 | * Pastes the last copied item from the history. 386 | */ 387 | static void kayokoPaste() { 388 | if (!applicationIsInForeground) { 389 | return; 390 | } 391 | 392 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 393 | (CFStringRef)kNotificationKeyPasteWillStart, nil, nil, YES); 394 | 395 | UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; 396 | 397 | // Get the latest copied item if the pasteboard cleared itself. 398 | // The pasteboard clears itself after inactivity. 399 | if (![pasteboard string] && ![pasteboard image]) { 400 | PasteboardItem *item = [[PasteboardManager sharedInstance] getLatestHistoryItem]; 401 | if (!item) { 402 | return; 403 | } 404 | 405 | if (![[item imageName] isEqualToString:@""]) { 406 | [pasteboard setImage:[[PasteboardManager sharedInstance] getImageForItem:item]]; 407 | } else { 408 | [pasteboard setString:[item content]]; 409 | } 410 | } 411 | 412 | [[UIApplication sharedApplication] sendAction:@selector(paste:) to:nil from:nil forEvent:nil]; 413 | } 414 | 415 | @interface KayokoKeyboardObserver : NSObject 416 | @end 417 | 418 | @implementation KayokoKeyboardObserver 419 | 420 | - (void)keyboardWillHide:(NSNotification *)notification { 421 | NSDictionary *userInfo = [notification userInfo]; 422 | BOOL isLocalKeyboard = [userInfo[UIKeyboardIsLocalUserInfoKey] boolValue]; 423 | if (!isLocalKeyboard) { 424 | return; 425 | } 426 | 427 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 428 | (CFStringRef)kNotificationKeyCoreHide, nil, nil, YES); 429 | } 430 | 431 | @end 432 | 433 | #pragma mark - Preferences 434 | 435 | /** 436 | * Loads the user's preferences. 437 | */ 438 | static void load_preferences() { 439 | kayokoHelperPreferences = [[NSUserDefaults alloc] 440 | initWithSuiteName:[NSString 441 | stringWithFormat:@"/var/mobile/Library/Preferences/%@.plist", kPreferencesIdentifier]]; 442 | 443 | #if THEOS_PACKAGE_SCHEME_ROOTHIDE 444 | libSandy_applyProfile("Kayoko_RootHide"); 445 | #else 446 | libSandy_applyProfile("Kayoko"); 447 | #endif 448 | 449 | [kayokoHelperPreferences registerDefaults:@{ 450 | kPreferenceKeyEnabled : @(kPreferenceKeyEnabledDefaultValue), 451 | kPreferenceKeyActivationMethod : @(kPreferenceKeyActivationMethodDefaultValue), 452 | kPreferenceKeyAutomaticallyPaste : @(kPreferenceKeyAutomaticallyPasteDefaultValue) 453 | }]; 454 | 455 | kayokoHelperPrefsEnabled = [[kayokoHelperPreferences objectForKey:kPreferenceKeyEnabled] boolValue]; 456 | kayokoHelperPrefsActivationMethod = 457 | [[kayokoHelperPreferences objectForKey:kPreferenceKeyActivationMethod] unsignedIntegerValue]; 458 | kayokoHelperPrefsAutomaticallyPaste = 459 | [[kayokoHelperPreferences objectForKey:kPreferenceKeyAutomaticallyPaste] boolValue]; 460 | } 461 | 462 | #pragma mark - Constructor 463 | 464 | /** 465 | * Initializes the helper. 466 | * 467 | * First it loads the preferences and continues if Kayoko is enabled. 468 | * Secondly it checks if the helper should run in the injected process. 469 | * Finally it sets up the hooks. 470 | */ 471 | __attribute((constructor)) static void initialize() { 472 | load_preferences(); 473 | 474 | if (!kayokoHelperPrefsEnabled) { 475 | return; 476 | } 477 | 478 | if (![NSProcessInfo processInfo]) { 479 | return; 480 | } 481 | 482 | NSString *processName = [[NSProcessInfo processInfo] processName]; 483 | BOOL isSpringBoard = [@"SpringBoard" isEqualToString:processName]; 484 | 485 | BOOL shouldLoad = NO; 486 | NSArray *args = [[objc_getClass("NSProcessInfo") processInfo] arguments]; 487 | NSUInteger count = [args count]; 488 | if (count != 0) { 489 | NSString *executablePath = args[0]; 490 | if (executablePath) { 491 | NSString *processName = [executablePath lastPathComponent]; 492 | BOOL isApplication = [executablePath rangeOfString:@"/Application/"].location != NSNotFound || 493 | [executablePath rangeOfString:@"/Applications/"].location != NSNotFound; 494 | BOOL isFileProvider = [[processName lowercaseString] rangeOfString:@"fileprovider"].location != NSNotFound; 495 | BOOL skip = [processName isEqualToString:@"AdSheet"] || [processName isEqualToString:@"CoreAuthUI"] || 496 | [processName isEqualToString:@"InCallService"] || 497 | [processName isEqualToString:@"MessagesNotificationViewService"] || 498 | [executablePath rangeOfString:@".appex/"].location != NSNotFound; 499 | if ((!isFileProvider && isApplication && !skip) || isSpringBoard) { 500 | shouldLoad = YES; 501 | } 502 | } 503 | } 504 | 505 | if (!shouldLoad) { 506 | return; 507 | } 508 | 509 | // Prediction Bar 510 | if (kayokoHelperPrefsActivationMethod & kActivationMethodPredictionBar) { 511 | if (@available(iOS 15.0, *)) { 512 | MSHookMessageEx(objc_getClass("UIKeyboardAutocorrectionController"), @selector(setAutocorrectionList:), 513 | (IMP)&override_UIKeyboardAutocorrectionController_setAutocorrectionList, 514 | (IMP *)&orig_UIKeyboardAutocorrectionController_setAutocorrectionList); 515 | } else { 516 | MSHookMessageEx(objc_getClass("UIKeyboardAutocorrectionController"), @selector(setTextSuggestionList:), 517 | (IMP)&override_UIKeyboardAutocorrectionController_setTextSuggestionList, 518 | (IMP *)&orig_UIKeyboardAutocorrectionController_setTextSuggestionList); 519 | } 520 | MSHookMessageEx(objc_getClass("UIPredictionViewController"), @selector(isVisibleForInputDelegate:inputViews:), 521 | (IMP)&override_UIPredictionViewController_isVisibleForInputDelegate_inputViews, (IMP *)nil); 522 | MSHookMessageEx(objc_getClass("UIKeyboardLayoutStar"), @selector(setKeyplaneName:), 523 | (IMP)&override_UIKeyboardLayoutStar_setKeyplaneName, 524 | (IMP *)&orig_UIKeyboardLayoutStar_setKeyplaneName); 525 | MSHookMessageEx(objc_getClass("UIPredictionViewController"), @selector(predictionView:didSelectCandidate:), 526 | (IMP)&override_UIPredictionViewController_predictionView_didSelectCandidate, 527 | (IMP *)&orig_UIPredictionViewController_predictionView_didSelectCandidate); 528 | } 529 | 530 | // Dictation Key 531 | if (kayokoHelperPrefsActivationMethod & kActivationMethodDictationKey) { 532 | EnableKayokoActivationDictation(); 533 | MSHookMessageEx(objc_getClass("UISystemKeyboardDockController"), 534 | @selector(dictationItemButtonWasPressed:withEvent:), 535 | (IMP)&override_UISystemKeyboardDockController_dictationItemButtonWasPressed_withEvent, nil); 536 | MSHookMessageEx(objc_getClass("UIKeyboardImpl"), @selector(shouldShowDictationKey), 537 | (IMP)&override_UIKeyboardImpl_shouldShowDictationKey, nil); 538 | MSHookMessageEx(objc_getClass("UIKeyboardLayoutStar"), @selector(keyHitTest:), 539 | (IMP)&override_UIKeyboardLayoutStar_keyHitTest, (IMP *)&orig_UIKeyboardLayoutStar_keyHitTest); 540 | } 541 | 542 | // Input Switcher 543 | if (kayokoHelperPrefsActivationMethod & kActivationMethodInputSwitcher) { 544 | EnableKayokoActivationGlobe(); 545 | } 546 | 547 | // Callout Bar 548 | if (kayokoHelperPrefsActivationMethod & kActivationMethodCalloutBar) { 549 | class_addMethod(NSClassFromString(@"UIResponder"), NSSelectorFromString(kayokoSelectorName), 550 | (IMP)addon_UIResponder_openKayoko, "v@:"); 551 | 552 | if (@available(iOS 16, *)) { 553 | Class targetCls = NSClassFromString(@"_UIEditMenuContentPresentation"); 554 | if (!targetCls) { 555 | targetCls = NSClassFromString(@"_UIEditMenuPresentation"); 556 | } 557 | MSHookMessageEx(targetCls, @selector(displayMenu:configuration:), 558 | (IMP)override__UIEditMenuPresentation_displayMenu_configuration_, 559 | (IMP *)&orig__UIEditMenuPresentation_displayMenu_configuration_); 560 | } else { 561 | MSHookMessageEx(NSClassFromString(@"UICalloutBar"), @selector(setExtraItems:), 562 | (IMP)override_UICalloutBar_setExtraItems_, (IMP *)&orig_UICalloutBar_setExtraItems_); 563 | MSHookMessageEx(NSClassFromString(@"UICalloutBar"), @selector(updateAvailableButtons), 564 | (IMP)override_UICalloutBar_updateAvailableButtons, 565 | (IMP *)&orig_UICalloutBar_updateAvailableButtons); 566 | } 567 | } 568 | 569 | MSHookMessageEx(objc_getClass("UIKeyboardLayoutStar"), @selector(didMoveToWindow), 570 | (IMP)&override_UIKeyboardLayoutStar_didMoveToWindow, 571 | (IMP *)&orig_UIKeyboardLayoutStar_didMoveToWindow); 572 | MSHookMessageEx(objc_getClass("UIKeyboardImpl"), @selector(applicationDidBecomeActive:), 573 | (IMP)&override_UIKeyboardImpl_applicationDidBecomeActive, 574 | (IMP *)&orig_UIKeyboardImpl_applicationDidBecomeActive); 575 | MSHookMessageEx(objc_getClass("UIKeyboardImpl"), @selector(applicationWillResignActive:), 576 | (IMP)&override_UIKeyboardImpl_applicationWillResignActive, 577 | (IMP *)&orig_UIKeyboardImpl_applicationWillResignActive); 578 | 579 | if (kayokoHelperPrefsAutomaticallyPaste) { 580 | CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, 581 | (CFNotificationCallback)kayokoPaste, (CFStringRef)kNotificationKeyHelperPaste, 582 | NULL, (CFNotificationSuspensionBehavior)CFNotificationSuspensionBehaviorDrop); 583 | } 584 | 585 | static KayokoKeyboardObserver *observer; 586 | observer = [[KayokoKeyboardObserver alloc] init]; 587 | 588 | [[NSNotificationCenter defaultCenter] addObserver:observer 589 | selector:@selector(keyboardWillHide:) 590 | name:UIKeyboardWillHideNotification 591 | object:nil]; 592 | } 593 | -------------------------------------------------------------------------------- /Tweak/Helper/KayokoHelper.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.UIKit" ); }; } 2 | -------------------------------------------------------------------------------- /Tweak/Helper/KayokoMenu.h: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoMenu.h 3 | // Kayoko 4 | // 5 | // Created by 82Flex on 2025/3/15. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | FOUNDATION_EXTERN NSString *KayokoAppleMenuIdentifier(void); 13 | 14 | FOUNDATION_EXTERN UIMenuItem *KayokoMenuItem(void); 15 | FOUNDATION_EXTERN UICommand *KayokoMenuItemUICommand(void); 16 | 17 | FOUNDATION_EXTERN BOOL KayokoMenuItemIsWritingTool(id input); 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /Tweak/Helper/KayokoMenu.m: -------------------------------------------------------------------------------- 1 | // 2 | // KayokoMenu.m 3 | // Kayoko 4 | // 5 | // Created by 82Flex on 2025/3/15. 6 | // 7 | 8 | #import "KayokoMenu.h" 9 | #import "KayokoHelper.h" 10 | 11 | NSString *KayokoAppleMenuIdentifier(void) { return @"com.apple.menu.standard-edit"; } 12 | 13 | UIMenuItem *KayokoMenuItem(void) { 14 | static UIMenuItem *menuItem = nil; 15 | if (!menuItem) { 16 | menuItem = [[UIMenuItem alloc] initWithTitle:kayokoMenuName 17 | action:NSSelectorFromString(kayokoSelectorName)]; 18 | } 19 | return menuItem; 20 | } 21 | 22 | UICommand *KayokoMenuItemUICommand(void) { 23 | static UICommand *command = nil; 24 | if (!command) { 25 | command = [UICommand commandWithTitle:kayokoMenuName 26 | image:nil 27 | action:NSSelectorFromString(kayokoSelectorName) 28 | propertyList:nil]; 29 | } 30 | return command; 31 | } 32 | 33 | BOOL KayokoMenuItemIsWritingTool(id input) { 34 | if ([input isKindOfClass:[UIMenu class]]) { 35 | UIMenu *menu = (UIMenu *)input; 36 | if ([menu.title isEqualToString:KayokoMenuItem().title]) { 37 | return YES; 38 | } 39 | } 40 | if ([input isKindOfClass:[UIAction class]]) { 41 | UIAction *action = (UIAction *)input; 42 | if ([action.title isEqualToString:KayokoMenuItem().title]) { 43 | return YES; 44 | } 45 | } 46 | if ([input isKindOfClass:[UICommand class]]) { 47 | UICommand *command = (UICommand *)input; 48 | NSString *selectorName = NSStringFromSelector(command.action); 49 | if ([selectorName isEqualToString:kayokoSelectorName]) { 50 | return YES; 51 | } 52 | } 53 | return NO; 54 | } 55 | -------------------------------------------------------------------------------- /Tweak/Helper/KeyokoHelperLogos.xm: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @import Foundation; 5 | @import UIKit; 6 | 7 | #import "KayokoHelper.h" 8 | #import "NotificationKeys.h" 9 | #import "PasteboardManager.h" 10 | 11 | #define ITEM_ID "codes.aurora.kayoko.globe" 12 | 13 | @interface UIInputSwitcherItem : NSObject 14 | @property(nonatomic, copy) NSString *identifier; 15 | @property(nonatomic, copy) NSString *localizedTitle; 16 | @property(nonatomic, copy) NSString *localizedSubtitle; 17 | @property(nonatomic, strong) UIFont *titleFont; 18 | @property(nonatomic, strong) UIFont *subtitleFont; 19 | @property(assign, nonatomic) BOOL usesDeviceLanguage; 20 | @property(nonatomic, strong) UISwitch *switchControl; 21 | @property(nonatomic, copy) id switchIsOnBlock; 22 | @property(nonatomic, copy) id switchToggleBlock; 23 | - (instancetype)initWithIdentifier:(NSString *)identifier; 24 | @end 25 | 26 | %group KayokoActivationGlobe 27 | 28 | %hook UIInputSwitcherView 29 | 30 | - (void)_reloadInputSwitcherItems { 31 | %orig; 32 | BOOL isForDictation = MSHookIvar(self, "m_isForDictation"); 33 | if (isForDictation) { 34 | return; 35 | } 36 | NSArray *items = MSHookIvar(self, "m_inputSwitcherItems"); 37 | NSMutableArray *newItems = [NSMutableArray arrayWithArray:items]; 38 | UIInputSwitcherItem *item = [[%c(UIInputSwitcherItem) alloc] initWithIdentifier:@ITEM_ID]; 39 | [item setLocalizedTitle:[[PasteboardManager localizationBundle] localizedStringForKey:@"Kayoko" 40 | value:nil 41 | table:@"Tweak"]]; 42 | if (item) { 43 | [newItems insertObject:item atIndex:newItems.count - 1]; 44 | } 45 | MSHookIvar(self, "m_inputSwitcherItems") = newItems; 46 | } 47 | 48 | - (void)didSelectItemAtIndex:(unsigned long long)index { 49 | NSArray *items = MSHookIvar(self, "m_inputSwitcherItems"); 50 | UIInputSwitcherItem *item = items[index]; 51 | if ([item.identifier isEqualToString:@ITEM_ID]) { 52 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 53 | (CFStringRef)kNotificationKeyCoreShow, nil, nil, YES); 54 | } 55 | %orig; 56 | } 57 | 58 | %end 59 | 60 | %end // KayokoActivationGlobe 61 | 62 | %group KayokoActivationDictation 63 | 64 | %hook UIKeyboardDockItem 65 | 66 | - (id)initWithImageName:(id)arg1 identifier:(id)arg2 { 67 | if ([arg1 isEqualToString:@"mic"]) { 68 | if (@available(iOS 16, *)) { 69 | arg1 = @"list.clipboard"; 70 | } else { 71 | arg1 = @"doc.on.clipboard"; 72 | } 73 | } 74 | return %orig; 75 | } 76 | 77 | - (void)setImageName:(NSString *)arg1 { 78 | if ([arg1 isEqualToString:@"mic"]) { 79 | if (@available(iOS 16, *)) { 80 | arg1 = @"list.clipboard"; 81 | } else { 82 | arg1 = @"doc.on.clipboard"; 83 | } 84 | } 85 | %orig; 86 | } 87 | 88 | %end 89 | 90 | %hook UIKeyboardDockItemButton 91 | 92 | - (CGRect)imageRectForContentRect:(CGRect)arg1 { 93 | CGRect origRect = %orig; 94 | if (@available(iOS 16, *)) { 95 | if (ABS(origRect.size.width - origRect.size.height) > 1.0) { 96 | CGSize newSize = CGSizeMake(origRect.size.width * 0.92, origRect.size.height * 0.92); 97 | CGPoint newOrigin = CGPointMake(origRect.origin.x + (origRect.size.width - newSize.width) / 2, origRect.origin.y + (origRect.size.height - newSize.height) / 2); 98 | return CGRectMake(newOrigin.x, newOrigin.y, newSize.width, newSize.height); 99 | } 100 | } else { 101 | if (ABS(origRect.size.width - origRect.size.height) > 1.0) { 102 | CGSize newSize = CGSizeMake(origRect.size.width * 0.86, origRect.size.height * 0.86); 103 | CGPoint newOrigin = CGPointMake(origRect.origin.x + (origRect.size.width - newSize.width) / 2, origRect.origin.y + (origRect.size.height - newSize.height) / 2); 104 | return CGRectMake(newOrigin.x, newOrigin.y, newSize.width, newSize.height); 105 | } 106 | } 107 | return origRect; 108 | } 109 | 110 | %end 111 | 112 | %hook UISystemKeyboardDockController 113 | 114 | - (void)dictationItemButtonWasPressed:(id)arg1 withEvent:(id)arg2 isRunningButton:(BOOL)arg3 { 115 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 116 | (CFStringRef)kNotificationKeyCoreShow, nil, nil, YES); 117 | } 118 | 119 | %end 120 | 121 | %end // KayokoActivationDictation 122 | 123 | 124 | void EnableKayokoActivationGlobe(void) { 125 | %init(KayokoActivationGlobe); 126 | } 127 | 128 | void EnableKayokoActivationDictation(void) { 129 | %init(KayokoActivationDictation); 130 | } -------------------------------------------------------------------------------- /Tweak/Helper/Makefile: -------------------------------------------------------------------------------- 1 | TWEAK_NAME := KayokoHelper 2 | 3 | KayokoHelper_FILES += KayokoHelper.m 4 | KayokoHelper_FILES += KeyokoHelperLogos.xm 5 | KayokoHelper_FILES += KayokoMenu.m 6 | KayokoHelper_FILES += $(wildcard ../../Manager/*.m ../../Utils/*.m) 7 | 8 | ifeq ($(THEOS_PACKAGE_SCHEME),roothide) 9 | KayokoHelper_FILES += ../../libroot/dyn.c 10 | endif 11 | 12 | KayokoHelper_CFLAGS += -fobjc-arc 13 | KayokoHelper_CFLAGS += -I../../Headers 14 | KayokoHelper_CFLAGS += -I../../Preferences 15 | KayokoHelper_CFLAGS += -I../../Manager 16 | KayokoHelper_CFLAGS += -I../../Utils 17 | 18 | ifeq ($(THEOS_PACKAGE_SCHEME),roothide) 19 | KayokoHelper_LDFLAGS += -L../../Libraries/_roothide 20 | else 21 | ifeq ($(THEOS_PACKAGE_SCHEME),rootless) 22 | KayokoHelper_LDFLAGS += -L../../Libraries/_rootless 23 | else 24 | KayokoHelper_LDFLAGS += -L../../Libraries 25 | endif 26 | endif 27 | 28 | KayokoHelper_FRAMEWORKS += UIKit 29 | KayokoHelper_LIBRARIES += sandy 30 | 31 | include $(THEOS)/makefiles/common.mk 32 | include $(THEOS_MAKE_PATH)/aggregate.mk 33 | include $(THEOS_MAKE_PATH)/tweak.mk 34 | -------------------------------------------------------------------------------- /Utils/AlertUtil.h: -------------------------------------------------------------------------------- 1 | // 2 | // AlertUtil.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface AlertUtil : NSObject 11 | + (void)showAlertWithTitle:(NSString *)title 12 | andMessage:(NSString *)message 13 | withDismissButtonTitle:(NSString *)dismissButtonTitle; 14 | @end 15 | -------------------------------------------------------------------------------- /Utils/AlertUtil.m: -------------------------------------------------------------------------------- 1 | // 2 | // AlertUtil.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "AlertUtil.h" 9 | 10 | @implementation AlertUtil 11 | 12 | /** 13 | * Shows a core foundation alert. 14 | * 15 | * @param title The title of the alert. 16 | * @param message The message displayed on the alert. 17 | * @param dismissButtonTitle The title of the dismiss button. 18 | */ 19 | + (void)showAlertWithTitle:(NSString *)title 20 | andMessage:(NSString *)message 21 | withDismissButtonTitle:(NSString *)dismissButtonTitle { 22 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 23 | NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; 24 | [dictionary setObject:title forKey:(__bridge NSString *)kCFUserNotificationAlertHeaderKey]; 25 | [dictionary setObject:message forKey:(__bridge NSString *)kCFUserNotificationAlertMessageKey]; 26 | [dictionary setObject:dismissButtonTitle forKey:(__bridge NSString *)kCFUserNotificationDefaultButtonTitleKey]; 27 | 28 | CFUserNotificationRef alert = CFUserNotificationCreate(NULL, 0, kCFUserNotificationPlainAlertLevel, nil, 29 | (__bridge CFDictionaryRef)dictionary); 30 | CFRelease(alert); 31 | }); 32 | } 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /Utils/ImageUtil.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImageUtil.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface ImageUtil : NSObject 11 | + (BOOL)imageHasAlpha:(UIImage *)image; 12 | + (UIImage *)getRotatedImageFromImage:(UIImage *)image; 13 | + (UIImage *)getImageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize; 14 | @end 15 | -------------------------------------------------------------------------------- /Utils/ImageUtil.m: -------------------------------------------------------------------------------- 1 | // 2 | // ImageUtil.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "ImageUtil.h" 9 | 10 | @implementation ImageUtil 11 | 12 | /** 13 | * Checks whether an image has an alpha channel or not. 14 | * 15 | * @param image The image object. 16 | * 17 | * @return Whether the image has an alpha channel or not. 18 | */ 19 | + (BOOL)imageHasAlpha:(UIImage *)image { 20 | CGImageAlphaInfo alpha = CGImageGetAlphaInfo([image CGImage]); 21 | return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || 22 | alpha == kCGImageAlphaPremultipliedLast); 23 | } 24 | 25 | /** 26 | * Rotates an image up and returns it. 27 | * 28 | * @param image The image to rotate. 29 | * 30 | * @return The rotated image. 31 | */ 32 | + (UIImage *)getRotatedImageFromImage:(UIImage *)image { 33 | if ([image imageOrientation] == UIImageOrientationUp) { 34 | return image; 35 | } 36 | 37 | UIGraphicsBeginImageContext([image size]); 38 | [image drawInRect:CGRectMake(0, 0, image.size.width, image.size.height)]; 39 | UIImage *rotatedImage = UIGraphicsGetImageFromCurrentImageContext(); 40 | UIGraphicsEndImageContext(); 41 | return rotatedImage; 42 | } 43 | 44 | /** 45 | * Scales an image to a given size. 46 | * 47 | * @param image The image to scale. 48 | * @param newSize The size to scale the image to. 49 | * 50 | * @return The scaled image. 51 | */ 52 | + (UIImage *)getImageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { 53 | UIGraphicsBeginImageContext(newSize); 54 | [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; 55 | UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); 56 | UIGraphicsEndImageContext(); 57 | return newImage; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /Utils/StringUtil.h: -------------------------------------------------------------------------------- 1 | // 2 | // StringUtil.h 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import 9 | 10 | @interface StringUtil : NSObject 11 | + (NSString *)getRandomStringWithLength:(NSUInteger)length; 12 | @end 13 | -------------------------------------------------------------------------------- /Utils/StringUtil.m: -------------------------------------------------------------------------------- 1 | // 2 | // StringUtil.m 3 | // Kayoko 4 | // 5 | // Created by Alexandra Aurora Göttlicher 6 | // 7 | 8 | #import "StringUtil.h" 9 | 10 | @implementation StringUtil 11 | 12 | /** 13 | * Generates a random string of a specified length. 14 | * 15 | * @param length The length of the returned string. 16 | * 17 | * @return The random string. 18 | */ 19 | + (NSString *)getRandomStringWithLength:(NSUInteger)length { 20 | NSString *characters = @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 21 | NSMutableString *string = [NSMutableString stringWithCapacity:length]; 22 | 23 | for (NSUInteger i = 0; i < length; i++) { 24 | [string appendFormat:@"%c", [characters characterAtIndex:arc4random_uniform([characters length])]]; 25 | } 26 | 27 | return string; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: codes.aurora.kayoko 2 | Name: Kayoko 3 | Version: 2.0 4 | Architecture: iphoneos-arm 5 | Description: Clipboard manager for iOS. 6 | Maintainer: Lessica <82flex@gmail.com> 7 | Author: Alexandra Aurora Göttlicher 8 | Section: Tweaks 9 | Depends: firmware (>= 14.0), mobilesubstrate, preferenceloader, com.opa334.libsandy 10 | -------------------------------------------------------------------------------- /devkit/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos 4 | export THEOS_PACKAGE_SCHEME= 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | export THEOS_DEVICE_SIMULATOR= 8 | -------------------------------------------------------------------------------- /devkit/roothide.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos-roothide 4 | export THEOS_PACKAGE_SCHEME=roothide 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | export THEOS_DEVICE_SIMULATOR= 8 | -------------------------------------------------------------------------------- /devkit/rootless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos 4 | export THEOS_PACKAGE_SCHEME=rootless 5 | export THEOS_DEVICE_IP=127.0.0.1 6 | export THEOS_DEVICE_PORT=58422 7 | export THEOS_DEVICE_SIMULATOR= 8 | -------------------------------------------------------------------------------- /devkit/sim-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$THEOS_DEVICE_SIMULATOR" ]; then 4 | exit 0 5 | fi 6 | 7 | cd $(dirname $0)/.. 8 | 9 | TWEAK_NAMES="DictationInKeyboard" 10 | 11 | for TWEAK_NAME in $TWEAK_NAMES; do 12 | sudo rm -f /opt/simject/$TWEAK_NAME.dylib 13 | sudo cp -v $THEOS_OBJ_DIR/$TWEAK_NAME.dylib /opt/simject/$TWEAK_NAME.dylib 14 | sudo codesign -f -s - /opt/simject/$TWEAK_NAME.dylib 15 | sudo cp -v $PWD/$TWEAK_NAME.plist /opt/simject 16 | done 17 | 18 | resim -------------------------------------------------------------------------------- /devkit/sim-launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$THEOS_DEVICE_SIMULATOR" ]; then 4 | exit 0 5 | fi 6 | 7 | cd $(dirname $0)/.. 8 | 9 | DEVICE_ID="6B660A64-7801-4D7B-9161-74C6737432AC" 10 | XCODE_PATH=$(xcode-select -p) 11 | 12 | xcrun simctl boot $DEVICE_ID 13 | open $XCODE_PATH/Applications/Simulator.app 14 | -------------------------------------------------------------------------------- /devkit/simulator.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export THEOS=$HOME/theos 4 | export THEOS_PACKAGE_SCHEME= 5 | export THEOS_DEVICE_IP= 6 | export THEOS_DEVICE_PORT= 7 | export THEOS_DEVICE_SIMULATOR=1 8 | -------------------------------------------------------------------------------- /layout/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | killall -9 druid pasted || true 4 | -------------------------------------------------------------------------------- /layout/DEBIAN/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | killall -9 druid pasted || true 4 | -------------------------------------------------------------------------------- /layout/Library/libSandy/Kayoko.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AllowedProcesses 6 | 7 | * 8 | 9 | Extensions 10 | 11 | 12 | type 13 | file 14 | extension_class 15 | com.apple.app-sandbox.read-write 16 | path 17 | /var/mobile/Library/Preferences/codes.aurora.kayoko.preferences.plist 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /layout/Library/libSandy/Kayoko_RootHide.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AllowedProcesses 6 | 7 | * 8 | 9 | Extensions 10 | 11 | 12 | type 13 | file 14 | extension_class 15 | com.apple.app-sandbox.read-write 16 | path 17 | /rootfs/var/mobile/Library/Preferences/codes.aurora.kayoko.preferences.plist 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /libroot/dyn.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #if THEOS_PACKAGE_SCHEME_ROOTHIDE 12 | #include 13 | #endif 14 | 15 | static const char *(*dyn_get_root_prefix)(void) = NULL; 16 | static const char *(*dyn_get_jbroot_prefix)(void) = NULL; 17 | static const char *(*dyn_get_boot_uuid)(void) = NULL; 18 | static char *(*dyn_jbrootpath)(const char *path, char *resolvedPath) = NULL; 19 | static char *(*dyn_rootfspath)(const char *path, char *resolvedPath) = NULL; 20 | 21 | #if TARGET_OS_SIMULATOR 22 | 23 | static const char *libroot_get_root_prefix_fallback(void) 24 | { 25 | return IPHONE_SIMULATOR_ROOT; 26 | } 27 | 28 | static const char *libroot_get_jbroot_prefix_fallback(void) 29 | { 30 | return IPHONE_SIMULATOR_ROOT; 31 | } 32 | 33 | #else 34 | 35 | #if THEOS_PACKAGE_SCHEME_ROOTHIDE 36 | 37 | static const char *libroot_get_root_prefix_fallback(void) 38 | { 39 | char *resolved = (char *)rootfs("/"); 40 | int len = strlen(resolved); 41 | if (len > 1 && resolved[len - 1] == '/') { 42 | resolved[len - 1] = '\0'; 43 | } 44 | return resolved; 45 | } 46 | 47 | static const char *libroot_get_jbroot_prefix_fallback(void) 48 | { 49 | char *resolved = (char *)jbroot("/"); 50 | int len = strlen(resolved); 51 | if (len > 1 && resolved[len - 1] == '/') { 52 | resolved[len - 1] = '\0'; 53 | } 54 | return resolved; 55 | } 56 | 57 | #else 58 | 59 | #if IPHONEOS_ARM64 60 | 61 | static const char *libroot_get_root_prefix_fallback(void) 62 | { 63 | return ""; 64 | } 65 | 66 | static const char *libroot_get_jbroot_prefix_fallback(void) 67 | { 68 | return "/var/jb"; 69 | } 70 | 71 | #else 72 | 73 | static const char *libroot_get_root_prefix_fallback(void) 74 | { 75 | return ""; 76 | } 77 | 78 | static const char *libroot_get_jbroot_prefix_fallback(void) 79 | { 80 | if (access("/var/LIY", F_OK) == 0) { 81 | // Legacy support for XinaA15 1.x (For those two people still using it) 82 | // Technically this should be deprecated, but with the libroot solution it's not the hardest thing in the world to maintain 83 | // So I decided to leave it in 84 | return "/var/jb"; 85 | } 86 | else { 87 | return ""; 88 | } 89 | } 90 | 91 | #endif 92 | #endif 93 | #endif 94 | 95 | static const char *libroot_get_boot_uuid_fallback(void) 96 | { 97 | return "00000000-0000-0000-0000-000000000000"; 98 | } 99 | 100 | static char *libroot_rootfspath_fallback(const char *path, char *resolvedPath) 101 | { 102 | if (!path) return NULL; 103 | if (!resolvedPath) resolvedPath = malloc(PATH_MAX); 104 | 105 | const char *prefix = libroot_dyn_get_root_prefix(); 106 | const char *jbRootPrefix = libroot_dyn_get_jbroot_prefix(); 107 | size_t jbRootPrefixLen = strlen(jbRootPrefix); 108 | 109 | if (path[0] == '/') { 110 | // This function has two different purposes 111 | // If what we have is a subpath of the jailbreak root, strip the jailbreak root prefix 112 | // Else, add the rootfs prefix 113 | if (!strncmp(path, jbRootPrefix, jbRootPrefixLen)) { 114 | strlcpy(resolvedPath, &path[jbRootPrefixLen], PATH_MAX); 115 | } 116 | else { 117 | strlcpy(resolvedPath, prefix, PATH_MAX); 118 | strlcat(resolvedPath, path, PATH_MAX); 119 | } 120 | } 121 | else { 122 | // Don't modify relative paths 123 | strlcpy(resolvedPath, path, PATH_MAX); 124 | } 125 | 126 | return resolvedPath; 127 | } 128 | 129 | static char *libroot_jbrootpath_fallback(const char *path, char *resolvedPath) 130 | { 131 | if (!path) return NULL; 132 | if (!resolvedPath) resolvedPath = malloc(PATH_MAX); 133 | 134 | const char *prefix = libroot_dyn_get_jbroot_prefix(); 135 | bool skipRedirection = path[0] != '/'; // Don't redirect relative paths 136 | 137 | #ifndef IPHONEOS_ARM64 138 | // Special case 139 | // On XinaA15 v1: Don't redirect /var/mobile paths to /var/jb/var/mobile 140 | if (!skipRedirection) { 141 | if (access("/var/LIY", F_OK) == 0) { 142 | skipRedirection = strncmp(path, "/var/mobile", 11) == 0; 143 | } 144 | } 145 | #endif 146 | 147 | if (!skipRedirection) { 148 | strlcpy(resolvedPath, prefix, PATH_MAX); 149 | strlcat(resolvedPath, path, PATH_MAX); 150 | } 151 | else { 152 | strlcpy(resolvedPath, path, PATH_MAX); 153 | } 154 | 155 | return resolvedPath; 156 | } 157 | 158 | static void libroot_load(void) 159 | { 160 | static dispatch_once_t onceToken; 161 | dispatch_once (&onceToken, ^{ 162 | void *handle = dlopen("@rpath/libroot.dylib", RTLD_NOW); 163 | if (handle) { 164 | dyn_get_root_prefix = dlsym(handle, "libroot_get_root_prefix"); 165 | dyn_get_jbroot_prefix = dlsym(handle, "libroot_get_jbroot_prefix"); 166 | dyn_get_boot_uuid = dlsym(handle, "libroot_get_boot_uuid"); 167 | dyn_jbrootpath = dlsym(handle, "libroot_jbrootpath"); 168 | dyn_rootfspath = dlsym(handle, "libroot_rootfspath"); 169 | } 170 | if (!dyn_get_root_prefix) dyn_get_root_prefix = libroot_get_root_prefix_fallback; 171 | if (!dyn_get_jbroot_prefix) dyn_get_jbroot_prefix = libroot_get_jbroot_prefix_fallback; 172 | if (!dyn_get_boot_uuid) dyn_get_boot_uuid = libroot_get_boot_uuid_fallback; 173 | if (!dyn_jbrootpath) dyn_jbrootpath = libroot_jbrootpath_fallback; 174 | if (!dyn_rootfspath) dyn_rootfspath = libroot_rootfspath_fallback; 175 | }); 176 | } 177 | 178 | const char *libroot_dyn_get_root_prefix(void) 179 | { 180 | libroot_load(); 181 | return dyn_get_root_prefix(); 182 | } 183 | 184 | const char *libroot_dyn_get_jbroot_prefix(void) 185 | { 186 | libroot_load(); 187 | return dyn_get_jbroot_prefix(); 188 | } 189 | 190 | const char *libroot_dyn_get_boot_uuid(void) 191 | { 192 | libroot_load(); 193 | return dyn_get_boot_uuid(); 194 | } 195 | 196 | char *libroot_dyn_rootfspath(const char *path, char *resolvedPath) 197 | { 198 | libroot_load(); 199 | return dyn_rootfspath(path, resolvedPath); 200 | } 201 | 202 | char *libroot_dyn_jbrootpath(const char *path, char *resolvedPath) 203 | { 204 | libroot_load(); 205 | return dyn_jbrootpath(path, resolvedPath); 206 | } --------------------------------------------------------------------------------