├── .gitignore ├── AlbumManager.plist ├── LICENSE ├── Makefile ├── cctoggle ├── AlbumManagerCCToggle.h ├── AlbumManagerCCToggle.m ├── Makefile └── Resources │ ├── Info.plist │ ├── SettingsIcon@2x.png │ └── SettingsIcon@3x.png ├── control ├── headers ├── AlbumManager │ ├── AlbumManager.h │ └── Preferences.h ├── Log.h ├── Photos+Private.h ├── PhotosUICore.h └── PhotosUIPrivate.h ├── layout ├── DEBIAN │ ├── postinst │ └── postrm └── Library │ └── libSandy │ └── AlbumManager_FileAccess.plist ├── preferences ├── AlbumManagerRootListController.m ├── CustomCells │ ├── AlbumManagerButton.m │ └── AlbumManagerSwitch.m ├── Makefile ├── Resources │ ├── Root.plist │ ├── icon.png │ ├── icon@2x.png │ └── icon@3x.png └── layout │ └── Library │ └── PreferenceLoader │ └── Preferences │ └── AlbumManager.plist └── src ├── AlbumManager.m ├── Tweak.h └── Tweak.x /.gitignore: -------------------------------------------------------------------------------- 1 | .theos/ 2 | packages/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /AlbumManager.plist: -------------------------------------------------------------------------------- 1 | { 2 | Filter = { 3 | Bundles = ( 4 | "com.apple.mobileslideshow", 5 | "com.apple.mobileslideshow.PhotosMessagesApp", 6 | "com.apple.mobileslideshow.photo-picker", 7 | "com.apple.mobileslideshow.photospicker", 8 | "com.apple.PhotosUICore", 9 | "com.apple.tccd", 10 | "com.apple.UIKit" 11 | ); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 NoisyFlake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:14.5:15.0 2 | INSTALL_TARGET_PROCESSES = SpringBoard MobileSlideShow 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | TWEAK_NAME = AlbumManager 7 | 8 | AlbumManager_FILES = src/Tweak.x src/AlbumManager.m 9 | AlbumManager_LIBRARIES = sandy 10 | AlbumManager_CFLAGS = -fobjc-arc -Wno-deprecated 11 | 12 | include $(THEOS_MAKE_PATH)/tweak.mk 13 | SUBPROJECTS += preferences 14 | SUBPROJECTS += cctoggle 15 | include $(THEOS_MAKE_PATH)/aggregate.mk 16 | -------------------------------------------------------------------------------- /cctoggle/AlbumManagerCCToggle.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "../headers/Log.h" 4 | #import "../headers/AlbumManager/AlbumManager.h" 5 | #import "../headers/AlbumManager/Preferences.h" 6 | 7 | @interface AlbumManagerCCToggle : CCUIToggleModule 8 | 9 | @end 10 | -------------------------------------------------------------------------------- /cctoggle/AlbumManagerCCToggle.m: -------------------------------------------------------------------------------- 1 | #import "AlbumManagerCCToggle.h" 2 | 3 | @implementation AlbumManagerCCToggle 4 | 5 | - (UIImage *)iconGlyph { 6 | UIImageSymbolConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:25 weight:UIImageSymbolWeightMedium scale:UIImageSymbolScaleLarge]; 7 | return [UIImage systemImageNamed:@"lock.slash" withConfiguration:configuration]; 8 | } 9 | 10 | - (UIImage *)selectedIconGlyph { 11 | UIImageSymbolConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:25 weight:UIImageSymbolWeightMedium scale:UIImageSymbolScaleLarge]; 12 | return [UIImage systemImageNamed:@"lock" withConfiguration:configuration]; 13 | } 14 | 15 | - (UIColor *)selectedColor { 16 | return kAlbumManagerColor; 17 | } 18 | 19 | - (BOOL)isSelected { 20 | AlbumManager *manager = [AlbumManager sharedInstance]; 21 | return [[manager objectForKey:@"showLockedAlbums"] boolValue]; 22 | } 23 | 24 | - (void)setSelected:(BOOL)selected { 25 | AlbumManager *manager = [AlbumManager sharedInstance]; 26 | [manager setObject:@(selected) forKey:@"showLockedAlbums"]; 27 | 28 | pid_t pid; 29 | int status; 30 | const char* args[] = {"killall", "-9", "MobileSlideShow", NULL}; 31 | posix_spawn(&pid, ROOT_PATH("/usr/bin/killall"), NULL, NULL, (char* const*)args, NULL); 32 | waitpid(pid, &status, WEXITED); 33 | 34 | [manager reloadSettings]; 35 | [super refreshState]; 36 | } 37 | 38 | @end -------------------------------------------------------------------------------- /cctoggle/Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:14.5:15.0 2 | INSTALL_TARGET_PROCESSES = SpringBoard 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | BUNDLE_NAME = AlbumManagerCCToggle 7 | AlbumManagerCCToggle_BUNDLE_EXTENSION = bundle 8 | AlbumManagerCCToggle_FILES = AlbumManagerCCToggle.m ../src/AlbumManager.m 9 | AlbumManagerCCToggle_CFLAGS = -fobjc-arc 10 | AlbumManagerCCToggle_FRAMEWORKS = UIKit 11 | AlbumManagerCCToggle_PRIVATE_FRAMEWORKS = ControlCenterUIKit 12 | AlbumManagerCCToggle_INSTALL_PATH = $(THEOS_PACKAGE_INSTALL_PREFIX)/Library/ControlCenter/Bundles/ 13 | 14 | include $(THEOS_MAKE_PATH)/bundle.mk 15 | -------------------------------------------------------------------------------- /cctoggle/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleName 6 | AlbumManagerCCToggle 7 | CFBundleDisplayName 8 | Locked Albums 9 | CFBundleExecutable 10 | AlbumManagerCCToggle 11 | CFBundleIdentifier 12 | com.noisyflake.albummanagercctoggle 13 | CFBundleDevelopmentRegion 14 | English 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundlePackageType 18 | BNDL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | MinimumOSVersion 24 | 11.0 25 | UIDeviceFamily 26 | 27 | 1 28 | 2 29 | 30 | NSPrincipalClass 31 | AlbumManagerCCToggle 32 | CFBundleSupportedPlatforms 33 | 34 | iPhoneOS 35 | 36 | CCSGetModuleSizeAtRuntime 37 | 38 | CCSModuleSize 39 | 40 | Portrait 41 | 42 | Height 43 | 1 44 | Width 45 | 1 46 | 47 | Landscape 48 | 49 | Height 50 | 1 51 | Width 52 | 1 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /cctoggle/Resources/SettingsIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/AlbumManager/93f1cc1b55fbd775bf9b8e53267c2120528211b5/cctoggle/Resources/SettingsIcon@2x.png -------------------------------------------------------------------------------- /cctoggle/Resources/SettingsIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/AlbumManager/93f1cc1b55fbd775bf9b8e53267c2120528211b5/cctoggle/Resources/SettingsIcon@3x.png -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.noisyflake.albummanager 2 | Name: AlbumManager 3 | Version: 1.0.2 4 | Architecture: iphoneos-arm 5 | Description: Organize and protect your photo albums 6 | Maintainer: NoisyFlake 7 | Author: NoisyFlake 8 | Section: Tweaks 9 | Depends: firmware (>=15.0), mobilesubstrate (>= 0.9.5000), preferenceloader, com.opa334.libsandy, com.opa334.ccsupport 10 | -------------------------------------------------------------------------------- /headers/AlbumManager/AlbumManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import "../Photos+Private.h" 6 | 7 | @interface AlbumManager : NSObject 8 | 9 | @property (nonatomic, retain) NSDictionary *settings; 10 | @property (nonatomic, retain) NSDictionary *defaultSettings; 11 | @property (nonatomic, retain) NSMutableArray *unlockedAlbums; 12 | @property (nonatomic, retain) NSMutableArray *unlockedProtections; 13 | 14 | + (instancetype)sharedInstance; 15 | - (id)objectForKey:(NSString *)key; 16 | - (void)setObject:(id)object forKey:(NSString *)key; 17 | - (void)removeObjectForKey:(NSString *)key; 18 | - (void)reloadSettings; 19 | - (void)resetSettings; 20 | - (NSString *)uuidForCollection:(PHAssetCollection *)collection; 21 | - (void)tryAccessingAlbumWithUUID:(NSString *)uuid forViewController:(UIViewController *)viewController WithCompletion:(void (^)(BOOL success))completion; 22 | - (void)authenticateWithBiometricsForViewController:(UIViewController *)viewController WithCompletion:(void (^)(BOOL success))completion; 23 | - (void)authenticateWithPasswordForHash:(NSString *)hash forViewController:(UIViewController *)viewController WithCompletion:(void (^)(BOOL success))completion; 24 | - (NSString*)sha256HashForText:(NSString*)text; 25 | - (void)resetUnlocks; 26 | - (BOOL)collectionListWantsLock:(PHCollectionList*)list; 27 | @end 28 | 29 | #define PREFERENCES_PATH ROOT_PATH_NS_VAR(@"/var/mobile/Library/Preferences/") 30 | #define PLIST_PATH ROOT_PATH_NS_VAR(@"/var/mobile/Library/Preferences/com.noisyflake.albummanager.plist") -------------------------------------------------------------------------------- /headers/AlbumManager/Preferences.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | #import "AlbumManager.h" 7 | #import "../Log.h" 8 | 9 | @interface NSTask : NSObject 10 | @property (copy) NSArray * arguments; 11 | @property (retain) id standardOutput; 12 | - (void)setLaunchPath:(NSString *)path; 13 | - (void)launch; 14 | - (void)waitUntilExit; 15 | @end 16 | 17 | @interface AlbumManagerRootListController : PSListController 18 | -(void)setupHeader; 19 | -(void)setupFooterVersion; 20 | -(void)resetSettings; 21 | -(void)twitter; 22 | -(void)paypal; 23 | -(void)setTweakEnabled:(id)value specifier:(PSSpecifier *)specifier; 24 | -(void)respring; 25 | -(void)killPhotosApp; 26 | @end 27 | 28 | @interface AlbumManagerButton : PSTableCell 29 | @end 30 | 31 | @interface PSSubtitleSwitchTableCell : PSSwitchTableCell 32 | @end 33 | 34 | @interface AlbumManagerSwitch : PSSubtitleSwitchTableCell 35 | @end 36 | 37 | @interface UINavigationItem (BetterAlarm) 38 | @property (assign,nonatomic) UINavigationBar * navigationBar; 39 | @end 40 | 41 | #define kAlbumManagerColor [UIColor colorWithRed: 1.00 green: 0.56 blue: 0.29 alpha: 1.00] 42 | -------------------------------------------------------------------------------- /headers/Log.h: -------------------------------------------------------------------------------- 1 | #define NSLog(fmt, ...) NSLog((@"[AlbumManager] " fmt), ##__VA_ARGS__) -------------------------------------------------------------------------------- /headers/Photos+Private.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface PHObject (Private) 4 | @property (readonly) NSString * uuid; 5 | @property (readonly) PHPhotoLibrary *photoLibrary; 6 | @end 7 | 8 | @interface PHAsset (Private) 9 | +(id)fetchAssetsWithUUIDs:(id)uuids options:(id)options; 10 | @end 11 | 12 | @interface PHAssetCollection (Private) 13 | @property (readonly) NSString *cloudGUID; 14 | @property (nonatomic,readonly) NSString * title; 15 | @end 16 | 17 | @interface PHQuery : NSObject 18 | @property (readonly) NSPredicate * basePredicate; 19 | @property (nonatomic,copy) PHFetchOptions * fetchOptions; 20 | @end 21 | 22 | @interface PHFetchResult (Private) 23 | @property (nonatomic,readonly) PHFetchOptions * fetchOptions; 24 | @property (nonatomic,readonly) PHPhotoLibrary * photoLibrary; 25 | @end 26 | 27 | @interface __NSArrayM : NSMutableArray 28 | @end 29 | 30 | @interface _PFArray : NSArray 31 | @end 32 | 33 | @interface TCCDService : NSObject 34 | @property (retain, nonatomic) NSString *name; 35 | @end 36 | 37 | @interface PLManagedAsset : NSObject 38 | @property (nonatomic, readonly, retain) id localID; 39 | @property (nonatomic, readonly) NSString *pl_uuid; 40 | -(id)pl_PHAssetFromPhotoLibrary:(id)photoLibrary; 41 | @end -------------------------------------------------------------------------------- /headers/PhotosUICore.h: -------------------------------------------------------------------------------- 1 | @interface PXNavigationListItem : NSObject 2 | @end 3 | 4 | @interface PXNavigationListDisplayAssetCollectionItem : PXNavigationListItem 5 | @property (nonatomic, assign, readonly) PHCollection *collection; 6 | @end 7 | 8 | @interface PXNavigationListAssetCollectionItem : PXNavigationListDisplayAssetCollectionItem 9 | @end 10 | 11 | @interface PXNavigationListController : UIViewController 12 | @end 13 | 14 | @interface PXNavigationListGadget : PXNavigationListController 15 | -(void)resetAlbumLocks:(NSNotification *)notification; 16 | @end 17 | 18 | @interface _UITableViewCellBadge : UIView 19 | @property (nonatomic, strong, readwrite) UILabel *badgeTextLabel; 20 | @end 21 | 22 | @interface PXNavigationListCell : UITableViewCell 23 | @end 24 | 25 | @interface PXGadgetUICollectionViewCell : UICollectionViewCell 26 | @property (nonatomic, strong, readwrite) UIView *gadgetContentView; 27 | @end 28 | 29 | @interface PXAssetReference : NSObject 30 | @property (nonatomic, readonly) id assetCollection; 31 | @end 32 | 33 | @interface PXSectionedObjectReference : NSObject 34 | @end 35 | 36 | @interface PXAssetCollectionReference : PXSectionedObjectReference 37 | @property (nonatomic, readonly) PHAssetCollection *assetCollection; 38 | @end 39 | 40 | @interface PXActionManager : NSObject 41 | @end 42 | 43 | @interface PXAssetCollectionActionManager : PXActionManager 44 | @property (nonatomic, readonly) PXAssetCollectionReference *assetCollectionReference; 45 | @end 46 | 47 | @interface PXPhotoKitAssetCollectionActionManager : PXAssetCollectionActionManager 48 | @end 49 | 50 | @interface PXPhotosViewConfiguration : NSObject 51 | @property (nonatomic, readonly) PXAssetCollectionActionManager *assetCollectionActionManager; 52 | @end 53 | 54 | @interface PXPhotosUIViewController : UIViewController 55 | @property (nonatomic, readonly) PXPhotosViewConfiguration *configuration; 56 | @property (nonatomic, readonly) PXAssetReference *assetReferenceForCurrentScrollPosition; 57 | @end 58 | 59 | 60 | @interface PXActionMenuController : NSObject 61 | @property (nonatomic, readonly) NSArray *actions; 62 | @property (nonatomic, readonly) NSArray *actionManagers; // PXPhotoKitAssetCollectionManager 63 | @property (nonatomic, readonly) NSArray *availableActionTypes; // PXAssetCollectionActionTypeDelete 64 | @end 65 | 66 | @interface PXPhotosGridActionMenuController : PXActionMenuController 67 | @end -------------------------------------------------------------------------------- /headers/PhotosUIPrivate.h: -------------------------------------------------------------------------------- 1 | @interface PUStackView : UIView 2 | @property (nonatomic, retain) UIView *lockView; 3 | -(void)updateLockViewForCollection:(PHCollection *)collection; 4 | @end 5 | 6 | @interface PUAlbumListCellContentView : UIView 7 | @property (setter=_setStackView:,nonatomic,retain) PUStackView* stackView; 8 | @end 9 | 10 | @interface PUAlbumGadget : NSObject 11 | @property(retain, nonatomic) PHCollection *collection; 12 | @property(retain, nonatomic) PUAlbumListCellContentView *albumListCellContentView; 13 | @end 14 | 15 | @interface PXGadgetUIViewController : UICollectionViewController 16 | @end 17 | 18 | @interface PXHorizontalCollectionGadget : PXGadgetUIViewController 19 | @end 20 | 21 | @interface PUHorizontalAlbumListGadget : PXHorizontalCollectionGadget 22 | -(PUAlbumGadget *)gadgetAtLocation:(CGPoint)location inCoordinateSpace:(id)space; 23 | -(PUAlbumGadget *)_gadgetAtIndexPath:(NSIndexPath *)indexPath; 24 | @end 25 | 26 | 27 | 28 | @interface PUSessionInfo : NSObject 29 | @property(retain, nonatomic) PHAssetCollection *targetAlbum; 30 | @property(retain, nonatomic) PHAssetCollection *sourceAlbum; 31 | @property (nonatomic, readwrite, copy) NSOrderedSet *transferredAssets; 32 | @end 33 | 34 | @interface PUAlbumPickerSessionInfo : PUSessionInfo 35 | @end 36 | 37 | @interface PUAlbumListViewController : UIViewController 38 | @property (nonatomic, readwrite, strong) PUSessionInfo *sessionInfo; 39 | @property (nonatomic, readwrite, strong) PHCollection *collection; 40 | -(PHAssetCollection *)collectionAtIndexPath:(NSIndexPath *)indexPath; 41 | @end 42 | 43 | @interface PUAlbumListCell : UICollectionViewCell 44 | @property (nonatomic, strong, readwrite) PUAlbumListCellContentView *albumListCellContentView; 45 | @end -------------------------------------------------------------------------------- /layout/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Restarting tccd" 4 | killall -9 tccd 5 | 6 | exit 0 -------------------------------------------------------------------------------- /layout/DEBIAN/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Restarting tccd" 4 | killall -9 tccd 5 | 6 | exit 0 -------------------------------------------------------------------------------- /layout/Library/libSandy/AlbumManager_FileAccess.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AllowedProcesses 6 | 7 | com.apple.mobileslideshow 8 | 9 | Conditions 10 | 11 | Extensions 12 | 13 | 14 | type 15 | file 16 | extension_class 17 | com.apple.app-sandbox.read-write 18 | path 19 | /var/mobile/Library/Preferences/ 20 | 21 | 22 | type 23 | file 24 | extension_class 25 | com.apple.app-sandbox.read-write 26 | path 27 | /var/jb/var/mobile/Library/Preferences/ 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /preferences/AlbumManagerRootListController.m: -------------------------------------------------------------------------------- 1 | #import "../headers/AlbumManager/Preferences.h" 2 | 3 | @implementation AlbumManagerRootListController 4 | - (NSArray *)specifiers { 5 | if (!_specifiers) { 6 | _specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self]; 7 | } 8 | 9 | return _specifiers; 10 | } 11 | 12 | - (id)readPreferenceValue:(PSSpecifier*)specifier { 13 | AlbumManager *manager = [NSClassFromString(@"AlbumManager") sharedInstance]; 14 | return [manager objectForKey:specifier.properties[@"key"]]; 15 | } 16 | 17 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier*)specifier { 18 | NSString *key = specifier.properties[@"key"]; 19 | 20 | AlbumManager *manager = [NSClassFromString(@"AlbumManager") sharedInstance]; 21 | [manager setObject:value forKey:key]; 22 | 23 | 24 | if ([key isEqualToString:@"rememberUnlock"] && ![value boolValue]) { 25 | // For whatever reason, we need to manually reload the settings here. 26 | // Probably because it's not fast enough via notifications, and we would otherwise access the old settings object instead 27 | [manager reloadSettings]; 28 | [manager setObject:@(NO) forKey:@"unlockSameAuth"]; 29 | [manager reloadSettings]; 30 | [self reloadSpecifiers]; 31 | } 32 | 33 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)@"com.noisyflake.albummanager.preferenceupdate", NULL, NULL, YES); 34 | 35 | if ([key isEqualToString:@"showLockedAlbums"]) { 36 | [self killPhotosApp]; 37 | } 38 | } 39 | 40 | -(void)viewDidLayoutSubviews { 41 | [super viewDidLayoutSubviews]; 42 | 43 | self.navigationItem.navigationBar.tintColor = kAlbumManagerColor; 44 | 45 | [self setupHeader]; 46 | [self setupFooterVersion]; 47 | } 48 | 49 | - (void)viewWillDisappear:(BOOL)animated { 50 | [super viewWillDisappear:animated]; 51 | 52 | self.navigationItem.navigationBar.tintColor = nil; 53 | } 54 | 55 | -(void)setupHeader { 56 | UIView *header = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 122)]; 57 | 58 | UILabel *tweakName = [[UILabel alloc] initWithFrame:CGRectMake(0, 30, self.view.bounds.size.width, 40)]; 59 | [tweakName layoutIfNeeded]; 60 | tweakName.numberOfLines = 1; 61 | tweakName.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 62 | tweakName.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:34.0f]; 63 | tweakName.textColor = kAlbumManagerColor; 64 | tweakName.textAlignment = NSTextAlignmentCenter; 65 | 66 | NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"AlbumManager"]; 67 | [attrString beginEditing]; 68 | [attrString addAttribute:NSFontAttributeName 69 | value:[UIFont fontWithName:@"HelveticaNeue-Medium" size:34.0f] 70 | range:NSMakeRange(0, 5)]; 71 | 72 | [attrString endEditing]; 73 | tweakName.attributedText = attrString; 74 | 75 | UILabel *subtitle = [[UILabel alloc] initWithFrame:CGRectMake(0, 70, self.view.bounds.size.width, 15)]; 76 | subtitle.numberOfLines = 1; 77 | subtitle.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); 78 | subtitle.font = [UIFont fontWithName:@"HelveticaNeue-Light" size:13.0f]; 79 | subtitle.textColor = UIColor.systemGrayColor; 80 | subtitle.textAlignment = NSTextAlignmentCenter; 81 | subtitle.text = [NSString stringWithFormat:@"Organize. Hide. Protect."]; 82 | 83 | [header addSubview:tweakName]; 84 | [header addSubview:subtitle]; 85 | 86 | self.table.tableHeaderView = header; 87 | } 88 | 89 | -(void)setupFooterVersion { 90 | NSString *firstLine = [NSString stringWithFormat:@"AlbumManager %@", PACKAGE_VERSION]; 91 | 92 | NSMutableAttributedString *fullFooter = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\nwith \u2665 by NoisyFlake", firstLine]]; 93 | 94 | [fullFooter beginEditing]; 95 | [fullFooter addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:18] range:NSMakeRange(0, [firstLine length])]; 96 | [fullFooter endEditing]; 97 | 98 | UILabel *footerLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 100)]; 99 | footerLabel.font = [UIFont systemFontOfSize:13]; 100 | footerLabel.textColor = UIColor.systemGrayColor; 101 | footerLabel.numberOfLines = 2; 102 | footerLabel.attributedText = fullFooter; 103 | footerLabel.textAlignment = NSTextAlignmentCenter; 104 | self.table.tableFooterView = footerLabel; 105 | } 106 | 107 | -(void)resetSettings { 108 | AlbumManager *manager = [NSClassFromString(@"AlbumManager") sharedInstance]; 109 | [manager resetSettings]; 110 | [self killPhotosApp]; 111 | 112 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)@"com.noisyflake.albummanager.preferenceupdate", NULL, NULL, YES); 113 | [self reload]; 114 | } 115 | 116 | -(void)twitter { 117 | NSURL *twitter = [NSURL URLWithString:@"twitter://user?screen_name=NoisyFlake"]; 118 | NSURL *web = [NSURL URLWithString:@"http://www.twitter.com/NoisyFlake"]; 119 | 120 | if ([[UIApplication sharedApplication] canOpenURL:twitter]) { 121 | [[UIApplication sharedApplication] openURL:twitter options:@{} completionHandler:nil]; 122 | } else { 123 | [[UIApplication sharedApplication] openURL:web options:@{} completionHandler:nil]; 124 | } 125 | } 126 | 127 | -(void)paypal { 128 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"http://www.paypal.me/NoisyFlake"] options:@{} completionHandler:nil]; 129 | } 130 | 131 | -(void)setTweakEnabled:(id)value specifier:(PSSpecifier *)specifier { 132 | [self setPreferenceValue:value specifier:specifier]; 133 | 134 | self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Respring" style:UIBarButtonItemStylePlain target:self action:@selector(respring)]; 135 | } 136 | 137 | -(void)respring { 138 | NSURL *relaunchURL = [NSURL URLWithString:@"prefs:root=AlbumManager"]; 139 | SBSRelaunchAction *restartAction = [NSClassFromString(@"SBSRelaunchAction") actionWithReason:@"RestartRenderServer" options:SBSRelaunchActionOptionsFadeToBlackTransition targetURL:relaunchURL]; 140 | [[NSClassFromString(@"FBSSystemService") sharedService] sendActions:[NSSet setWithObject:restartAction] withResult:nil]; 141 | } 142 | 143 | -(void)killPhotosApp { 144 | pid_t pid; 145 | int status; 146 | const char* args[] = {"killall", "-9", "MobileSlideShow", NULL}; 147 | posix_spawn(&pid, ROOT_PATH("/usr/bin/killall"), NULL, NULL, (char* const*)args, NULL); 148 | waitpid(pid, &status, WEXITED); 149 | } 150 | @end -------------------------------------------------------------------------------- /preferences/CustomCells/AlbumManagerButton.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/AlbumManager/Preferences.h" 2 | 3 | @implementation AlbumManagerButton 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | if (specifier.properties[@"systemIcon"]) { 9 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 10 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 11 | [specifier setProperty:image forKey:@"iconImage"]; 12 | 13 | self.imageView.tintColor = kAlbumManagerColor; 14 | } 15 | } 16 | 17 | return self; 18 | } 19 | 20 | -(void)layoutSubviews { 21 | [super layoutSubviews]; 22 | 23 | self.textLabel.textColor = kAlbumManagerColor; 24 | self.textLabel.highlightedTextColor = kAlbumManagerColor; 25 | 26 | if (self.specifier.properties[@"systemIcon"]) { 27 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 28 | } 29 | } 30 | @end -------------------------------------------------------------------------------- /preferences/CustomCells/AlbumManagerSwitch.m: -------------------------------------------------------------------------------- 1 | #import "../../headers/AlbumManager/Preferences.h" 2 | 3 | @implementation AlbumManagerSwitch 4 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier specifier:(PSSpecifier *)specifier { 5 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier specifier:specifier]; 6 | 7 | if (self) { 8 | [((UISwitch *)[self control]) setOnTintColor:kAlbumManagerColor]; 9 | 10 | if (specifier.properties[@"systemIcon"]) { 11 | UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithFont:[UIFont systemFontOfSize:25]]; 12 | UIImage *image = [UIImage systemImageNamed:specifier.properties[@"systemIcon"] withConfiguration:config]; 13 | [specifier setProperty:image forKey:@"iconImage"]; 14 | 15 | self.imageView.tintColor = kAlbumManagerColor; 16 | } 17 | 18 | self.detailTextLabel.numberOfLines = 4; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | -(void)layoutSubviews { 25 | [super layoutSubviews]; 26 | 27 | if (self.specifier.properties[@"systemIcon"]) { 28 | self.textLabel.frame = CGRectMake(60, self.textLabel.frame.origin.y, self.textLabel.frame.size.width, self.textLabel.frame.size.height); 29 | self.detailTextLabel.frame = CGRectMake(60, self.detailTextLabel.frame.origin.y, self.detailTextLabel.frame.size.width, self.detailTextLabel.frame.size.height); 30 | } 31 | 32 | } 33 | @end -------------------------------------------------------------------------------- /preferences/Makefile: -------------------------------------------------------------------------------- 1 | TARGET := iphone:clang:14.5:15.0 2 | INSTALL_TARGET_PROCESSES = Preferences 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | BUNDLE_NAME = AlbumManager 7 | 8 | AlbumManager_FILES = $(wildcard AlbumManagerRootListController.m CustomCells/*.m ../src/AlbumManager.m) 9 | AlbumManager_PRIVATE_FRAMEWORKS = Preferences 10 | AlbumManager_INSTALL_PATH = $(THEOS_PACKAGE_INSTALL_PREFIX)/Library/PreferenceBundles 11 | AlbumManager_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' 12 | 13 | include $(THEOS_MAKE_PATH)/bundle.mk -------------------------------------------------------------------------------- /preferences/Resources/Root.plist: -------------------------------------------------------------------------------- 1 | { 2 | items = ( 3 | { 4 | cell = PSSwitchCell; 5 | cellClass = AlbumManagerSwitch; 6 | cellSubtitleText = "Requires a Respring"; 7 | key = enabled; 8 | label = "Enable Tweak"; 9 | // systemIcon = "power"; 10 | set = "setTweakEnabled:specifier:"; 11 | }, 12 | { 13 | cell = PSGroupCell; 14 | label = "Settings"; 15 | }, 16 | { 17 | cell = PSSwitchCell; 18 | cellClass = AlbumManagerSwitch; 19 | cellSubtitleText = "Keep unlocked albums unlocked\nuntil you leave the Photos app"; 20 | key = rememberUnlock; 21 | label = "Keep albums unlocked"; 22 | // systemIcon = "clock.fill"; 23 | }, 24 | { 25 | cell = PSSwitchCell; 26 | cellClass = AlbumManagerSwitch; 27 | cellSubtitleText = "Unlock all albums with the same authentication method at once"; 28 | key = unlockSameAuth; 29 | label = "Unlock all albums"; 30 | // systemIcon = "key.fill"; 31 | }, 32 | { 33 | cell = PSSwitchCell; 34 | cellClass = AlbumManagerSwitch; 35 | cellSubtitleText = "Only applies to the Photos app"; 36 | key = showLockedAlbums; 37 | label = "Show locked albums"; 38 | // systemIcon = "eye.circle.fill"; 39 | }, 40 | { 41 | cell = PSButtonCell; 42 | cellClass = AlbumManagerButton; 43 | action = resetSettings; 44 | label = "Reset Settings"; 45 | confirmation = { 46 | title = "Reset Settings"; 47 | prompt = "Reset all settings and ALBUM LOCKS.\nThis action cannot be undone."; 48 | cancelTitle = "Cancel"; 49 | }; 50 | isDestructive = 1; 51 | // systemIcon = "gobackward"; 52 | }, 53 | { 54 | cell = PSGroupCell; 55 | label = "Developer"; 56 | }, 57 | { 58 | cell = PSButtonCell; 59 | cellClass = AlbumManagerButton; 60 | action = twitter; 61 | label = "Follow on Twitter"; 62 | // systemIcon = "person.crop.circle.fill.badge.plus"; 63 | }, 64 | { 65 | cell = PSButtonCell; 66 | cellClass = AlbumManagerButton; 67 | action = paypal; 68 | label = "Buy me a coffee"; 69 | // systemIcon = "gift.circle.fill"; 70 | } 71 | 72 | ); 73 | title = ""; 74 | } -------------------------------------------------------------------------------- /preferences/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/AlbumManager/93f1cc1b55fbd775bf9b8e53267c2120528211b5/preferences/Resources/icon.png -------------------------------------------------------------------------------- /preferences/Resources/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/AlbumManager/93f1cc1b55fbd775bf9b8e53267c2120528211b5/preferences/Resources/icon@2x.png -------------------------------------------------------------------------------- /preferences/Resources/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoisyFlake/AlbumManager/93f1cc1b55fbd775bf9b8e53267c2120528211b5/preferences/Resources/icon@3x.png -------------------------------------------------------------------------------- /preferences/layout/Library/PreferenceLoader/Preferences/AlbumManager.plist: -------------------------------------------------------------------------------- 1 | { 2 | entry = { 3 | bundle = AlbumManager; 4 | cell = PSLinkCell; 5 | detail = AlbumManagerRootListController; 6 | icon = "icon.png"; 7 | isController = 1; 8 | label = "AlbumManager"; 9 | }; 10 | } -------------------------------------------------------------------------------- /src/AlbumManager.m: -------------------------------------------------------------------------------- 1 | #import "Tweak.h" 2 | 3 | @implementation AlbumManager 4 | 5 | + (instancetype)sharedInstance { 6 | static AlbumManager *sharedInstance = nil; 7 | static dispatch_once_t onceToken; 8 | dispatch_once(&onceToken, ^{ 9 | sharedInstance = [[self alloc] init]; 10 | }); 11 | return sharedInstance; 12 | } 13 | 14 | - (instancetype)init { 15 | self = [super init]; 16 | 17 | if (self) { 18 | NSFileManager *manager = [NSFileManager defaultManager]; 19 | if(![manager fileExistsAtPath:PREFERENCES_PATH isDirectory:nil]) { 20 | if(![manager createDirectoryAtPath:PREFERENCES_PATH withIntermediateDirectories:YES attributes:nil error:nil]) { 21 | NSLog(@"ERROR: Unable to create preferences folder"); 22 | return nil; 23 | } 24 | } 25 | 26 | if(![manager fileExistsAtPath:PLIST_PATH isDirectory:nil]) { 27 | if (![manager createFileAtPath:PLIST_PATH contents:nil attributes:nil]) { 28 | NSLog(@"ERROR: Unable to create preferences file"); 29 | return nil; 30 | } 31 | 32 | [[NSDictionary new] writeToURL:[NSURL fileURLWithPath:PLIST_PATH] error:nil]; 33 | } 34 | 35 | _settings = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:PLIST_PATH] error:nil]; 36 | _unlockedAlbums = [NSMutableArray new]; 37 | _unlockedProtections = [NSMutableArray new]; 38 | 39 | _defaultSettings = [NSDictionary dictionaryWithObjectsAndKeys: 40 | @YES, @"enabled", 41 | @YES, @"rememberUnlock", 42 | @YES, @"unlockSameAuth", 43 | @YES, @"showLockedAlbums", 44 | nil]; 45 | 46 | CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, (CFNotificationCallback)reloadAlbumManagerSettings, CFSTR("com.noisyflake.albummanager.preferenceupdate"), NULL, CFNotificationSuspensionBehaviorCoalesce); 47 | } 48 | 49 | return self; 50 | } 51 | 52 | static void reloadAlbumManagerSettings() { 53 | AlbumManager *manager = [NSClassFromString(@"AlbumManager") sharedInstance]; 54 | [manager reloadSettings]; 55 | } 56 | 57 | - (id)objectForKey:(NSString *)key { 58 | return [_settings objectForKey:key] ?: [_defaultSettings objectForKey:key]; 59 | } 60 | 61 | - (void)setObject:(id)object forKey:(NSString *)key { 62 | NSMutableDictionary *settings = [_settings mutableCopy]; 63 | 64 | [settings setObject:object forKey:key]; 65 | [settings writeToURL:[NSURL fileURLWithPath:PLIST_PATH] error:nil]; 66 | 67 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)@"com.noisyflake.albummanager.preferenceupdate", NULL, NULL, YES); 68 | } 69 | 70 | -(void)removeObjectForKey:(NSString *)key { 71 | NSMutableDictionary *settings = [_settings mutableCopy]; 72 | 73 | [settings removeObjectForKey:key]; 74 | [settings writeToURL:[NSURL fileURLWithPath:PLIST_PATH] error:nil]; 75 | 76 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (CFStringRef)@"com.noisyflake.albummanager.preferenceupdate", NULL, NULL, YES); 77 | } 78 | 79 | -(void)reloadSettings { 80 | _settings = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:PLIST_PATH] error:nil]; 81 | } 82 | 83 | -(void)resetSettings { 84 | _settings = [NSDictionary new]; 85 | [_settings writeToURL:[NSURL fileURLWithPath:PLIST_PATH] error:nil]; 86 | } 87 | 88 | - (NSString *)uuidForCollection:(PHAssetCollection *)collection { 89 | return collection.localIdentifier; 90 | } 91 | 92 | - (void)tryAccessingAlbumWithUUID:(NSString *)uuid forViewController:(UIViewController *)viewController WithCompletion:(void (^)(BOOL success))completion { 93 | NSString *protection = [self objectForKey:uuid]; 94 | 95 | if (protection == nil || 96 | ([[self objectForKey:@"rememberUnlock"] boolValue] && [_unlockedAlbums containsObject:uuid]) || 97 | ([[self objectForKey:@"unlockSameAuth"] boolValue] && [_unlockedProtections containsObject:protection])) { 98 | completion(YES); 99 | return; 100 | } 101 | 102 | if ([protection isEqualToString:@"biometrics"]) { 103 | [self authenticateWithBiometricsForViewController:viewController WithCompletion:^(BOOL success) { 104 | dispatch_async(dispatch_get_main_queue(), ^{ 105 | if (success) { 106 | [_unlockedAlbums addObject:uuid]; 107 | [_unlockedProtections addObject:protection]; 108 | completion(YES); 109 | return; 110 | } 111 | }); 112 | }]; 113 | } else { 114 | [self authenticateWithPasswordForHash:protection forViewController:viewController WithCompletion:^(BOOL success) { 115 | dispatch_async(dispatch_get_main_queue(), ^{ 116 | if (success) { 117 | [_unlockedAlbums addObject:uuid]; 118 | [_unlockedProtections addObject:protection]; 119 | completion(YES); 120 | return; 121 | } 122 | }); 123 | }]; 124 | } 125 | 126 | completion(NO); 127 | } 128 | 129 | - (void)authenticateWithBiometricsForViewController:(UIViewController *)viewController WithCompletion:(void (^)(BOOL success))completion { 130 | LAContext *context = [[LAContext alloc] init]; 131 | NSError *authError = nil; 132 | 133 | if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) { 134 | [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:@"View album" reply:^(BOOL success, NSError *error) { 135 | completion(success); 136 | }]; 137 | } else { 138 | NSString *biometryType = context.biometryType == LABiometryTypeFaceID ? @"Face ID" : @"Touch ID"; 139 | 140 | UIAlertController *authFailed = [UIAlertController alertControllerWithTitle:@"No authentication method" message:[NSString stringWithFormat:@"%@ is currently unavailable", biometryType] preferredStyle:UIAlertControllerStyleAlert]; 141 | UIAlertAction *ok = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}]; 142 | [authFailed addAction:ok]; 143 | 144 | [viewController presentViewController:authFailed animated:YES completion:nil]; 145 | completion(NO); 146 | } 147 | } 148 | 149 | - (void)authenticateWithPasswordForHash:(NSString *)hash forViewController:(UIViewController *)viewController WithCompletion:(void (^)(BOOL success))completion { 150 | NSString *requestedKeyboard = [hash substringToIndex:1]; 151 | NSString *title = [requestedKeyboard isEqualToString:@"c"] ? @"Album Passcode?" : @"Album Password?"; 152 | 153 | UIAlertController *passwordVC = [UIAlertController alertControllerWithTitle:title message:nil preferredStyle:UIAlertControllerStyleAlert]; 154 | hash = [hash substringFromIndex:1]; // Remove keyboard indicator from hash 155 | 156 | [passwordVC addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { 157 | textField.secureTextEntry = YES; 158 | textField.autocorrectionType = UITextAutocorrectionTypeNo; 159 | textField.spellCheckingType = UITextSpellCheckingTypeNo; 160 | textField.keyboardType = [requestedKeyboard isEqualToString:@"c"] ? UIKeyboardTypeNumberPad : UIKeyboardTypeDefault; 161 | }]; 162 | 163 | UIAlertAction *checkPassword = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 164 | NSString *enteredPassword = [passwordVC.textFields[0] text]; 165 | if (enteredPassword.length <= 0) return; 166 | 167 | NSString *passwordHash = [self sha256HashForText:enteredPassword]; 168 | if ([passwordHash isEqualToString:hash]) { 169 | completion(YES); 170 | } else { 171 | passwordVC.textFields[0].text = @""; 172 | [viewController presentViewController:passwordVC animated:YES completion:nil]; 173 | } 174 | 175 | }]; 176 | UIAlertAction *cancelPassword = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){}]; 177 | [passwordVC addAction:checkPassword]; 178 | [passwordVC addAction:cancelPassword]; 179 | 180 | 181 | [viewController presentViewController:passwordVC animated:YES completion:nil]; 182 | } 183 | 184 | -(NSString*)sha256HashForText:(NSString*)text { 185 | const char* utf8chars = [text UTF8String]; 186 | unsigned char result[CC_SHA256_DIGEST_LENGTH]; 187 | CC_SHA256(utf8chars, (CC_LONG)strlen(utf8chars), result); 188 | 189 | NSMutableString *ret = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH*2]; 190 | for(int i = 0; i 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | #import "../headers/Log.h" 9 | #import "../headers/Photos+Private.h" 10 | #import "../headers/PhotosUICore.h" 11 | #import "../headers/PhotosUIPrivate.h" 12 | 13 | #import "../headers/AlbumManager/AlbumManager.h" -------------------------------------------------------------------------------- /src/Tweak.x: -------------------------------------------------------------------------------- 1 | #import 2 | #import "Tweak.h" 3 | 4 | AlbumManager *albumManager; 5 | 6 | 7 | /***************************************************** 8 | ** ** 9 | ** Stock albums front page controller: ** 10 | ** Handle taps, show lock symbol instead of count ** 11 | ** ** 12 | *****************************************************/ 13 | 14 | 15 | %hook PXNavigationListGadget 16 | -(id)_navigateTolistItem:(PXNavigationListAssetCollectionItem *)item animated:(BOOL)animated { 17 | PHAssetCollection *collection = (PHAssetCollection *)item.collection; 18 | NSString *uuid = [albumManager uuidForCollection:collection]; 19 | 20 | id __block orig = nil; 21 | 22 | [albumManager tryAccessingAlbumWithUUID:uuid forViewController:self WithCompletion:^(BOOL success) { 23 | if (success) orig = %orig; 24 | }]; 25 | 26 | return orig; 27 | } 28 | 29 | -(void)_configureCell:(PXNavigationListCell *)cell forListItem:(PXNavigationListAssetCollectionItem *)item textColor:(id)color { 30 | %orig; 31 | 32 | PHAssetCollection *collection = (PHAssetCollection *)item.collection; 33 | NSString *uuid = [albumManager uuidForCollection:collection]; 34 | NSString *protection = [albumManager objectForKey:uuid]; 35 | 36 | if (protection == nil || 37 | ([[albumManager objectForKey:@"rememberUnlock"] boolValue] && [albumManager.unlockedAlbums containsObject:uuid]) || 38 | ([[albumManager objectForKey:@"unlockSameAuth"] boolValue] && [albumManager.unlockedProtections containsObject:protection]) 39 | ) return; 40 | 41 | NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; 42 | attachment.image = [UIImage systemImageNamed:@"lock.fill"]; 43 | attachment.image = [attachment.image imageWithTintColor:UIColor.systemGrayColor]; 44 | NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:attachment]; 45 | 46 | _UITableViewCellBadge *badge = [cell valueForKey:@"badge"]; 47 | UILabel *badgeLabel = badge.badgeTextLabel; 48 | badgeLabel.attributedText = attachmentString; 49 | } 50 | 51 | -(void)viewWillAppear:(BOOL)animated { 52 | %orig; 53 | 54 | // Update the gadgets whenever the view appears (e.g. after using the back button) 55 | UITableView *tableView = self.view.subviews[0]; 56 | [tableView reloadData]; 57 | 58 | // Make sure all albums are locked when the app enters the background 59 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; 60 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetAlbumLocks:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 61 | } 62 | 63 | %new 64 | -(void)resetAlbumLocks:(NSNotification *)notification { 65 | [albumManager resetUnlocks]; 66 | 67 | UITableView *tableView = self.view.subviews[0]; 68 | [tableView reloadData]; 69 | } 70 | %end 71 | 72 | 73 | /***************************************************** 74 | ** ** 75 | ** User albums front page controller: ** 76 | ** Handle taps and trigger the PUStackView update ** 77 | ** ** 78 | *****************************************************/ 79 | 80 | 81 | %hook PUHorizontalAlbumListGadget 82 | -(void)_navigateToCollection:(PHAssetCollection *)collection animated:(BOOL)animated interactive:(BOOL)interactive completion:(id)completion { 83 | NSString *uuid = [albumManager uuidForCollection:collection]; 84 | 85 | [albumManager tryAccessingAlbumWithUUID:uuid forViewController:self WithCompletion:^(BOOL success) { 86 | if (success) %orig; 87 | }]; 88 | } 89 | 90 | -(id)targetPreviewViewForLocation:(CGPoint)location inCoordinateSpace:(id)space { 91 | PHAssetCollection *collection = (PHAssetCollection *)[self gadgetAtLocation:location inCoordinateSpace:space].collection; 92 | NSString *uuid = [albumManager uuidForCollection:collection]; 93 | 94 | // Block the long-press menu on protected albums 95 | return [albumManager objectForKey:uuid] ? nil : %orig; 96 | } 97 | 98 | -(void)collectionView:(id)collectionView willDisplayCell:(PXGadgetUICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { 99 | %orig; 100 | 101 | // Update the lockView whenever a cell is rendered 102 | PUAlbumGadget *gadget = [self _gadgetAtIndexPath:indexPath]; 103 | PUStackView *stackView = gadget.albumListCellContentView.stackView; 104 | PHAssetCollection *collection = (PHAssetCollection *)gadget.collection; 105 | 106 | [stackView updateLockViewForCollection:collection]; 107 | } 108 | 109 | -(void)viewWillAppear:(BOOL)animated { 110 | %orig; 111 | 112 | // Update the gadgets whenever the view appears (e.g. after using the back button) 113 | UICollectionView *collectionView = self.view.subviews[0]; 114 | [collectionView reloadData]; 115 | 116 | // Make sure all albums are locked when the app enters the background 117 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; 118 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetAlbumLocks:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 119 | } 120 | 121 | %new 122 | -(void)resetAlbumLocks:(NSNotification *)notification { 123 | [albumManager resetUnlocks]; 124 | 125 | UICollectionView *collectionView = self.view.subviews[0]; 126 | [collectionView reloadData]; 127 | } 128 | %end 129 | 130 | 131 | /***************************************************** 132 | ** ** 133 | ** User albums "see all" list controller: ** 134 | ** Handle taps and trigger the PUStackView update, ** 135 | ** also move photos out of albums ** 136 | ** ** 137 | *****************************************************/ 138 | 139 | 140 | %hook PUAlbumListViewController 141 | -(void)navigateToCollection:(PHAssetCollection *)collection animated:(BOOL)animated completion:(id)completion { 142 | 143 | // This is the edge-case for the "all photos" album inside a user album 144 | if ([collection isKindOfClass:NSClassFromString(@"PHAssetCollection")] && collection.assetCollectionType == 2 && collection.assetCollectionSubtype == 200) { 145 | if ([self.collection isKindOfClass:NSClassFromString(@"PHCollectionList")]) { 146 | BOOL wantsLock = [albumManager collectionListWantsLock:(PHCollectionList *)self.collection]; 147 | if (wantsLock) { 148 | NSString *message = @"Please unlock all other albums inside this folder to access this album"; 149 | UIAlertController *hint = [UIAlertController alertControllerWithTitle:@"Album locked" message:message preferredStyle:UIAlertControllerStyleAlert]; 150 | UIAlertAction *ok = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){}]; 151 | [hint addAction:ok]; 152 | 153 | [self presentViewController:hint animated:YES completion:nil]; 154 | return; 155 | } 156 | } 157 | } 158 | 159 | NSString *uuid = [albumManager uuidForCollection:collection]; 160 | 161 | [albumManager tryAccessingAlbumWithUUID:uuid forViewController:self WithCompletion:^(BOOL success) { 162 | if (success) %orig; 163 | }]; 164 | } 165 | 166 | -(id)collectionView:(id)collectionView contextMenuConfigurationForItemAtIndexPath:(id)indexPath point:(CGPoint)point { 167 | PHAssetCollection *collection = [self collectionAtIndexPath:indexPath]; 168 | NSString *uuid = [albumManager uuidForCollection:collection]; 169 | 170 | // Block the long-press menu on protected albums 171 | return [albumManager objectForKey:uuid] ? nil : %orig; 172 | } 173 | 174 | -(PUAlbumListCell *)collectionView:(id)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { 175 | PUAlbumListCell *cell = %orig; 176 | 177 | // Update the lock view whenever a cell is rendered 178 | PUAlbumListCellContentView *contentView = cell.albumListCellContentView; 179 | PUStackView *stackView = contentView.stackView; 180 | PHCollection *collection = [self collectionAtIndexPath:indexPath]; 181 | 182 | if (collection == nil) { 183 | PHCollection *mainCollection = self.collection; 184 | 185 | if ([mainCollection isKindOfClass:NSClassFromString(@"PHCollectionList")]) { 186 | // Since collection is nil and the "parent" is a list, this must be the "All photos" folder inside a folder, 187 | // so use the parentCollection lock status for this 188 | collection = mainCollection; 189 | } 190 | } 191 | 192 | [stackView updateLockViewForCollection:collection]; 193 | 194 | return cell; 195 | } 196 | 197 | -(void)viewWillAppear:(BOOL)animated { 198 | %orig; 199 | 200 | // Update the gadgets whenever the view appears (e.g. after using the back button) 201 | UICollectionView *collectionView = self.view.subviews[0]; 202 | [collectionView reloadData]; 203 | 204 | // Make sure all albums are locked when the app enters the background 205 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; 206 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resetAlbumLocks:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 207 | } 208 | 209 | %new 210 | -(void)resetAlbumLocks:(NSNotification *)notification { 211 | [albumManager resetUnlocks]; 212 | 213 | UICollectionView *collectionView = self.view.subviews[0]; 214 | [collectionView reloadData]; 215 | } 216 | 217 | -(void)handleSessionInfoAlbumSelection:(PHAssetCollection *)collection { 218 | NSString *message = [NSString stringWithFormat:@"Do you want to copy or move %@ into this album?", self.sessionInfo.transferredAssets.count > 1 ? @"these photos" : @"this photo"]; 219 | UIAlertController *actionSelect = [UIAlertController alertControllerWithTitle:collection.title message:message preferredStyle:UIAlertControllerStyleActionSheet]; 220 | 221 | UIAlertAction *copy = [UIAlertAction actionWithTitle:@"Copy" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 222 | %orig; 223 | }]; 224 | 225 | UIAlertAction *move = [UIAlertAction actionWithTitle:@"Move" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 226 | %orig; 227 | 228 | PXPhotosUIViewController *currentController = nil; 229 | 230 | UIViewController *presentingVC = self.presentingViewController; 231 | for (UIViewController *controller in presentingVC.childViewControllers) { 232 | for (UIViewController *subController in controller.childViewControllers) { 233 | if ([subController isKindOfClass:NSClassFromString(@"PXPhotosUIViewController")]) { 234 | currentController = (PXPhotosUIViewController *)subController; 235 | break; 236 | } 237 | } 238 | if (currentController) break; 239 | } 240 | 241 | PHAssetCollection *sourceAlbum = currentController.configuration.assetCollectionActionManager.assetCollectionReference.assetCollection; 242 | 243 | [collection.photoLibrary performChanges:^{ 244 | for (PLManagedAsset *managedAsset in self.sessionInfo.transferredAssets) { 245 | PHAsset *asset = [managedAsset pl_PHAssetFromPhotoLibrary:collection.photoLibrary]; 246 | 247 | if (sourceAlbum.assetCollectionType == PHAssetCollectionTypeAlbum) { 248 | PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:sourceAlbum]; 249 | [request removeAssets:@[asset]]; 250 | } else { 251 | PHAssetChangeRequest *request = [PHAssetChangeRequest changeRequestForAsset:asset]; 252 | request.hidden = YES; 253 | } 254 | } 255 | } completionHandler:nil]; 256 | }]; 257 | 258 | UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){}]; 259 | 260 | [actionSelect addAction:copy]; 261 | [actionSelect addAction:move]; 262 | [actionSelect addAction:cancel]; 263 | 264 | [self presentViewController:actionSelect animated:YES completion:nil]; 265 | } 266 | %end 267 | 268 | 269 | /***************************************************** 270 | ** ** 271 | ** Add actions for (un)locking to the album menu ** 272 | ** ** 273 | *****************************************************/ 274 | 275 | 276 | %hook PXPhotosGridActionMenuController 277 | -(NSArray *)actions { 278 | NSMutableArray *actions = [%orig mutableCopy]; 279 | 280 | PHAssetCollection *collection = nil; 281 | 282 | for (id manager in self.actionManagers) { 283 | if ([manager isKindOfClass:NSClassFromString(@"PXPhotoKitAssetCollectionActionManager")]) { 284 | PXPhotoKitAssetCollectionActionManager *collectionManager = (PXPhotoKitAssetCollectionActionManager *)manager; 285 | collection = collectionManager.assetCollectionReference.assetCollection; 286 | } 287 | } 288 | 289 | NSString *uuid = [albumManager uuidForCollection:collection]; 290 | 291 | if (uuid) { 292 | NSString *locked = [albumManager objectForKey:uuid]; 293 | 294 | if (locked) { 295 | UIAction *unlockAction = [UIAction actionWithTitle:@"Remove Album Lock" image:[UIImage systemImageNamed:@"lock.open"] identifier:@"AlbumManagerUnlockAlbum" handler:^(__kindof UIAction* _Nonnull action) { 296 | [albumManager removeObjectForKey:uuid]; 297 | }]; 298 | [actions addObject:unlockAction]; 299 | } else { 300 | UIAction *lockAction = [UIAction actionWithTitle:@"Lock Album" image:[UIImage systemImageNamed:@"lock"] identifier:@"AlbumManagerLockAlbum" handler:^(__kindof UIAction* _Nonnull action) { 301 | 302 | UIViewController *rootVC = [[[[UIApplication sharedApplication] windows] firstObject] rootViewController]; 303 | 304 | LAContext *context = [[LAContext alloc] init]; 305 | BOOL isBiometryAvailable = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil]; 306 | NSString *biometryType = context.biometryType == LABiometryTypeFaceID ? @"Face ID" : @"Touch ID"; 307 | 308 | UIAlertController *authTypeVC = [UIAlertController alertControllerWithTitle:@"Authentication Method" message:nil preferredStyle:UIAlertControllerStyleActionSheet]; 309 | 310 | UIAlertAction *biometrics = [UIAlertAction actionWithTitle:[NSString stringWithFormat:@" Lock with %@", biometryType] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 311 | [albumManager setObject:@"biometrics" forKey:uuid]; 312 | }]; 313 | [biometrics setValue:[[UIImage systemImageNamed:context.biometryType == LABiometryTypeFaceID ? @"faceid" : @"touchid"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forKey:@"image"]; 314 | [biometrics setValue:kCAAlignmentLeft forKey:@"titleTextAlignment"]; 315 | BOOL isXina = [[NSFileManager defaultManager] fileExistsAtPath:@"/var/LIY"]; 316 | BOOL isFaceId = context.biometryType == LABiometryTypeFaceID; 317 | biometrics.enabled = isBiometryAvailable && (!isXina || !isFaceId); 318 | 319 | UIAlertAction *password = [UIAlertAction actionWithTitle:@"Lock with Password" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 320 | UIAlertController *passwordVC = [UIAlertController alertControllerWithTitle:@"Set Password" message:nil preferredStyle:UIAlertControllerStyleAlert]; 321 | [passwordVC addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { 322 | textField.autocorrectionType = UITextAutocorrectionTypeNo; 323 | textField.spellCheckingType = UITextSpellCheckingTypeNo; 324 | }]; 325 | 326 | UIAlertAction *acceptPassword = [UIAlertAction actionWithTitle:@"Lock Album" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 327 | NSString *passwordCleartext = [passwordVC.textFields[0] text]; 328 | if (passwordCleartext.length <= 0) return; 329 | 330 | NSString *passwordHash = [albumManager sha256HashForText:passwordCleartext]; 331 | [albumManager setObject:[NSString stringWithFormat:@"p%@", passwordHash] forKey:uuid]; 332 | }]; 333 | UIAlertAction *cancelPassword = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){}]; 334 | [passwordVC addAction:acceptPassword]; 335 | [passwordVC addAction:cancelPassword]; 336 | 337 | [rootVC presentViewController:passwordVC animated:YES completion:nil]; 338 | 339 | 340 | }]; 341 | [password setValue:[[UIImage systemImageNamed:@"textformat.abc"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forKey:@"image"]; 342 | [password setValue:kCAAlignmentLeft forKey:@"titleTextAlignment"]; 343 | 344 | UIAlertAction *passcode = [UIAlertAction actionWithTitle:@"Lock with Passcode" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 345 | UIAlertController *passwordVC = [UIAlertController alertControllerWithTitle:@"Set Passcode" message:nil preferredStyle:UIAlertControllerStyleAlert]; 346 | [passwordVC addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { 347 | textField.autocorrectionType = UITextAutocorrectionTypeNo; 348 | textField.spellCheckingType = UITextSpellCheckingTypeNo; 349 | textField.keyboardType = UIKeyboardTypeNumberPad; 350 | }]; 351 | 352 | UIAlertAction *acceptPassword = [UIAlertAction actionWithTitle:@"Lock Album" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action){ 353 | NSString *passwordCleartext = [passwordVC.textFields[0] text]; 354 | if (passwordCleartext.length <= 0) return; 355 | 356 | NSString *passwordHash = [albumManager sha256HashForText:passwordCleartext]; 357 | [albumManager setObject:[NSString stringWithFormat:@"c%@", passwordHash] forKey:uuid]; 358 | }]; 359 | UIAlertAction *cancelPassword = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){}]; 360 | [passwordVC addAction:acceptPassword]; 361 | [passwordVC addAction:cancelPassword]; 362 | 363 | [rootVC presentViewController:passwordVC animated:YES completion:nil]; 364 | 365 | 366 | }]; 367 | [passcode setValue:[[UIImage systemImageNamed:@"textformat.123"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forKey:@"image"]; 368 | [passcode setValue:kCAAlignmentLeft forKey:@"titleTextAlignment"]; 369 | 370 | UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action){}]; 371 | 372 | [authTypeVC addAction:biometrics]; 373 | [authTypeVC addAction:password]; 374 | [authTypeVC addAction:passcode]; 375 | [authTypeVC addAction:cancel]; 376 | 377 | [rootVC presentViewController:authTypeVC animated:YES completion:nil]; 378 | }]; 379 | 380 | [actions addObject:lockAction]; 381 | } 382 | } 383 | 384 | return actions; 385 | } 386 | %end 387 | 388 | 389 | /***************************************************** 390 | ** ** 391 | ** Add blur and lock icon to the album preview ** 392 | ** ** 393 | *****************************************************/ 394 | 395 | 396 | %hook PUStackView 397 | %property (nonatomic, retain) UIView *lockView; 398 | 399 | %new 400 | -(void)updateLockViewForCollection:(PHCollection *)collection { 401 | 402 | NSString *uuid = [albumManager uuidForCollection:(PHAssetCollection *)collection]; 403 | NSString *protection = [albumManager objectForKey:uuid]; 404 | 405 | UILabel *subtitle = [self.superview valueForKey:@"_subtitleLabel"]; 406 | 407 | BOOL listWantsLock = NO; 408 | if ([collection isKindOfClass:NSClassFromString(@"PHCollectionList")]) { 409 | listWantsLock = [albumManager collectionListWantsLock:(PHCollectionList *)collection]; 410 | } 411 | 412 | if (!listWantsLock && ([albumManager objectForKey:uuid] == nil || 413 | ([[albumManager objectForKey:@"rememberUnlock"] boolValue] && [albumManager.unlockedAlbums containsObject:uuid]) || 414 | ([[albumManager objectForKey:@"unlockSameAuth"] boolValue] && [albumManager.unlockedProtections containsObject:protection])) 415 | ) { 416 | if (self.lockView) { 417 | [self.lockView removeFromSuperview]; 418 | self.lockView = nil; 419 | } 420 | 421 | subtitle.hidden = NO; 422 | 423 | return; 424 | } 425 | 426 | if (!self.lockView) { 427 | UIView *lockView = [[UIView alloc] initWithFrame:self.bounds]; 428 | lockView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 429 | 430 | UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 431 | UIVisualEffectView *blurEffectView = [[UIVisualEffectView alloc] initWithEffect:blurEffect]; 432 | blurEffectView.frame = lockView.bounds; 433 | blurEffectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 434 | [lockView addSubview:blurEffectView]; 435 | 436 | UIImageSymbolConfiguration *configuration = [UIImageSymbolConfiguration configurationWithPointSize:30 weight:UIImageSymbolWeightMedium scale:UIImageSymbolScaleLarge]; 437 | UIImage *lockIcon = [UIImage systemImageNamed:@"lock" withConfiguration:configuration]; 438 | UIImageView *imageView = [[UIImageView alloc] initWithImage:lockIcon]; 439 | imageView.tintColor = UIColor.whiteColor; 440 | imageView.frame = lockView.bounds; 441 | imageView.contentMode = UIViewContentModeCenter; 442 | imageView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin; 443 | [lockView addSubview:imageView]; 444 | 445 | [self addSubview:lockView]; 446 | self.lockView = lockView; 447 | } 448 | 449 | subtitle.hidden = YES; 450 | } 451 | %end 452 | 453 | 454 | /***************************************************** 455 | ** ** 456 | ** Go to the root controller when leaving ** 457 | ** the app and a locked album is open ** 458 | ** ** 459 | *****************************************************/ 460 | 461 | 462 | %hook PXPhotosUIViewController 463 | -(void)viewWillAppear:(BOOL)animated { 464 | %orig; 465 | 466 | [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; 467 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(goToRootIfLocked:) name:UIApplicationDidEnterBackgroundNotification object:nil]; 468 | } 469 | 470 | %new 471 | -(void)goToRootIfLocked:(NSNotification *)notification { 472 | PHAssetCollection *collection = (PHAssetCollection *)self.assetReferenceForCurrentScrollPosition.assetCollection; 473 | NSString *uuid = [albumManager uuidForCollection:collection]; 474 | NSString *protection = [albumManager objectForKey:uuid]; 475 | 476 | if (protection != nil) { 477 | [self.navigationController popToRootViewControllerAnimated:YES]; 478 | } 479 | } 480 | %end 481 | 482 | 483 | /***************************************************** 484 | ** ** 485 | ** Show hidden photos in albums, fix asset count ** 486 | ** ** 487 | *****************************************************/ 488 | 489 | 490 | %hook PHAsset 491 | + (PHFetchResult *)fetchAssetsInAssetCollection:(PHAssetCollection *)assetCollection options:(PHFetchOptions *)options { 492 | if (assetCollection.assetCollectionType == PHAssetCollectionTypeAlbum || assetCollection.assetCollectionSubtype == PHAssetCollectionSubtypeSmartAlbumGeneric) { 493 | options.includeHiddenAssets = YES; 494 | } 495 | 496 | return %orig; 497 | } 498 | + (PHFetchResult *)fetchKeyAssetsInAssetCollection:(PHAssetCollection *)assetCollection options:(PHFetchOptions *)options { 499 | if (assetCollection.assetCollectionType == PHAssetCollectionTypeAlbum) { 500 | options.includeHiddenAssets = YES; 501 | } 502 | 503 | return %orig; 504 | } 505 | %end 506 | 507 | %hook PHAssetCollection 508 | -(unsigned long long)estimatedAssetCount { 509 | if (self.assetCollectionType == PHAssetCollectionTypeAlbum) { 510 | PHFetchOptions *options = [PHFetchOptions new]; 511 | options.includeHiddenAssets = YES; 512 | PHFetchResult *result = [PHAsset fetchAssetsInAssetCollection:self options:options]; 513 | return result.count; 514 | } 515 | 516 | return %orig; 517 | } 518 | + (PHFetchResult *)fetchAssetCollectionsWithType:(PHAssetCollectionType)type subtype:(PHAssetCollectionSubtype)subtype options:(PHFetchOptions *)options { 519 | if (type == PHAssetCollectionTypeAlbum && options) { 520 | options.includeHiddenAssets = YES; 521 | } 522 | 523 | return %orig; 524 | } 525 | %end 526 | 527 | 528 | /***************************************************** 529 | ** ** 530 | ** Fix interaction with hidden photos in albums ** 531 | ** ** 532 | *****************************************************/ 533 | 534 | 535 | %hook PHFetchResult 536 | -(id)initWithQuery:(PHQuery*)arg1 { 537 | 538 | // This is necessary as iOS performs various checks if the album actually contains an asset when 539 | // e.g. moving photos around or removing them from an album. Without this, these actions will fail. 540 | 541 | if ([arg1.basePredicate.predicateFormat containsString:@"albums CONTAINS"]) { 542 | arg1.fetchOptions.includeHiddenAssets = YES; 543 | } 544 | 545 | return %orig; 546 | } 547 | %end 548 | 549 | 550 | /***************************************************** 551 | ** ** 552 | ** Hide locked albums ** 553 | ** ** 554 | *****************************************************/ 555 | 556 | 557 | %hook PHBatchFetchingArray 558 | -(id)initWithOIDs:(_PFArray*)arr options:(id)options photoLibrary:(id)photoLibrary { 559 | id orig = %orig; 560 | 561 | // If the first object is not a collection, the rest won't be either, so we can skip modifying the array 562 | if ([orig count] <= 0 || ![[orig objectAtIndex:0] isKindOfClass:[PHCollection class]]) return orig; 563 | 564 | BOOL hideLockedAlbums = ![[NSBundle mainBundle].bundleIdentifier containsString:@"com.apple.mobileslideshow"] || ![[albumManager objectForKey:@"showLockedAlbums"] boolValue]; 565 | 566 | if (orig && hideLockedAlbums) { 567 | __NSArrayM *mutableArray = [arr mutableCopyWithZone:nil]; 568 | 569 | NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet]; 570 | for(PHObject *object in orig) { 571 | if([object isKindOfClass:[PHAssetCollection class]] && [albumManager objectForKey:object.localIdentifier]) { 572 | [indexes addIndex:[orig indexOfObject:object]]; 573 | } 574 | } 575 | 576 | [mutableArray removeObjectsAtIndexes:indexes]; 577 | arr = [mutableArray copyWithZone:nil]; 578 | } 579 | 580 | // Yes, we are calling %orig here again, but this time with our modified array as first argument. 581 | // No, we can't modify the orig result directly as it's an immutable array with no way to make it mutable again, smartass. 582 | return %orig; 583 | } 584 | %end 585 | 586 | /***************************************************** 587 | ** ** 588 | ** Third-party app fix: ** 589 | ** display albums that contain only hidden photos ** 590 | ** ** 591 | *****************************************************/ 592 | 593 | %hook PHFetchOptions 594 | -(void)setPredicate:(NSPredicate *)predicate { 595 | 596 | // When using predicates, the database gets asked, so our estimatedAssetCount hook won't work. 597 | // Therefore, we simply remove this predicate here to get any results on albums that contain only hidden photos 598 | 599 | if ([predicate.predicateFormat containsString:@"estimatedAssetCount > 0"]) { 600 | predicate = nil; 601 | } 602 | 603 | %orig; 604 | } 605 | %end 606 | 607 | 608 | /***************************************************** 609 | ** ** 610 | ** Allow FaceID usage in Photos app ** 611 | ** ** 612 | *****************************************************/ 613 | 614 | 615 | // Credits to https://github.com/jacobcxdev/iDunnoU/blob/648e27a564b42df45c0ed77dc5d1609baedc98ef/Tweak.x 616 | %hook TCCDService 617 | - (void)setDefaultAllowedIdentifiersList:(NSArray *)list { 618 | if ([self.name isEqual:@"kTCCServiceFaceID"]) { 619 | NSMutableArray *tcclist = [list mutableCopy]; 620 | [tcclist addObject:@"com.apple.mobileslideshow"]; 621 | [tcclist addObject:@"com.apple.PhotosUICore"]; 622 | return %orig([tcclist copy]); 623 | } 624 | return %orig; 625 | } 626 | %end 627 | 628 | 629 | %group AllowFaceId 630 | %hook NSBundle 631 | - (NSDictionary *)infoDictionary { 632 | NSMutableDictionary *info = [%orig mutableCopy]; 633 | [info setValue:@"View locked albums" forKey:@"NSFaceIDUsageDescription"]; 634 | return info; 635 | } 636 | %end 637 | %end 638 | 639 | 640 | /***************************************************** 641 | ** ** 642 | ** Tweak Constructor ** 643 | ** ** 644 | *****************************************************/ 645 | 646 | 647 | %ctor { 648 | NSString *bundleId = [NSBundle mainBundle].bundleIdentifier; 649 | 650 | if ([bundleId isEqualToString:@"com.apple.mobileslideshow"]) { 651 | libSandy_applyProfile("AlbumManager_FileAccess"); 652 | } else if ([bundleId isEqualToString:@"com.huiyun.CareViewerInternational"]) { 653 | return; 654 | } 655 | 656 | albumManager = [NSClassFromString(@"AlbumManager") sharedInstance]; 657 | 658 | if ([[albumManager objectForKey:@"enabled"] boolValue]) { 659 | %init(_ungrouped); 660 | 661 | if ([bundleId containsString:@"com.apple.mobileslideshow"]) { 662 | %init(AllowFaceId); 663 | } 664 | } 665 | } --------------------------------------------------------------------------------