├── .gitignore ├── Common └── Common.h ├── LICENSE ├── Makefile ├── PSpotifyPrefs ├── Makefile ├── MiscVC.h ├── MiscVC.m ├── PSRootVC.h ├── PSRootVC.m ├── Resources │ ├── ++ Features.plist │ ├── Assets │ │ ├── Amēlija@2x.png │ │ ├── April@2x.png │ │ ├── PSBanner.png │ │ ├── PSIcon@2x.png │ │ ├── PSIcon@3x.png │ │ └── PSpotifyIcon.png │ ├── Info.plist │ ├── Now Playing UI.plist │ ├── PSContributors.plist │ ├── PSLinks.plist │ ├── PSMisc.plist │ ├── Root.plist │ └── SpringBoard.plist └── layout │ └── Library │ └── PreferenceLoader │ └── Preferences │ └── PerfectSpotify.plist ├── PerfectSpotify.h ├── PerfectSpotify.plist ├── PerfectSpotify.xm ├── README.md └── control /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .theos/ 3 | packages/ 4 | compile_commands.json 5 | -------------------------------------------------------------------------------- /Common/Common.h: -------------------------------------------------------------------------------- 1 | static NSString *const kPath = @"/var/mobile/Library/Preferences/me.luki.perfectspotifyprefs.plist"; 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | ### 1. Definitions 5 | 6 | **1.1. “Contributor”** 7 | means each individual or legal entity that creates, contributes to 8 | the creation of, or owns Covered Software. 9 | 10 | **1.2. “Contributor Version”** 11 | means the combination of the Contributions of others (if any) used 12 | by a Contributor and that particular Contributor's Contribution. 13 | 14 | **1.3. “Contribution”** 15 | means Covered Software of a particular Contributor. 16 | 17 | **1.4. “Covered Software”** 18 | means Source Code Form to which the initial Contributor has attached 19 | the notice in Exhibit A, the Executable Form of such Source Code 20 | Form, and Modifications of such Source Code Form, in each case 21 | including portions thereof. 22 | 23 | **1.5. “Incompatible With Secondary Licenses”** 24 | means 25 | 26 | * **(a)** that the initial Contributor has attached the notice described 27 | in Exhibit B to the Covered Software; or 28 | * **(b)** that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the 30 | terms of a Secondary License. 31 | 32 | **1.6. “Executable Form”** 33 | means any form of the work other than Source Code Form. 34 | 35 | **1.7. “Larger Work”** 36 | means a work that combines Covered Software with other material, in 37 | a separate file or files, that is not Covered Software. 38 | 39 | **1.8. “License”** 40 | means this document. 41 | 42 | **1.9. “Licensable”** 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, any and 45 | all of the rights conveyed by this License. 46 | 47 | **1.10. “Modifications”** 48 | means any of the following: 49 | 50 | * **(a)** any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered 52 | Software; or 53 | * **(b)** any new file in Source Code Form that contains any Covered 54 | Software. 55 | 56 | **1.11. “Patent Claims” of a Contributor** 57 | means any patent claim(s), including without limitation, method, 58 | process, and apparatus claims, in any patent Licensable by such 59 | Contributor that would be infringed, but for the grant of the 60 | License, by the making, using, selling, offering for sale, having 61 | made, import, or transfer of either its Contributions or its 62 | Contributor Version. 63 | 64 | **1.12. “Secondary License”** 65 | means either the GNU General Public License, Version 2.0, the GNU 66 | Lesser General Public License, Version 2.1, the GNU Affero General 67 | Public License, Version 3.0, or any later versions of those 68 | licenses. 69 | 70 | **1.13. “Source Code Form”** 71 | means the form of the work preferred for making modifications. 72 | 73 | **1.14. “You” (or “Your”)** 74 | means an individual or a legal entity exercising rights under this 75 | License. For legal entities, “You” includes any entity that 76 | controls, is controlled by, or is under common control with You. For 77 | purposes of this definition, “control” means **(a)** the power, direct 78 | or indirect, to cause the direction or management of such entity, 79 | whether by contract or otherwise, or **(b)** ownership of more than 80 | fifty percent (50%) of the outstanding shares or beneficial 81 | ownership of such entity. 82 | 83 | 84 | ### 2. License Grants and Conditions 85 | 86 | #### 2.1. Grants 87 | 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license: 90 | 91 | * **(a)** under intellectual property rights (other than patent or trademark) 92 | Licensable by such Contributor to use, reproduce, make available, 93 | modify, display, perform, distribute, and otherwise exploit its 94 | Contributions, either on an unmodified basis, with Modifications, or 95 | as part of a Larger Work; and 96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer 97 | for sale, have made, import, and otherwise transfer either its 98 | Contributions or its Contributor Version. 99 | 100 | #### 2.2. Effective Date 101 | 102 | The licenses granted in Section 2.1 with respect to any Contribution 103 | become effective for each Contribution on the date the Contributor first 104 | distributes such Contribution. 105 | 106 | #### 2.3. Limitations on Grant Scope 107 | 108 | The licenses granted in this Section 2 are the only rights granted under 109 | this License. No additional rights or licenses will be implied from the 110 | distribution or licensing of Covered Software under this License. 111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 112 | Contributor: 113 | 114 | * **(a)** for any code that a Contributor has removed from Covered Software; 115 | or 116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's 117 | modifications of Covered Software, or **(ii)** the combination of its 118 | Contributions with other software (except as part of its Contributor 119 | Version); or 120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of 121 | its Contributions. 122 | 123 | This License does not grant any rights in the trademarks, service marks, 124 | or logos of any Contributor (except as may be necessary to comply with 125 | the notice requirements in Section 3.4). 126 | 127 | #### 2.4. Subsequent Licenses 128 | 129 | No Contributor makes additional grants as a result of Your choice to 130 | distribute the Covered Software under a subsequent version of this 131 | License (see Section 10.2) or under the terms of a Secondary License (if 132 | permitted under the terms of Section 3.3). 133 | 134 | #### 2.5. Representation 135 | 136 | Each Contributor represents that the Contributor believes its 137 | Contributions are its original creation(s) or it has sufficient rights 138 | to grant the rights to its Contributions conveyed by this License. 139 | 140 | #### 2.6. Fair Use 141 | 142 | This License is not intended to limit any rights You have under 143 | applicable copyright doctrines of fair use, fair dealing, or other 144 | equivalents. 145 | 146 | #### 2.7. Conditions 147 | 148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 149 | in Section 2.1. 150 | 151 | 152 | ### 3. Responsibilities 153 | 154 | #### 3.1. Distribution of Source Form 155 | 156 | All distribution of Covered Software in Source Code Form, including any 157 | Modifications that You create or to which You contribute, must be under 158 | the terms of this License. You must inform recipients that the Source 159 | Code Form of the Covered Software is governed by the terms of this 160 | License, and how they can obtain a copy of this License. You may not 161 | attempt to alter or restrict the recipients' rights in the Source Code 162 | Form. 163 | 164 | #### 3.2. Distribution of Executable Form 165 | 166 | If You distribute Covered Software in Executable Form then: 167 | 168 | * **(a)** such Covered Software must also be made available in Source Code 169 | Form, as described in Section 3.1, and You must inform recipients of 170 | the Executable Form how they can obtain a copy of such Source Code 171 | Form by reasonable means in a timely manner, at a charge no more 172 | than the cost of distribution to the recipient; and 173 | 174 | * **(b)** You may distribute such Executable Form under the terms of this 175 | License, or sublicense it under different terms, provided that the 176 | license for the Executable Form does not attempt to limit or alter 177 | the recipients' rights in the Source Code Form under this License. 178 | 179 | #### 3.3. Distribution of a Larger Work 180 | 181 | You may create and distribute a Larger Work under terms of Your choice, 182 | provided that You also comply with the requirements of this License for 183 | the Covered Software. If the Larger Work is a combination of Covered 184 | Software with a work governed by one or more Secondary Licenses, and the 185 | Covered Software is not Incompatible With Secondary Licenses, this 186 | License permits You to additionally distribute such Covered Software 187 | under the terms of such Secondary License(s), so that the recipient of 188 | the Larger Work may, at their option, further distribute the Covered 189 | Software under the terms of either this License or such Secondary 190 | License(s). 191 | 192 | #### 3.4. Notices 193 | 194 | You may not remove or alter the substance of any license notices 195 | (including copyright notices, patent notices, disclaimers of warranty, 196 | or limitations of liability) contained within the Source Code Form of 197 | the Covered Software, except that You may alter any license notices to 198 | the extent required to remedy known factual inaccuracies. 199 | 200 | #### 3.5. Application of Additional Terms 201 | 202 | You may choose to offer, and to charge a fee for, warranty, support, 203 | indemnity or liability obligations to one or more recipients of Covered 204 | Software. However, You may do so only on Your own behalf, and not on 205 | behalf of any Contributor. You must make it absolutely clear that any 206 | such warranty, support, indemnity, or liability obligation is offered by 207 | You alone, and You hereby agree to indemnify every Contributor for any 208 | liability incurred by such Contributor as a result of warranty, support, 209 | indemnity or liability terms You offer. You may include additional 210 | disclaimers of warranty and limitations of liability specific to any 211 | jurisdiction. 212 | 213 | 214 | ### 4. Inability to Comply Due to Statute or Regulation 215 | 216 | If it is impossible for You to comply with any of the terms of this 217 | License with respect to some or all of the Covered Software due to 218 | statute, judicial order, or regulation then You must: **(a)** comply with 219 | the terms of this License to the maximum extent possible; and **(b)** 220 | describe the limitations and the code they affect. Such description must 221 | be placed in a text file included with all distributions of the Covered 222 | Software under this License. Except to the extent prohibited by statute 223 | or regulation, such description must be sufficiently detailed for a 224 | recipient of ordinary skill to be able to understand it. 225 | 226 | 227 | ### 5. Termination 228 | 229 | **5.1.** The rights granted under this License will terminate automatically 230 | if You fail to comply with any of its terms. However, if You become 231 | compliant, then the rights granted under this License from a particular 232 | Contributor are reinstated **(a)** provisionally, unless and until such 233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an 234 | ongoing basis, if such Contributor fails to notify You of the 235 | non-compliance by some reasonable means prior to 60 days after You have 236 | come back into compliance. Moreover, Your grants from a particular 237 | Contributor are reinstated on an ongoing basis if such Contributor 238 | notifies You of the non-compliance by some reasonable means, this is the 239 | first time You have received notice of non-compliance with this License 240 | from such Contributor, and You become compliant prior to 30 days after 241 | Your receipt of the notice. 242 | 243 | **5.2.** If You initiate litigation against any entity by asserting a patent 244 | infringement claim (excluding declaratory judgment actions, 245 | counter-claims, and cross-claims) alleging that a Contributor Version 246 | directly or indirectly infringes any patent, then the rights granted to 247 | You by any and all Contributors for the Covered Software under Section 248 | 2.1 of this License shall terminate. 249 | 250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all 251 | end user license agreements (excluding distributors and resellers) which 252 | have been validly granted by You or Your distributors under this License 253 | prior to termination shall survive termination. 254 | 255 | 256 | ### 6. Disclaimer of Warranty 257 | 258 | > Covered Software is provided under this License on an “as is” 259 | > basis, without warranty of any kind, either expressed, implied, or 260 | > statutory, including, without limitation, warranties that the 261 | > Covered Software is free of defects, merchantable, fit for a 262 | > particular purpose or non-infringing. The entire risk as to the 263 | > quality and performance of the Covered Software is with You. 264 | > Should any Covered Software prove defective in any respect, You 265 | > (not any Contributor) assume the cost of any necessary servicing, 266 | > repair, or correction. This disclaimer of warranty constitutes an 267 | > essential part of this License. No use of any Covered Software is 268 | > authorized under this License except under this disclaimer. 269 | 270 | ### 7. Limitation of Liability 271 | 272 | > Under no circumstances and under no legal theory, whether tort 273 | > (including negligence), contract, or otherwise, shall any 274 | > Contributor, or anyone who distributes Covered Software as 275 | > permitted above, be liable to You for any direct, indirect, 276 | > special, incidental, or consequential damages of any character 277 | > including, without limitation, damages for lost profits, loss of 278 | > goodwill, work stoppage, computer failure or malfunction, or any 279 | > and all other commercial damages or losses, even if such party 280 | > shall have been informed of the possibility of such damages. This 281 | > limitation of liability shall not apply to liability for death or 282 | > personal injury resulting from such party's negligence to the 283 | > extent applicable law prohibits such limitation. Some 284 | > jurisdictions do not allow the exclusion or limitation of 285 | > incidental or consequential damages, so this exclusion and 286 | > limitation may not apply to You. 287 | 288 | 289 | ### 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the 292 | courts of a jurisdiction where the defendant maintains its principal 293 | place of business and such litigation shall be governed by laws of that 294 | jurisdiction, without reference to its conflict-of-law provisions. 295 | Nothing in this Section shall prevent a party's ability to bring 296 | cross-claims or counter-claims. 297 | 298 | 299 | ### 9. Miscellaneous 300 | 301 | This License represents the complete agreement concerning the subject 302 | matter hereof. If any provision of this License is held to be 303 | unenforceable, such provision shall be reformed only to the extent 304 | necessary to make it enforceable. Any law or regulation which provides 305 | that the language of a contract shall be construed against the drafter 306 | shall not be used to construe this License against a Contributor. 307 | 308 | 309 | ### 10. Versions of the License 310 | 311 | #### 10.1. New Versions 312 | 313 | Mozilla Foundation is the license steward. Except as provided in Section 314 | 10.3, no one other than the license steward has the right to modify or 315 | publish new versions of this License. Each version will be given a 316 | distinguishing version number. 317 | 318 | #### 10.2. Effect of New Versions 319 | 320 | You may distribute the Covered Software under the terms of the version 321 | of the License under which You originally received the Covered Software, 322 | or under the terms of any subsequent version published by the license 323 | steward. 324 | 325 | #### 10.3. Modified Versions 326 | 327 | If you create software not governed by this License, and you want to 328 | create a new license for such software, you may create and use a 329 | modified version of this License if you rename the license and remove 330 | any references to the name of the license steward (except to note that 331 | such modified license differs from this License). 332 | 333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 334 | 335 | If You choose to distribute Source Code Form that is Incompatible With 336 | Secondary Licenses under the terms of this version of the License, the 337 | notice described in Exhibit B of this License must be attached. 338 | 339 | ## Exhibit A - Source Code Form License Notice 340 | 341 | This Source Code Form is subject to the terms of the Mozilla Public 342 | License, v. 2.0. If a copy of the MPL was not distributed with this 343 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular 346 | file, then You may include the notice in a location (such as a LICENSE 347 | file in a relevant directory) where a recipient would be likely to look 348 | for such a notice. 349 | 350 | You may add additional accurate notices of copyright ownership. 351 | 352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice 353 | 354 | This Source Code Form is "Incompatible With Secondary Licenses", as 355 | defined by the Mozilla Public License, v. 2.0. 356 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export TARGET := iphone:clang:latest:14.0 2 | INSTALL_TARGET_PROCESSES = Spotify Preferences 3 | 4 | TWEAK_NAME = PerfectSpotify 5 | 6 | PerfectSpotify_FILES = PerfectSpotify.xm 7 | PerfectSpotify_CFLAGS = -fobjc-arc 8 | PerfectSpotify_LIBRARIES += gcuniversal kitten 9 | PerfectSpotify_PRIVATE_FRAMEWORKS = MediaRemote 10 | 11 | SUBPROJECTS = PSpotifyPrefs 12 | 13 | include $(THEOS)/makefiles/common.mk 14 | include $(THEOS_MAKE_PATH)/tweak.mk 15 | include $(THEOS_MAKE_PATH)/aggregate.mk 16 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Makefile: -------------------------------------------------------------------------------- 1 | BUNDLE_NAME = PSpotifyPrefs 2 | 3 | PSpotifyPrefs_FILES = PSRootVC.m MiscVC.m 4 | PSpotifyPrefs_CFLAGS = -fobjc-arc 5 | PSpotifyPrefs_LIBRARIES = gcuniversal 6 | PSpotifyPrefs_FRAMEWORKS = UIKit 7 | PSpotifyPrefs_PRIVATE_FRAMEWORKS = Preferences OnBoardingKit 8 | PSpotifyPrefs_INSTALL_PATH = /Library/PreferenceBundles 9 | 10 | include $(THEOS)/makefiles/common.mk 11 | include $(THEOS_MAKE_PATH)/bundle.mk 12 | -------------------------------------------------------------------------------- /PSpotifyPrefs/MiscVC.h: -------------------------------------------------------------------------------- 1 | @import Preferences.PSSpecifier; 2 | @import Preferences.PSListController; 3 | #import "Common/Common.h" 4 | 5 | 6 | @interface SpringBoardVC : PSListController 7 | @end 8 | 9 | 10 | @interface MiscVC : PSListController 11 | @end 12 | 13 | 14 | @interface NowPlayingUIVC : PSListController 15 | @end 16 | 17 | 18 | @interface ExtraFeaturesVC : PSListController 19 | @property (nonatomic, strong) NSMutableDictionary *savedSpecifiers; 20 | @end 21 | 22 | 23 | @interface PSListController (Private) 24 | - (BOOL)containsSpecifier:(PSSpecifier *)arg1; 25 | @end 26 | -------------------------------------------------------------------------------- /PSpotifyPrefs/MiscVC.m: -------------------------------------------------------------------------------- 1 | #import "MiscVC.h" 2 | 3 | 4 | // Reusable 5 | 6 | static id readPreferenceValue(PSSpecifier *specifier) { 7 | 8 | NSMutableDictionary *settings = [NSMutableDictionary dictionary]; 9 | [settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile: kPath]]; 10 | return settings[specifier.properties[@"key"]] ?: specifier.properties[@"default"]; 11 | 12 | } 13 | 14 | static void setPreferenceValue(id value, PSSpecifier *specifier) { 15 | 16 | NSMutableDictionary *settings = [NSMutableDictionary dictionary]; 17 | [settings addEntriesFromDictionary:[NSDictionary dictionaryWithContentsOfFile: kPath]]; 18 | [settings setObject:value forKey:specifier.properties[@"key"]]; 19 | [settings writeToFile:kPath atomically:YES]; 20 | 21 | } 22 | 23 | @implementation MiscVC 24 | 25 | - (NSArray *)specifiers { 26 | 27 | if(!_specifiers) _specifiers = [self loadSpecifiersFromPlistName:@"PSMisc" target:self]; 28 | return _specifiers; 29 | 30 | } 31 | 32 | 33 | - (id)readPreferenceValue:(PSSpecifier *)specifier { return readPreferenceValue(specifier); } 34 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { 35 | 36 | setPreferenceValue(value, specifier); 37 | 38 | NSString *key = [specifier propertyForKey:@"key"]; 39 | if(![key isEqualToString:@"enableLyricsForAllTracks"]) return; 40 | if(![[self readPreferenceValue:[self specifierForID:@"LyricsSwitch"]] boolValue]) return; 41 | 42 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"PerfectSpotify" message:@"No, this switch won't enable lyrics if they aren't already available for your country. What it does is to 'unlock' them for new released songs which for some reason still don't have them. Do you understand? You better, I don't want to get DM's about this ok? Lol just kidding, but yeah." preferredStyle:UIAlertControllerStyleAlert]; 43 | UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"Got it" style:UIAlertActionStyleDefault handler:nil]; 44 | [alertController addAction:confirmAction]; 45 | [self presentViewController:alertController animated:YES completion:nil]; 46 | 47 | [super setPreferenceValue:value specifier:specifier]; 48 | 49 | } 50 | 51 | @end 52 | 53 | 54 | @implementation NowPlayingUIVC 55 | 56 | - (NSArray *)specifiers { 57 | 58 | if(!_specifiers) _specifiers = [self loadSpecifiersFromPlistName:@"Now Playing UI" target:self]; 59 | return _specifiers; 60 | 61 | } 62 | 63 | @end 64 | 65 | 66 | @implementation SpringBoardVC 67 | 68 | - (NSArray *)specifiers { 69 | 70 | if(!_specifiers) _specifiers = [self loadSpecifiersFromPlistName:@"SpringBoard" target:self]; 71 | return _specifiers; 72 | 73 | } 74 | 75 | 76 | - (id)readPreferenceValue:(PSSpecifier *)specifier { return readPreferenceValue(specifier); } 77 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { 78 | 79 | setPreferenceValue(value, specifier); 80 | [super setPreferenceValue:value specifier:specifier]; 81 | 82 | [NSNotificationCenter.defaultCenter postNotificationName:@"updateShortcutItems" object:nil]; 83 | 84 | } 85 | 86 | @end 87 | 88 | 89 | @implementation ExtraFeaturesVC 90 | 91 | - (NSArray *)specifiers { 92 | 93 | if(_specifiers) return nil; 94 | _specifiers = [self loadSpecifiersFromPlistName:@"++ Features" target:self]; 95 | 96 | NSArray *chosenIDs = @[ 97 | @"ArtworkBasedColorsSwitch", 98 | @"GroupCell1", 99 | @"HapticsSwitch", 100 | @"GroupCell2", 101 | @"HapticsOptionsCell", 102 | @"GroupCell3", 103 | @"CanvasOptionsCell" 104 | ]; 105 | 106 | self.savedSpecifiers = self.savedSpecifiers ?: [NSMutableDictionary new]; 107 | 108 | for(PSSpecifier *specifier in _specifiers) 109 | 110 | if([chosenIDs containsObject:[specifier propertyForKey:@"id"]]) 111 | 112 | [self.savedSpecifiers setObject:specifier forKey:[specifier propertyForKey:@"id"]]; 113 | 114 | return _specifiers; 115 | 116 | } 117 | 118 | 119 | - (void)viewDidLoad { 120 | 121 | [super viewDidLoad]; 122 | [self reloadSpecifiers]; 123 | 124 | } 125 | 126 | 127 | - (void)reloadSpecifiers { 128 | 129 | [super reloadSpecifiers]; 130 | 131 | if(![[self readPreferenceValue:[self specifierForID:@"SpotifyUISwitch"]] boolValue]) 132 | 133 | [self removeContiguousSpecifiers:@[self.savedSpecifiers[@"ArtworkBasedColorsSwitch"], self.savedSpecifiers[@"GroupCell1"], self.savedSpecifiers[@"HapticsSwitch"], self.savedSpecifiers[@"GroupCell2"], self.savedSpecifiers[@"HapticsOptionsCell"]] animated:NO]; 134 | 135 | 136 | else if(![self containsSpecifier:self.savedSpecifiers[@"ArtworkBasedColorsSwitch"]]) 137 | 138 | [self insertContiguousSpecifiers:@[self.savedSpecifiers[@"ArtworkBasedColorsSwitch"], self.savedSpecifiers[@"GroupCell1"], self.savedSpecifiers[@"HapticsSwitch"], self.savedSpecifiers[@"GroupCell2"], self.savedSpecifiers[@"HapticsOptionsCell"]] afterSpecifierID:@"SpotifyUISwitch" animated:NO]; 139 | 140 | 141 | if(![[self readPreferenceValue:[self specifierForID:@"SaveCanvasSwitch"]] boolValue]) { 142 | 143 | [self removeSpecifier:self.savedSpecifiers[@"GroupCell3"] animated:NO]; 144 | [self removeSpecifier:self.savedSpecifiers[@"CanvasOptionsCell"] animated:NO]; 145 | 146 | } 147 | 148 | else if(![self containsSpecifier:self.savedSpecifiers[@"CanvasOptionsCell"]]) { 149 | 150 | [self insertSpecifier:self.savedSpecifiers[@"GroupCell3"] afterSpecifierID:@"SaveCanvasSwitch" animated:NO]; 151 | [self insertSpecifier:self.savedSpecifiers[@"CanvasOptionsCell"] afterSpecifierID:@"GroupCell3" animated:NO]; 152 | 153 | } 154 | 155 | } 156 | 157 | 158 | - (id)readPreferenceValue:(PSSpecifier *)specifier { return readPreferenceValue(specifier); } 159 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { 160 | 161 | setPreferenceValue(value, specifier); 162 | [super setPreferenceValue:value specifier:specifier]; 163 | 164 | NSString *key = [specifier propertyForKey:@"key"]; 165 | 166 | if([key isEqualToString:@"enableSpotifyUI"]) { 167 | 168 | if(![[self readPreferenceValue:[self specifierForID:@"SpotifyUISwitch"]] boolValue]) 169 | 170 | [self removeContiguousSpecifiers:@[self.savedSpecifiers[@"ArtworkBasedColorsSwitch"], self.savedSpecifiers[@"GroupCell1"], self.savedSpecifiers[@"HapticsSwitch"], self.savedSpecifiers[@"GroupCell2"], self.savedSpecifiers[@"HapticsOptionsCell"]] animated:YES]; 171 | 172 | else if(![self containsSpecifier:self.savedSpecifiers[@"ArtworkBasedColorsSwitch"]]) 173 | 174 | [self insertContiguousSpecifiers:@[self.savedSpecifiers[@"ArtworkBasedColorsSwitch"], self.savedSpecifiers[@"GroupCell1"], self.savedSpecifiers[@"HapticsSwitch"], self.savedSpecifiers[@"GroupCell2"], self.savedSpecifiers[@"HapticsOptionsCell"]] afterSpecifierID:@"SpotifyUISwitch" animated:YES]; 175 | 176 | } 177 | 178 | if([key isEqualToString:@"saveCanvas"]) { 179 | 180 | if(![value boolValue]) { 181 | 182 | [self removeSpecifier:self.savedSpecifiers[@"GroupCell3"] animated:YES]; 183 | [self removeSpecifier:self.savedSpecifiers[@"CanvasOptionsCell"] animated:YES]; 184 | 185 | } 186 | 187 | else if(![self containsSpecifier:self.savedSpecifiers[@"CanvasOptionsCell"]]) { 188 | 189 | [self insertSpecifier:self.savedSpecifiers[@"GroupCell3"] afterSpecifierID:@"SaveCanvasSwitch" animated:YES]; 190 | [self insertSpecifier:self.savedSpecifiers[@"CanvasOptionsCell"] afterSpecifierID:@"GroupCell3" animated:YES]; 191 | 192 | } 193 | 194 | } 195 | 196 | } 197 | 198 | @end 199 | -------------------------------------------------------------------------------- /PSpotifyPrefs/PSRootVC.h: -------------------------------------------------------------------------------- 1 | @import AudioToolbox.AudioServices; 2 | @import Preferences.PSSpecifier; 3 | @import Preferences.PSListController; 4 | #import 5 | #import "Common/Common.h" 6 | 7 | 8 | #define kPSpotifyTintColor [UIColor colorWithRed: 0.11 green: 0.73 blue: 0.33 alpha: 1.0] 9 | 10 | 11 | @interface OBWelcomeController : UIViewController; 12 | - (id)initWithTitle:(id)arg1 detailText:(id)arg2 icon:(id)arg3; 13 | - (void)addBulletedListItemWithTitle:(id)arg1 description:(id)arg2 image:(id)arg3; 14 | @end 15 | 16 | 17 | @interface _UIBackdropViewSettings : NSObject 18 | + (id)settingsForStyle:(NSInteger)arg1; 19 | @end 20 | 21 | 22 | @interface _UIBackdropView : UIView 23 | @property (assign, nonatomic) BOOL blurRadiusSetOnce; 24 | @property (assign, nonatomic) double _blurRadius; 25 | @property (copy, nonatomic) NSString *_blurQuality; 26 | - (id)initWithSettings:(id)arg1; 27 | - (id)initWithFrame:(CGRect)arg1 autosizesToFitSuperview:(BOOL)arg2 settings:(id)arg3; 28 | @end 29 | 30 | 31 | @interface PSRootVC : PSListController 32 | @end 33 | 34 | 35 | @interface PSContributorsVC : PSListController 36 | @end 37 | 38 | 39 | @interface PSLinksVC : PSListController 40 | @end 41 | 42 | 43 | @interface PSTableCell () 44 | - (void)setTitle:(NSString *)title; 45 | @end 46 | 47 | 48 | @interface PSpotifyTableCell : PSTableCell 49 | @end 50 | 51 | 52 | @interface UIApplication (PSpotify) 53 | - (BOOL)launchApplicationWithIdentifier:(id)arg1 suspended:(BOOL)arg2; 54 | @end 55 | -------------------------------------------------------------------------------- /PSpotifyPrefs/PSRootVC.m: -------------------------------------------------------------------------------- 1 | #import "PSRootVC.h" 2 | 3 | 4 | @implementation PSRootVC { 5 | 6 | UIImageView *iconView; 7 | UIButton *killButton; 8 | UIButton *changelogButton; 9 | UIView *headerView; 10 | UIImageView *headerImageView; 11 | OBWelcomeController *changelogController; 12 | 13 | } 14 | 15 | 16 | - (NSArray *)specifiers { 17 | 18 | if(!_specifiers) _specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self]; 19 | return _specifiers; 20 | 21 | } 22 | 23 | 24 | - (id)init { 25 | 26 | self = [super init]; 27 | if(self) [self setupUI]; 28 | return self; 29 | 30 | } 31 | 32 | 33 | - (void)setupUI { 34 | 35 | UIImage *icon = [UIImage imageWithContentsOfFile:@"/Library/PreferenceBundles/PSpotifyPrefs.bundle/Assets/PSIcon@2x.png"];; 36 | UIImage *banner = [UIImage imageWithContentsOfFile:@"/Library/PreferenceBundles/PSpotifyPrefs.bundle/Assets/PSBanner.png"]; 37 | 38 | changelogButton = [UIButton buttonWithType:UIButtonTypeCustom]; 39 | changelogButton.tintColor = kPSpotifyTintColor; 40 | [changelogButton setImage:[UIImage systemImageNamed:@"atom"] forState:UIControlStateNormal]; 41 | [changelogButton addTarget:self action:@selector(showWtfChangedInThisVersion) forControlEvents:UIControlEventTouchUpInside]; 42 | 43 | UIBarButtonItem *changelogButtonItem = [[UIBarButtonItem alloc] initWithCustomView:changelogButton]; 44 | 45 | killButton = [UIButton buttonWithType:UIButtonTypeCustom]; 46 | killButton.tintColor = kPSpotifyTintColor; 47 | [killButton setImage:[UIImage systemImageNamed:@"checkmark.circle"] forState:UIControlStateNormal]; 48 | [killButton addTarget:self action:@selector(killSpotify) forControlEvents:UIControlEventTouchUpInside]; 49 | 50 | UIBarButtonItem *killButtonItem = [[UIBarButtonItem alloc] initWithCustomView:killButton]; 51 | 52 | NSArray *rightButtons; 53 | rightButtons = @[killButtonItem, changelogButtonItem]; 54 | self.navigationItem.rightBarButtonItems = rightButtons; 55 | 56 | self.navigationItem.titleView = [UIView new]; 57 | iconView = [UIImageView new]; 58 | iconView.image = icon; 59 | iconView.contentMode = UIViewContentModeScaleAspectFit; 60 | iconView.translatesAutoresizingMaskIntoConstraints = NO; 61 | [self.navigationItem.titleView addSubview:iconView]; 62 | 63 | headerView = [[UIView alloc] initWithFrame:CGRectMake(0,0,200,200)]; 64 | headerImageView = [UIImageView new]; 65 | headerImageView.image = banner; 66 | headerImageView.contentMode = UIViewContentModeScaleAspectFill; 67 | headerImageView.translatesAutoresizingMaskIntoConstraints = NO; 68 | [headerView addSubview:headerImageView]; 69 | 70 | [self layoutUI]; 71 | 72 | } 73 | 74 | 75 | - (void)layoutUI { 76 | 77 | [iconView.topAnchor constraintEqualToAnchor: self.navigationItem.titleView.topAnchor].active = YES; 78 | [iconView.bottomAnchor constraintEqualToAnchor: self.navigationItem.titleView.bottomAnchor].active = YES; 79 | [iconView.leadingAnchor constraintEqualToAnchor: self.navigationItem.titleView.leadingAnchor].active = YES; 80 | [iconView.trailingAnchor constraintEqualToAnchor: self.navigationItem.titleView.trailingAnchor].active = YES; 81 | 82 | [headerImageView.topAnchor constraintEqualToAnchor: headerView.topAnchor].active = YES; 83 | [headerImageView.bottomAnchor constraintEqualToAnchor: headerView.bottomAnchor].active = YES; 84 | [headerImageView.leadingAnchor constraintEqualToAnchor: headerView.leadingAnchor].active = YES; 85 | [headerImageView.trailingAnchor constraintEqualToAnchor: headerView.trailingAnchor].active = YES; 86 | 87 | } 88 | 89 | 90 | - (void)killSpotify { 91 | 92 | AudioServicesPlaySystemSound(1521); 93 | 94 | pid_t pid; 95 | const char* args[] = {"killall", "Spotify", NULL}; 96 | posix_spawn(&pid, "/usr/bin/killall", NULL, NULL, (char* const*)args, NULL); 97 | 98 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"PerfectSpotify" message:@"Spotify was succesfully destroyed and shattered into pieces, shall we rebuild it by launching it again?" preferredStyle:UIAlertControllerStyleAlert]; 99 | UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"Shoot" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 100 | 101 | [self launchSpotify]; 102 | 103 | }]; 104 | 105 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Maybe later" style:UIAlertActionStyleDefault handler:nil]; 106 | 107 | [alertController addAction: confirmAction]; 108 | [alertController addAction: cancelAction]; 109 | 110 | [self presentViewController:alertController animated:YES completion:nil]; 111 | 112 | } 113 | 114 | 115 | - (void)launchSpotify { 116 | 117 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.05 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 118 | 119 | [UIApplication.sharedApplication launchApplicationWithIdentifier:@"com.spotify.client" suspended:0]; 120 | 121 | }); 122 | 123 | } 124 | 125 | 126 | - (void)showWtfChangedInThisVersion { 127 | 128 | AudioServicesPlaySystemSound(1521); 129 | 130 | UIImage *tweakIconImage = [UIImage imageWithContentsOfFile:@"/Library/PreferenceBundles/PSpotifyPrefs.bundle/Assets/PSpotifyIcon.png"]; 131 | UIImage *checkmarkImage = [UIImage systemImageNamed:@"checkmark.circle.fill"]; 132 | 133 | changelogController = [[OBWelcomeController alloc] initWithTitle:@"PerfectSpotify" detailText:@"2.3~EOL" icon: tweakIconImage]; 134 | [changelogController addBulletedListItemWithTitle:@"Tweak" description:@"Please visit the source on GitHub to see the changelog." image: checkmarkImage]; 135 | 136 | _UIBackdropViewSettings *settings = [_UIBackdropViewSettings settingsForStyle:2]; 137 | 138 | _UIBackdropView *backdropView = [[_UIBackdropView alloc] initWithSettings:settings]; 139 | backdropView.clipsToBounds = YES; 140 | backdropView.layer.masksToBounds = YES; 141 | backdropView.translatesAutoresizingMaskIntoConstraints = NO; 142 | [changelogController.viewIfLoaded insertSubview:backdropView atIndex:0]; 143 | 144 | [backdropView.topAnchor constraintEqualToAnchor: changelogController.viewIfLoaded.topAnchor].active = YES; 145 | [backdropView.bottomAnchor constraintEqualToAnchor: changelogController.viewIfLoaded.bottomAnchor].active = YES; 146 | [backdropView.leadingAnchor constraintEqualToAnchor: changelogController.viewIfLoaded.leadingAnchor].active = YES; 147 | [backdropView.trailingAnchor constraintEqualToAnchor: changelogController.viewIfLoaded.trailingAnchor].active = YES; 148 | 149 | changelogController.view.tintColor = kPSpotifyTintColor; 150 | changelogController.modalInPresentation = NO; 151 | changelogController.modalPresentationStyle = UIModalPresentationPageSheet; 152 | changelogController.viewIfLoaded.backgroundColor = UIColor.clearColor; 153 | 154 | [self presentViewController:changelogController animated:YES completion:nil]; 155 | 156 | } 157 | 158 | 159 | - (void)resetPreferences { 160 | 161 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"PerfectSpotify" message:@"Are you sure bozo?" preferredStyle:UIAlertControllerStyleAlert]; 162 | UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"Shoot" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { 163 | 164 | [[NSFileManager defaultManager] removeItemAtPath:kPath error:nil]; 165 | [self crossDissolveBlur]; 166 | 167 | }]; 168 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Maybe not" style:UIAlertActionStyleCancel handler:nil]; 169 | 170 | [alertController addAction: confirmAction]; 171 | [alertController addAction: cancelAction]; 172 | 173 | [self presentViewController:alertController animated:YES completion:nil]; 174 | 175 | } 176 | 177 | 178 | - (void)crossDissolveBlur { 179 | 180 | UIBlurEffect *blur = [UIBlurEffect effectWithStyle: UIBlurEffectStyleRegular]; 181 | UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect: blur]; 182 | blurView.alpha = 0; 183 | blurView.frame = self.view.bounds; 184 | [self.view addSubview: blurView]; 185 | 186 | [UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ 187 | 188 | blurView.alpha = 1; 189 | 190 | } completion:^(BOOL finished) { [self launchRespring]; }]; 191 | 192 | } 193 | 194 | 195 | - (void)launchRespring { 196 | 197 | pid_t pid; 198 | const char* args[] = {"killall", "backboardd", NULL}; 199 | posix_spawn(&pid, "/usr/bin/killall", NULL, NULL, (char *const *)args, NULL); 200 | 201 | } 202 | 203 | 204 | // ! UITableViewDataSource 205 | 206 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 207 | 208 | tableView.tableHeaderView = headerView; 209 | return [super tableView:tableView cellForRowAtIndexPath:indexPath]; 210 | 211 | } 212 | 213 | @end 214 | 215 | 216 | @implementation PSContributorsVC 217 | 218 | - (NSArray *)specifiers { 219 | 220 | if(!_specifiers) _specifiers = [self loadSpecifiersFromPlistName:@"PSContributors" target:self]; 221 | return _specifiers; 222 | 223 | } 224 | 225 | @end 226 | 227 | 228 | @implementation PSLinksVC 229 | 230 | - (NSArray *)specifiers { 231 | 232 | if(!_specifiers) _specifiers = [self loadSpecifiersFromPlistName:@"PSLinks" target:self]; 233 | return _specifiers; 234 | 235 | } 236 | 237 | 238 | - (void)launchLyricsLink { 239 | 240 | [self launchURL: [NSURL URLWithString: @"https://techcrunch.com/2020/06/29/in-a-significant-expansion-spotify-to-launch-real-time-lyrics-in-26-markets/"]]; 241 | 242 | } 243 | 244 | 245 | - (void)launchPayPal { 246 | 247 | [self launchURL: [NSURL URLWithString: @"https://paypal.me/Luki120"]]; 248 | 249 | } 250 | 251 | 252 | - (void)launchGitHub { 253 | 254 | [self launchURL: [NSURL URLWithString: @"https://github.com/Luki120/PerfectSpotify"]]; 255 | 256 | } 257 | 258 | 259 | - (void)launchAmelija { 260 | 261 | [self launchURL: [NSURL URLWithString: @"https://repo.twickd.com/get/me.luki.amelija"]]; 262 | 263 | } 264 | 265 | 266 | - (void)launchApril { 267 | 268 | [self launchURL: [NSURL URLWithString: @"https://repo.twickd.com/get/com.twickd.luki120.april"]]; 269 | 270 | } 271 | 272 | 273 | - (void)launchURL:(NSURL *)url { 274 | 275 | [UIApplication.sharedApplication openURL:url options:@{} completionHandler:nil]; 276 | 277 | } 278 | 279 | @end 280 | 281 | 282 | @implementation PSpotifyTableCell 283 | 284 | - (void)setTitle:(NSString *)title { 285 | 286 | [super setTitle: title]; 287 | self.titleLabel.textColor = kPSpotifyTintColor; 288 | 289 | } 290 | 291 | @end 292 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/++ Features.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | ++ Features 12 | footerText 13 | These options may cause the button images fail to load. 14 | 15 | 16 | cell 17 | PSSwitchCell 18 | default 19 | 20 | defaults 21 | me.luki.perfectspotifyprefs 22 | key 23 | enableSpotifyUI 24 | label 25 | Enable Spotify UI (Beta) 26 | id 27 | SpotifyUISwitch 28 | 29 | 30 | cell 31 | PSSwitchCell 32 | default 33 | 34 | defaults 35 | me.luki.perfectspotifyprefs 36 | key 37 | enableArtworkBasedColors 38 | label 39 | Enable Artwork Based Colors 40 | id 41 | ArtworkBasedColorsSwitch 42 | 43 | 44 | cell 45 | PSGroupCell 46 | id 47 | GroupCell1 48 | 49 | 50 | cell 51 | PSSwitchCell 52 | default 53 | 54 | defaults 55 | me.luki.perfectspotifyprefs 56 | key 57 | enableHaptics 58 | label 59 | Enable Haptics 60 | id 61 | HapticsSwitch 62 | 63 | 64 | cell 65 | PSGroupCell 66 | id 67 | GroupCell2 68 | 69 | 70 | cell 71 | PSSegmentCell 72 | height 73 | 50 74 | defaults 75 | me.luki.perfectspotifyprefs 76 | default 77 | 2 78 | key 79 | hapticsStrength 80 | validValues 81 | 82 | 0 83 | 1 84 | 2 85 | 86 | validTitles 87 | 88 | Soft 89 | Medium 90 | Strong 91 | 92 | id 93 | HapticsOptionsCell 94 | 95 | 96 | cell 97 | PSGroupCell 98 | 99 | 100 | cell 101 | PSSwitchCell 102 | default 103 | 104 | defaults 105 | me.luki.perfectspotifyprefs 106 | key 107 | saveCanvas 108 | label 109 | Save Canvas 110 | id 111 | SaveCanvasSwitch 112 | 113 | 114 | cell 115 | PSGroupCell 116 | id 117 | GroupCell3 118 | 119 | 120 | cell 121 | PSSegmentCell 122 | height 123 | 50 124 | defaults 125 | me.luki.perfectspotifyprefs 126 | default 127 | 0 128 | key 129 | saveCanvasDestination 130 | validValues 131 | 132 | 0 133 | 1 134 | 135 | validTitles 136 | 137 | Filza 138 | Gallery 139 | 140 | id 141 | CanvasOptionsCell 142 | 143 | 144 | cell 145 | PSGroupCell 146 | 147 | 148 | cell 149 | PSSwitchCell 150 | default 151 | 152 | defaults 153 | me.luki.perfectspotifyprefs 154 | key 155 | centerText 156 | label 157 | Center Artist/Song Labels 158 | 159 | 160 | cell 161 | PSSwitchCell 162 | default 163 | 164 | defaults 165 | me.luki.perfectspotifyprefs 166 | key 167 | textToTheTop 168 | label 169 | Align Song Labels To The Top 170 | 171 | 172 | title 173 | PerfectSpotify 174 | 175 | 176 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Assets/Amēlija@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luki120/PerfectSpotify/f75ecdd7e0efc0fc5afe54e25bf3811b61395d77/PSpotifyPrefs/Resources/Assets/Amēlija@2x.png -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Assets/April@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luki120/PerfectSpotify/f75ecdd7e0efc0fc5afe54e25bf3811b61395d77/PSpotifyPrefs/Resources/Assets/April@2x.png -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Assets/PSBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luki120/PerfectSpotify/f75ecdd7e0efc0fc5afe54e25bf3811b61395d77/PSpotifyPrefs/Resources/Assets/PSBanner.png -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Assets/PSIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luki120/PerfectSpotify/f75ecdd7e0efc0fc5afe54e25bf3811b61395d77/PSpotifyPrefs/Resources/Assets/PSIcon@2x.png -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Assets/PSIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luki120/PerfectSpotify/f75ecdd7e0efc0fc5afe54e25bf3811b61395d77/PSpotifyPrefs/Resources/Assets/PSIcon@3x.png -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Assets/PSpotifyIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luki120/PerfectSpotify/f75ecdd7e0efc0fc5afe54e25bf3811b61395d77/PSpotifyPrefs/Resources/Assets/PSpotifyIcon.png -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | PSpotifyPrefs 9 | CFBundleIdentifier 10 | me.luki.perfectspotifyprefs 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 | PSRootVC.m 23 | 24 | 25 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Now Playing UI.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | Background UI 12 | footerText 13 | Artwork based gradient colors. 14 | 15 | 16 | cell 17 | PSSwitchCell 18 | default 19 | 20 | defaults 21 | me.luki.perfectspotifyprefs 22 | key 23 | enableKleiColors 24 | label 25 | Enable Klei Colors 26 | 27 | 28 | cell 29 | PSGroupCell 30 | 31 | 32 | cell 33 | PSSwitchCell 34 | default 35 | 36 | defaults 37 | me.luki.perfectspotifyprefs 38 | key 39 | enableNowPlayingUIBGColor 40 | label 41 | Enable Custom BG Color 42 | 43 | 44 | cell 45 | PSGroupCell 46 | 47 | 48 | cell 49 | PSLinkCell 50 | cellClass 51 | GcColorPickerCell 52 | label 53 | Background Color 54 | defaults 55 | me.luki.perfectspotifyprefs 56 | key 57 | nowPlayingUIBGColor 58 | supportsAlpha 59 | 60 | fallback 61 | 000000 62 | 63 | 64 | cell 65 | PSGroupCell 66 | label 67 | Now Playing UI 68 | 69 | 70 | cell 71 | PSSwitchCell 72 | default 73 | 74 | defaults 75 | me.luki.perfectspotifyprefs 76 | key 77 | hideCloseButton 78 | label 79 | Hide Close Button 80 | 81 | 82 | cell 83 | PSSwitchCell 84 | default 85 | 86 | defaults 87 | me.luki.perfectspotifyprefs 88 | key 89 | hidePlaylistNameText 90 | label 91 | Hide Playlist Name 92 | 93 | 94 | cell 95 | PSSwitchCell 96 | default 97 | 98 | defaults 99 | me.luki.perfectspotifyprefs 100 | key 101 | hideContextMenuButton 102 | label 103 | Hide Context Button 104 | 105 | 106 | cell 107 | PSSwitchCell 108 | default 109 | 110 | defaults 111 | me.luki.perfectspotifyprefs 112 | key 113 | hideLikeButton 114 | label 115 | Hide Like Button 116 | 117 | 118 | cell 119 | PSSwitchCell 120 | default 121 | 122 | defaults 123 | me.luki.perfectspotifyprefs 124 | key 125 | hideSliderKnob 126 | label 127 | Hide Slider Knob 128 | 129 | 130 | cell 131 | PSSwitchCell 132 | default 133 | 134 | defaults 135 | me.luki.perfectspotifyprefs 136 | key 137 | hideTimeSlider 138 | label 139 | Hide Time Slider 140 | 141 | 142 | cell 143 | PSSwitchCell 144 | default 145 | 146 | defaults 147 | me.luki.perfectspotifyprefs 148 | key 149 | hideElapsedTime 150 | label 151 | Hide Elapsed Time 152 | 153 | 154 | cell 155 | PSSwitchCell 156 | default 157 | 158 | defaults 159 | me.luki.perfectspotifyprefs 160 | key 161 | hideRemainingTime 162 | label 163 | Hide Remaining Time 164 | 165 | 166 | cell 167 | PSSwitchCell 168 | default 169 | 170 | defaults 171 | me.luki.perfectspotifyprefs 172 | key 173 | hideShuffleButton 174 | label 175 | Hide Shuffle Button 176 | 177 | 178 | cell 179 | PSSwitchCell 180 | default 181 | 182 | defaults 183 | me.luki.perfectspotifyprefs 184 | key 185 | hidePreviousTrackButton 186 | label 187 | Hide Previous Track Button 188 | 189 | 190 | cell 191 | PSSwitchCell 192 | default 193 | 194 | defaults 195 | me.luki.perfectspotifyprefs 196 | key 197 | hidePlayPauseButton 198 | label 199 | Hide Play/Pause Button 200 | 201 | 202 | cell 203 | PSSwitchCell 204 | default 205 | 206 | defaults 207 | me.luki.perfectspotifyprefs 208 | key 209 | hideNextTrackButton 210 | label 211 | Hide Next Track Button 212 | 213 | 214 | cell 215 | PSSwitchCell 216 | default 217 | 218 | defaults 219 | me.luki.perfectspotifyprefs 220 | key 221 | hideRepeatButton 222 | label 223 | Hide Repeat Button 224 | 225 | 226 | cell 227 | PSSwitchCell 228 | default 229 | 230 | defaults 231 | me.luki.perfectspotifyprefs 232 | key 233 | hideDevicesButton 234 | label 235 | Hide Devices Button 236 | 237 | 238 | cell 239 | PSSwitchCell 240 | default 241 | 242 | defaults 243 | me.luki.perfectspotifyprefs 244 | key 245 | hideFeedbackButton 246 | label 247 | Hide Feedback Button 248 | 249 | 250 | cell 251 | PSSwitchCell 252 | default 253 | 254 | defaults 255 | me.luki.perfectspotifyprefs 256 | key 257 | hideShareButton 258 | label 259 | Hide Share Button 260 | 261 | 262 | cell 263 | PSSwitchCell 264 | default 265 | 266 | defaults 267 | me.luki.perfectspotifyprefs 268 | key 269 | hideQueueButton 270 | label 271 | Hide Queue Button 272 | 273 | 274 | cell 275 | PSGroupCell 276 | label 277 | Podcasts UI 278 | 279 | 280 | cell 281 | PSSwitchCell 282 | default 283 | 284 | defaults 285 | me.luki.perfectspotifyprefs 286 | key 287 | hideSpeedButton 288 | label 289 | Hide Speed Button 290 | 291 | 292 | cell 293 | PSSwitchCell 294 | default 295 | 296 | defaults 297 | me.luki.perfectspotifyprefs 298 | key 299 | hideBackButton 300 | label 301 | Hide Back Button 302 | 303 | 304 | cell 305 | PSSwitchCell 306 | default 307 | 308 | defaults 309 | me.luki.perfectspotifyprefs 310 | key 311 | hideForwardButton 312 | label 313 | Hide Forward Button 314 | 315 | 316 | cell 317 | PSSwitchCell 318 | default 319 | 320 | defaults 321 | me.luki.perfectspotifyprefs 322 | key 323 | hideMoonButton 324 | label 325 | Hide Moon Button 326 | 327 | 328 | cell 329 | PSGroupCell 330 | 331 | 332 | cell 333 | PSLinkCell 334 | detail 335 | ExtraFeaturesVC 336 | isController 337 | 338 | label 339 | ++ Features 340 | 341 | 342 | title 343 | PerfectSpotify 344 | 345 | 346 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/PSContributors.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | Lead Developer 12 | 13 | 14 | cell 15 | PSButtonCell 16 | cellClass 17 | GcTwitterCell 18 | accountLabel 19 | Luki120 20 | account 21 | Lukii120 22 | URL 23 | https://avatars.githubusercontent.com/u/74214115?v=4 24 | 25 | 26 | cell 27 | PSGroupCell 28 | label 29 | Inspiration to get into tweak development, help with OLED dark mode, credits for the Klei colors and preference panel idea 30 | 31 | 32 | cell 33 | PSButtonCell 34 | cellClass 35 | GcTwitterCell 36 | accountLabel 37 | Litten 38 | account 39 | schneelittchen 40 | URL 41 | https://avatars.githubusercontent.com/u/83172201?v=4 42 | 43 | 44 | cell 45 | PSGroupCell 46 | label 47 | Overall Help 48 | 49 | 50 | cell 51 | PSButtonCell 52 | cellClass 53 | GcTwitterCell 54 | accountLabel 55 | ETHN 56 | account 57 | EthanWhited 58 | URL 59 | https://avatars.githubusercontent.com/u/41249541?v=4 60 | 61 | 62 | cell 63 | PSButtonCell 64 | cellClass 65 | GcTwitterCell 66 | accountLabel 67 | MTAC 68 | account 69 | MTAC8 70 | URL 71 | https://avatars.githubusercontent.com/u/13209789?v=4 72 | 73 | 74 | cell 75 | PSGroupCell 76 | label 77 | Help with implementing the align song labels to the top option 78 | 79 | 80 | cell 81 | PSButtonCell 82 | cellClass 83 | GcTwitterCell 84 | accountLabel 85 | RuntimeOverflow 86 | account 87 | RuntimeOverflow 88 | URL 89 | https://avatars.githubusercontent.com/u/38386956?v=4 90 | 91 | 92 | cell 93 | PSGroupCell 94 | label 95 | Help with implementing libsparkcolourpicker at the time 96 | 97 | 98 | cell 99 | PSButtonCell 100 | cellClass 101 | GcTwitterCell 102 | accountLabel 103 | Azzou 104 | account 105 | AzzouDuGhetto 106 | URL 107 | https://avatars.githubusercontent.com/u/60717896?v=4 108 | 109 | 110 | cell 111 | PSGroupCell 112 | label 113 | Special thanks 114 | 115 | 116 | cell 117 | PSButtonCell 118 | cellClass 119 | GcTwitterCell 120 | accountLabel 121 | SouthernGirlWhoCode 122 | account 123 | SouthGalWhoCode 124 | URL 125 | https://pbs.twimg.com/profile_images/1433736955070722072/UdUjX5Rq_400x400.jpg 126 | 127 | 128 | cell 129 | PSGroupCell 130 | label 131 | Testers 132 | 133 | 134 | cell 135 | PSButtonCell 136 | cellClass 137 | GcTwitterCell 138 | accountLabel 139 | Denial 140 | account 141 | danielpan1234 142 | URL 143 | https://avatars.githubusercontent.com/u/56142819?v=4 144 | 145 | 146 | cell 147 | PSButtonCell 148 | cellClass 149 | GcTwitterCell 150 | accountLabel 151 | AlphaStream 152 | account 153 | Kutarin_ 154 | URL 155 | https://avatars.githubusercontent.com/u/5411646?v=4 156 | 157 | 158 | cell 159 | PSGroupCell 160 | footerText 161 | Icon by Nightwind, banner by ETHN. 162 | isStaticText 163 | 164 | 165 | 166 | title 167 | PerfectSpotify 168 | 169 | 170 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/PSLinks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | Maybe 12 | 13 | 14 | cell 15 | PSButtonCell 16 | cellClass 17 | PSpotifyTableCell 18 | action 19 | launchLyricsLink 20 | label 21 | Can I Use Native Lyrics? 22 | 23 | 24 | cell 25 | PSButtonCell 26 | cellClass 27 | PSpotifyTableCell 28 | action 29 | launchPayPal 30 | label 31 | Donate To Luki 🥰 32 | 33 | 34 | cell 35 | PSButtonCell 36 | cellClass 37 | PSpotifyTableCell 38 | action 39 | launchGitHub 40 | label 41 | Source Code 🔥 42 | 43 | 44 | cell 45 | PSGroupCell 46 | label 47 | You Might Like 48 | 49 | 50 | cell 51 | PSButtonCell 52 | cellClass 53 | PSpotifyTableCell 54 | action 55 | launchAmelija 56 | label 57 | Amēlija 58 | icon 59 | Assets/Amēlija@2x.png 60 | 61 | 62 | cell 63 | PSButtonCell 64 | cellClass 65 | PSpotifyTableCell 66 | action 67 | launchApril 68 | label 69 | April 70 | icon 71 | Assets/April@2x.png 72 | 73 | 74 | title 75 | Perfect Spotify 76 | 77 | 78 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/PSMisc.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | Homepage 12 | 13 | 14 | cell 15 | PSSwitchCell 16 | default 17 | 18 | defaults 19 | me.luki.perfectspotifyprefs 20 | key 21 | oledSpotify 22 | label 23 | OLED Spotify 24 | 25 | 26 | cell 27 | PSSwitchCell 28 | default 29 | 30 | defaults 31 | me.luki.perfectspotifyprefs 32 | key 33 | hideTabBarLabels 34 | label 35 | Hide Tab Bar Labels 36 | 37 | 38 | cell 39 | PSSwitchCell 40 | default 41 | 42 | defaults 43 | me.luki.perfectspotifyprefs 44 | key 45 | hideConnectButton 46 | label 47 | Hide Connect Button 48 | 49 | 50 | cell 51 | PSGroupCell 52 | label 53 | Search Page 54 | 55 | 56 | cell 57 | PSSwitchCell 58 | default 59 | 60 | defaults 61 | me.luki.perfectspotifyprefs 62 | key 63 | hideCancelButton 64 | label 65 | Hide Cancel Button 66 | 67 | 68 | cell 69 | PSSwitchCell 70 | default 71 | 72 | defaults 73 | me.luki.perfectspotifyprefs 74 | key 75 | hidePlayWhatYouLoveText 76 | label 77 | Hide Play What You Love Label 78 | 79 | 80 | cell 81 | PSSwitchCell 82 | default 83 | 84 | defaults 85 | me.luki.perfectspotifyprefs 86 | key 87 | hideClearRecentSearchesButton 88 | label 89 | Hide Clear Recent Searches Button 90 | 91 | 92 | cell 93 | PSGroupCell 94 | label 95 | Playlists 96 | 97 | 98 | cell 99 | PSSwitchCell 100 | default 101 | 102 | defaults 103 | me.luki.perfectspotifyprefs 104 | key 105 | showSongCount 106 | label 107 | Show Songs Count 108 | 109 | 110 | cell 111 | PSSwitchCell 112 | default 113 | 114 | defaults 115 | me.luki.perfectspotifyprefs 116 | key 117 | hideEnhanceButton 118 | label 119 | Hide Enhance Button 120 | 121 | 122 | cell 123 | PSSwitchCell 124 | default 125 | 126 | defaults 127 | me.luki.perfectspotifyprefs 128 | key 129 | hideAddSongsButton 130 | label 131 | Hide Add Songs Button 132 | 133 | 134 | cell 135 | PSSwitchCell 136 | default 137 | 138 | defaults 139 | me.luki.perfectspotifyprefs 140 | key 141 | hideQueuePopUp 142 | label 143 | No Pop-Up When Queuing 144 | 145 | 146 | cell 147 | PSSwitchCell 148 | default 149 | 150 | defaults 151 | me.luki.perfectspotifyprefs 152 | key 153 | noPopUp 154 | label 155 | No Pop-Up When Liking Songs 156 | 157 | 158 | cell 159 | PSGroupCell 160 | label 161 | Miscellaneous 162 | 163 | 164 | cell 165 | PSSwitchCell 166 | default 167 | 168 | defaults 169 | me.luki.perfectspotifyprefs 170 | key 171 | trueShuffle 172 | label 173 | True Shuffle 174 | 175 | 176 | cell 177 | PSSwitchCell 178 | default 179 | 180 | defaults 181 | me.luki.perfectspotifyprefs 182 | key 183 | showStatusBar 184 | label 185 | Show Status Bar 186 | 187 | 188 | cell 189 | PSSwitchCell 190 | default 191 | 192 | defaults 193 | me.luki.perfectspotifyprefs 194 | key 195 | disableStorylines 196 | label 197 | Disable Storylines 198 | 199 | 200 | cell 201 | PSSwitchCell 202 | default 203 | 204 | defaults 205 | me.luki.perfectspotifyprefs 206 | key 207 | enableLyricsForAllTracks 208 | label 209 | Lyrics For All Tracks 210 | id 211 | LyricsSwitch 212 | 213 | 214 | cell 215 | PSSwitchCell 216 | default 217 | 218 | defaults 219 | me.luki.perfectspotifyprefs 220 | key 221 | disableGeniusLyrics 222 | label 223 | Disable Genius Lyrics 224 | 225 | 226 | cell 227 | PSSwitchCell 228 | default 229 | 230 | defaults 231 | me.luki.perfectspotifyprefs 232 | key 233 | spoofIsPlayingRemotely 234 | label 235 | Spoof Playing Remotely Status 236 | 237 | 238 | title 239 | PerfectSpotify 240 | 241 | 242 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | Customization 12 | 13 | 14 | cell 15 | PSLinkCell 16 | detail 17 | MiscVC 18 | isController 19 | 20 | label 21 | Miscellaneous 22 | 23 | 24 | cell 25 | PSLinkCell 26 | detail 27 | NowPlayingUIVC 28 | isController 29 | 30 | label 31 | Now Playing UI 32 | 33 | 34 | cell 35 | PSLinkCell 36 | detail 37 | SpringBoardVC 38 | isController 39 | 40 | label 41 | SpringBoard (HS) 42 | 43 | 44 | cell 45 | PSGroupCell 46 | 47 | 48 | cell 49 | PSButtonCell 50 | cellClass 51 | PSpotifyTableCell 52 | action 53 | resetPreferences 54 | label 55 | Reset Preferences 56 | 57 | 58 | cell 59 | PSGroupCell 60 | 61 | 62 | cell 63 | PSLinkCell 64 | cellClass 65 | PSpotifyTableCell 66 | detail 67 | PSContributorsVC 68 | isController 69 | 70 | label 71 | Contributors 72 | 73 | 74 | cell 75 | PSGroupCell 76 | 77 | 78 | cell 79 | PSLinkCell 80 | cellClass 81 | PSpotifyTableCell 82 | detail 83 | PSLinksVC 84 | isController 85 | 86 | label 87 | Take A Look 88 | 89 | 90 | cell 91 | PSGroupCell 92 | footerAlignment 93 | 1 94 | footerText 95 | Officially supported Spotify versions ≤ 8.7.22 96 | isStaticText 97 | 98 | 99 | 100 | cell 101 | PSGroupCell 102 | footerAlignment 103 | 1 104 | footerText 105 | made by Luki with ❤️ 106 | isStaticText 107 | 108 | 109 | 110 | cell 111 | PSGroupCell 112 | footerAlignment 113 | 1 114 | footerText 115 | 2022 © Luki120 116 | isStaticText 117 | 118 | 119 | 120 | title 121 | PerfectSpotify 122 | 123 | 124 | -------------------------------------------------------------------------------- /PSpotifyPrefs/Resources/SpringBoard.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | label 11 | 3D Touch 12 | 13 | 14 | cell 15 | PSSwitchCell 16 | default 17 | 18 | defaults 19 | me.luki.perfectspotifyprefs 20 | key 21 | addPSpotifyShortcut 22 | label 23 | Add PSpotify Shortcut 24 | 25 | 26 | cell 27 | PSGroupCell 28 | label 29 | Spotify 30 | 31 | 32 | cell 33 | PSSwitchCell 34 | default 35 | 36 | defaults 37 | me.luki.perfectspotifyprefs 38 | key 39 | removeSpotifySearchShortcut 40 | label 41 | Remove "Search" 42 | 43 | 44 | cell 45 | PSSwitchCell 46 | default 47 | 48 | defaults 49 | me.luki.perfectspotifyprefs 50 | key 51 | removeSpotifyRecentlyPlayedShortcut 52 | label 53 | Remove "Recently Played" 54 | 55 | 56 | cell 57 | PSGroupCell 58 | label 59 | Stock 60 | 61 | 62 | cell 63 | PSSwitchCell 64 | default 65 | 66 | defaults 67 | me.luki.perfectspotifyprefs 68 | key 69 | removeEditHSShortcut 70 | label 71 | Remove "Edit HomeScreen" 72 | 73 | 74 | cell 75 | PSSwitchCell 76 | default 77 | 78 | defaults 79 | me.luki.perfectspotifyprefs 80 | key 81 | removeShareAppShortcut 82 | label 83 | Remove "Share App" 84 | 85 | 86 | cell 87 | PSSwitchCell 88 | default 89 | 90 | defaults 91 | me.luki.perfectspotifyprefs 92 | key 93 | removeRemoveAppShortcut 94 | label 95 | Remove "Remove App" 96 | 97 | 98 | 99 | title 100 | PerfectSpotify 101 | 102 | 103 | -------------------------------------------------------------------------------- /PSpotifyPrefs/layout/Library/PreferenceLoader/Preferences/PerfectSpotify.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | entry 6 | 7 | bundle 8 | PSpotifyPrefs 9 | cell 10 | PSLinkCell 11 | detail 12 | PSRootVC 13 | icon 14 | Assets/PSIcon.png 15 | isController 16 | 17 | label 18 | PerfectSpotify 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /PerfectSpotify.h: -------------------------------------------------------------------------------- 1 | @import UIKit; 2 | #import "Common/Common.h" 3 | #import "MediaRemote.h" 4 | #import 5 | #import 6 | #import 7 | 8 | 9 | // Miscellaneous 10 | 11 | static BOOL oledSpotify; 12 | static BOOL hideTabBarLabels; 13 | static BOOL hideConnectButton; 14 | 15 | static BOOL hideCancelButton; 16 | static BOOL hidePlayWhatYouLoveText; 17 | static BOOL hideClearRecentSearchesButton; 18 | 19 | static BOOL showSongCount; 20 | static BOOL hideEnhanceButton; 21 | static BOOL hideAddSongsButton; 22 | static BOOL hideQueuePopUp; 23 | static BOOL noPopUp; 24 | 25 | static BOOL trueShuffle; 26 | static BOOL showStatusBar; 27 | static BOOL disableStorylines; 28 | static BOOL enableLyricsForAllTracks; 29 | static BOOL disableGeniusLyrics; 30 | static BOOL spoofIsPlayingRemotely; 31 | 32 | 33 | // Now Playing UI 34 | 35 | static BOOL enableKleiColors; 36 | static BOOL enableNowPlayingUIBGColor; 37 | 38 | static NSString *nowPlayingUIBGColor; 39 | 40 | static BOOL hideCloseButton; 41 | static BOOL hidePlaylistNameText; 42 | static BOOL hideContextMenuButton; 43 | static BOOL hideLikeButton; 44 | static BOOL hideSliderKnob; 45 | static BOOL hideTimeSlider; 46 | static BOOL hideElapsedTime; 47 | static BOOL hideRemainingTime; 48 | static BOOL hideShuffleButton; 49 | static BOOL hidePreviousTrackButton; 50 | static BOOL hidePlayPauseButton; 51 | static BOOL hideNextTrackButton; 52 | static BOOL hideRepeatButton; 53 | static BOOL hideDevicesButton; 54 | static BOOL hideFeedbackButton; 55 | static BOOL hideShareButton; 56 | static BOOL hideQueueButton; 57 | 58 | static BOOL hideSpeedButton; 59 | static BOOL hideBackButton; 60 | static BOOL hideForwardButton; 61 | static BOOL hideMoonButton; 62 | 63 | static BOOL enableSpotifyUI; 64 | static BOOL enableHaptics; 65 | static BOOL enableArtworkBasedColors; 66 | 67 | static NSInteger hapticsStrength; 68 | 69 | static BOOL saveCanvas; 70 | 71 | static NSInteger saveCanvasDestination; 72 | 73 | static BOOL centerText; 74 | static BOOL textToTheTop; 75 | 76 | 77 | // SpringBoard 78 | 79 | static BOOL addPSpotifyShortcut; 80 | 81 | static BOOL removeEditHSShortcut; 82 | static BOOL removeShareAppShortcut; 83 | static BOOL removeRemoveAppShortcut; 84 | 85 | static BOOL removeSpotifySearchShortcut; 86 | static BOOL removeSpotifyRecentlyPlayedShortcut; 87 | 88 | #define kClass(string) NSClassFromString(string) 89 | #define kIsCurrentApp(string) [[[NSBundle mainBundle] bundleIdentifier] isEqual: string] 90 | #define kOrionExists [[NSFileManager defaultManager] fileExistsAtPath:@"/Library/MobileSubstrate/DynamicLibraries/OrionSettings.dylib"] 91 | #define kShuffleExists [[NSFileManager defaultManager] fileExistsAtPath:@"/Library/MobileSubstrate/DynamicLibraries/shuffle.dylib"] 92 | 93 | 94 | static void loadPrefs() { 95 | 96 | NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile: kPath]; 97 | NSMutableDictionary *prefs = dict ? [dict mutableCopy] : [NSMutableDictionary dictionary]; 98 | 99 | // Miscellaneous 100 | oledSpotify = prefs[@"oledSpotify"] ? [prefs[@"oledSpotify"] boolValue] : NO; 101 | hideTabBarLabels = prefs[@"hideTabBarLabels"] ? [prefs[@"hideTabBarLabels"] boolValue] : NO; 102 | hideConnectButton = prefs[@"hideConnectButton"] ? [prefs[@"hideConnectButton"] boolValue] : NO; 103 | 104 | hideCancelButton = prefs[@"hideCancelButton"] ? [prefs[@"hideCancelButton"] boolValue] : NO; 105 | hidePlayWhatYouLoveText = prefs[@"hidePlayWhatYouLoveText"] ? [prefs[@"hidePlayWhatYouLoveText"] boolValue] : NO; 106 | hideClearRecentSearchesButton = prefs[@"hideClearRecentSearchesButton"] ? [prefs[@"hideClearRecentSearchesButton"] boolValue] : NO; 107 | 108 | showSongCount = prefs[@"showSongCount"] ? [prefs[@"showSongCount"] boolValue] : NO; 109 | hideEnhanceButton = prefs[@"hideEnhanceButton"] ? [prefs[@"hideEnhanceButton"] boolValue] : NO; 110 | hideAddSongsButton = prefs[@"hideAddSongsButton"] ? [prefs[@"hideAddSongsButton"] boolValue] : NO; 111 | hideQueuePopUp = prefs[@"hideQueuePopUp"] ? [prefs[@"hideQueuePopUp"] boolValue] : NO; 112 | noPopUp = prefs[@"noPopUp"] ? [prefs[@"noPopUp"] boolValue] : NO; 113 | 114 | trueShuffle = prefs[@"trueShuffle"] ? [prefs[@"trueShuffle"] boolValue] : NO; 115 | showStatusBar = prefs[@"showStatusBar"] ? [prefs[@"showStatusBar"] boolValue] : NO; 116 | disableStorylines = prefs[@"disableStorylines"] ? [prefs[@"disableStorylines"] boolValue] : NO; 117 | enableLyricsForAllTracks = prefs[@"enableLyricsForAllTracks"] ? [prefs[@"enableLyricsForAllTracks"] boolValue] : NO; 118 | disableGeniusLyrics = prefs[@"disableGeniusLyrics"] ? [prefs[@"disableGeniusLyrics"] boolValue] : NO; 119 | spoofIsPlayingRemotely = prefs[@"spoofIsPlayingRemotely"] ? [prefs[@"spoofIsPlayingRemotely"] boolValue] : NO; 120 | 121 | // Now Playing UI 122 | enableKleiColors = prefs[@"enableKleiColors"] ? [prefs[@"enableKleiColors"] boolValue] : NO; 123 | enableNowPlayingUIBGColor = prefs[@"enableNowPlayingUIBGColor"] ? [prefs[@"enableNowPlayingUIBGColor"] boolValue] : NO; 124 | 125 | nowPlayingUIBGColor = prefs[@"nowPlayingUIBGColor"] ?: [prefs[@"nowPlayingUIBGColor"] stringValue]; 126 | 127 | hideCloseButton = prefs[@"hideCloseButton"] ? [prefs[@"hideCloseButton"] boolValue] : NO; 128 | hidePlaylistNameText = prefs[@"hidePlaylistNameText"] ? [prefs[@"hidePlaylistNameText"] boolValue] : NO; 129 | hideContextMenuButton = prefs[@"hideContextMenuButton"] ? [prefs[@"hideContextMenuButton"] boolValue] : NO; 130 | hideLikeButton = prefs[@"hideLikeButton"] ? [prefs[@"hideLikeButton"] boolValue] : NO; 131 | hideSliderKnob = prefs[@"hideSliderKnob"] ? [prefs[@"hideSliderKnob"] boolValue] : NO; 132 | hideTimeSlider = prefs[@"hideTimeSlider"] ? [prefs[@"hideTimeSlider"] boolValue] : NO; 133 | hideElapsedTime = prefs[@"hideElapsedTime"] ? [prefs[@"hideElapsedTime"] boolValue] : NO; 134 | hideRemainingTime = prefs[@"hideRemainingTime"] ? [prefs[@"hideRemainingTime"] boolValue] : NO; 135 | hideShuffleButton = prefs[@"hideShuffleButton"] ? [prefs[@"hideShuffleButton"] boolValue] : NO; 136 | hidePreviousTrackButton = prefs[@"hidePreviousTrackButton"] ? [prefs[@"hidePreviousTrackButton"] boolValue] : NO; 137 | hidePlayPauseButton = prefs[@"hidePlayPauseButton"] ? [prefs[@"hidePlayPauseButton"] boolValue] : NO; 138 | hideNextTrackButton = prefs[@"hideNextTrackButton"] ? [prefs[@"hideNextTrackButton"] boolValue] : NO; 139 | hideRepeatButton = prefs[@"hideRepeatButton"] ? [prefs[@"hideRepeatButton"] boolValue] : NO; 140 | hideDevicesButton = prefs[@"hideDevicesButton"] ? [prefs[@"hideDevicesButton"] boolValue] : NO; 141 | hideFeedbackButton = prefs[@"hideFeedbackButton"] ? [prefs[@"hideFeedbackButton"] boolValue] : NO; 142 | hideShareButton = prefs[@"hideShareButton"] ? [prefs[@"hideShareButton"] boolValue] : NO; 143 | hideQueueButton = prefs[@"hideQueueButton"] ? [prefs[@"hideQueueButton"] boolValue] : NO; 144 | 145 | enableSpotifyUI = prefs[@"enableSpotifyUI"] ? [prefs[@"enableSpotifyUI"] boolValue] : NO; 146 | enableHaptics = prefs[@"enableHaptics"] ? [prefs[@"enableHaptics"] boolValue] : NO; 147 | enableArtworkBasedColors = prefs[@"enableArtworkBasedColors"] ? [prefs[@"enableArtworkBasedColors"] boolValue] : NO; 148 | hapticsStrength = prefs[@"hapticsStrength"] ? [prefs[@"hapticsStrength"] integerValue] : 2; 149 | 150 | saveCanvas = prefs[@"saveCanvas"] ? [prefs[@"saveCanvas"] boolValue] : NO; 151 | saveCanvasDestination = prefs[@"saveCanvasDestination"] ? [prefs[@"saveCanvasDestination"] integerValue] : 0; 152 | 153 | centerText = prefs[@"centerText"] ? [prefs[@"centerText"] boolValue] : NO; 154 | textToTheTop = prefs[@"textToTheTop"] ? [prefs[@"textToTheTop"] boolValue] : NO; 155 | 156 | hideSpeedButton = prefs[@"hideSpeedButton"] ? [prefs[@"hideSpeedButton"] boolValue] : NO; 157 | hideBackButton = prefs[@"hideBackButton"] ? [prefs[@"hideBackButton"] boolValue] : NO; 158 | hideForwardButton = prefs[@"hideForwardButton"] ? [prefs[@"hideForwardButton"] boolValue] : NO; 159 | hideMoonButton = prefs[@"hideMoonButton"] ? [prefs[@"hideMoonButton"] boolValue] : NO; 160 | 161 | // SpringBoard 162 | addPSpotifyShortcut = prefs[@"addPSpotifyShortcut"] ? [prefs[@"addPSpotifyShortcut"] boolValue] : NO; 163 | 164 | removeEditHSShortcut = prefs[@"removeEditHSShortcut"] ? [prefs[@"removeEditHSShortcut"] boolValue] : NO; 165 | removeShareAppShortcut = prefs[@"removeShareAppShortcut"] ? [prefs[@"removeShareAppShortcut"] boolValue] : NO; 166 | removeRemoveAppShortcut = prefs[@"removeRemoveAppShortcut"] ? [prefs[@"removeRemoveAppShortcut"] boolValue] : NO; 167 | 168 | removeSpotifySearchShortcut = prefs[@"removeSpotifySearchShortcut"] ? [prefs[@"removeSpotifySearchShortcut"] boolValue] : NO; 169 | removeSpotifyRecentlyPlayedShortcut = prefs[@"removeSpotifyRecentlyPlayedShortcut"] ? [prefs[@"removeSpotifyRecentlyPlayedShortcut"] boolValue] : NO; 170 | 171 | } 172 | 173 | 174 | // Global 175 | 176 | static CAGradientLayer *gradient; 177 | static UIColor *cachedPrimaryColors; 178 | static UIColor *cachedSecondaryColors; 179 | static UIColor *cachedBackgroundColors; 180 | 181 | 182 | @interface SPTNowPlayingBackgroundViewController : UIViewController 183 | - (void)setColors; // libKitten 184 | @end 185 | 186 | 187 | // Miscellaneous 188 | 189 | @interface GLUEGradientView : UIView 190 | @property (assign, nonatomic) CGFloat alpha; 191 | @end 192 | 193 | 194 | @interface SPTHomeView : UIView 195 | @end 196 | 197 | 198 | @interface SPTHomeGradientBackgroundView : UIView 199 | @end 200 | 201 | 202 | @interface SPTBarGradientView : UIView 203 | @end 204 | 205 | 206 | @interface GLUEEmptyStateView : UIView 207 | @end 208 | 209 | 210 | @interface SPTSearch2ViewController : UIViewController 211 | @end 212 | 213 | 214 | @interface SPTUIBlurView : UIView 215 | @end 216 | 217 | 218 | @interface SPTNowPlayingBarViewController : UIViewController 219 | @property (nonatomic, strong) UIView *contentView; 220 | @end 221 | 222 | 223 | @interface SPTEncoreLabel : UILabel 224 | - (UIViewController *)_viewControllerForAncestor; 225 | @end 226 | 227 | 228 | // Now Playing UI 229 | 230 | @interface SPTNowPlayingTitleButton : UIButton 231 | @end 232 | 233 | 234 | @interface SPTNowPlayingMarqueeLabel : UIView 235 | @property UIView *topLabel, *bottomLabel; 236 | @property UIColor *textColor; 237 | @end 238 | 239 | 240 | @interface SPTContextMenuAccessoryButton : UIButton 241 | @end 242 | 243 | 244 | @interface SPTNowPlayingSliderV2 : UIView 245 | @end 246 | 247 | 248 | // ++ Features 249 | // Spotify UI 250 | 251 | @interface SPTNowPlayingHeadUnitView : UIView 252 | @property (nonatomic, strong) UIButton *rewindButton; 253 | @property (nonatomic, strong) UIButton *skipButton; 254 | @property (nonatomic, strong) UIButton *playPauseButton; 255 | @property (nonatomic, strong) UIStackView *buttonsStackView; 256 | - (void)didTapRewindButton; 257 | - (void)didTapPlayPauseButton; 258 | - (void)didTapSkipButton; 259 | - (void)sendMRCommandAndTriggerHapticsIfRequested:(MRCommand)command; 260 | - (void)setupSpotifyUI; 261 | - (void)setupSpotifyUIConstraints; 262 | - (UIButton *)createButtonWithImage:(UIImage *)image forSelector:(SEL)selector; 263 | - (void)setupSizeConstraintsForButton:(UIButton *)button; 264 | @end 265 | 266 | 267 | @interface SPTPlayerState : NSObject 268 | @property (assign, getter=isPaused, nonatomic) BOOL paused; 269 | @end 270 | 271 | 272 | // Canvas 273 | 274 | @interface SPTNowPlayingViewController : UIViewController 275 | - (void)didDoubleTapCanvas; 276 | @end 277 | 278 | 279 | @interface SPTPopupDialog : NSObject 280 | + (instancetype)popupWithTitle:(NSString *)arg1 message:(NSString *)arg2 dismissButtonTitle:(NSString *)arg3; 281 | @end 282 | 283 | 284 | @interface SPTPopupManager : NSObject 285 | @property (assign, nonatomic) NSMutableArray *presentationQueue; 286 | + (SPTPopupManager *)sharedManager; 287 | - (void)presentNextQueuedPopup; 288 | @end 289 | 290 | 291 | // 3DTouch shortcut items 292 | 293 | @interface SBSApplicationShortcutIcon : NSObject 294 | @end 295 | 296 | 297 | @interface SBSApplicationShortcutCustomImageIcon : SBSApplicationShortcutIcon 298 | - (id)initWithImageData:(id)arg1 dataType:(NSInteger)arg2 isTemplate:(BOOL)arg3; 299 | @end 300 | 301 | 302 | @interface SBSApplicationShortcutItem : NSObject 303 | @property (copy, nonatomic) NSString *type; 304 | @property (copy, nonatomic) NSString *localizedTitle; 305 | @property (copy, nonatomic) SBSApplicationShortcutIcon *icon; 306 | @end 307 | 308 | 309 | @interface SBIcon : NSObject 310 | @property (copy, nonatomic, readonly) NSString *displayName; 311 | - (id)applicationBundleID; 312 | - (BOOL)isApplicationIcon; 313 | @end 314 | 315 | 316 | @interface SBIconView : NSObject 317 | @property (nonatomic, strong) SBIcon *icon; 318 | - (NSString *)applicationBundleIdentifier; 319 | - (NSString *)applicationBundleIdentifierForShortcuts; 320 | @end 321 | 322 | 323 | @interface UIApplication () 324 | - (BOOL)_openURL:(NSURL *)url; 325 | @end 326 | 327 | 328 | // For instances 329 | 330 | static id playlistController = nil; 331 | static SPTNowPlayingHeadUnitView *headUnitView = nil; 332 | static SPTNowPlayingViewController *canvasContentLayerVC = nil; 333 | -------------------------------------------------------------------------------- /PerfectSpotify.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.spotify.client", "com.apple.springboard" ); }; } 2 | -------------------------------------------------------------------------------- /PerfectSpotify.xm: -------------------------------------------------------------------------------- 1 | #import "PerfectSpotify.h" 2 | 3 | 4 | /*************/ 5 | // ! | Colors | 6 | /*************/ 7 | 8 | %group PerfectSpotify 9 | 10 | 11 | %hook MPNowPlayingInfoCenterArtworkContext 12 | 13 | 14 | - (void)setArtworkData:(NSData *)data { 15 | 16 | %orig; 17 | [NSNotificationCenter.defaultCenter postNotificationName:@"kleiUpdateColors" object:nil]; 18 | 19 | } 20 | 21 | 22 | %end 23 | 24 | 25 | %hook SPTNowPlayingBackgroundViewController 26 | 27 | 28 | %new 29 | 30 | - (void)setColors { // get artwork colors 31 | 32 | loadPrefs(); 33 | 34 | MRMediaRemoteGetNowPlayingInfo(dispatch_get_main_queue(), ^(CFDictionaryRef information) { 35 | 36 | NSDictionary *dict = (__bridge NSDictionary *)information; 37 | 38 | if(dict) { 39 | 40 | CATransition *transition = [CATransition animation]; 41 | transition.type = kCATransitionFade; 42 | transition.duration = 1.0f; 43 | transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 44 | 45 | [gradient addAnimation:transition forKey:nil]; 46 | [headUnitView.rewindButton.layer addAnimation:transition forKey:nil]; 47 | [headUnitView.playPauseButton.layer addAnimation:transition forKey:nil]; 48 | [headUnitView.skipButton.layer addAnimation:transition forKey:nil]; 49 | 50 | NSData *artworkData = [dict objectForKey:(__bridge NSString *)kMRMediaRemoteNowPlayingInfoArtworkData]; 51 | 52 | if(artworkData != nil) { 53 | 54 | UIImage *artwork = [UIImage imageWithData: artworkData]; 55 | BOOL dont = NO; 56 | 57 | if(artworkData.length == 0x282e && artwork.size.width == 0x258) { 58 | 59 | if(!cachedPrimaryColors) cachedPrimaryColors = [libKitten primaryColor:artwork]; 60 | if(!cachedSecondaryColors) cachedSecondaryColors = [libKitten secondaryColor:artwork]; 61 | if(!cachedBackgroundColors) cachedBackgroundColors = [libKitten backgroundColor:artwork]; 62 | 63 | dont = YES; 64 | 65 | } 66 | 67 | UIColor *primaryColor = dont ? cachedPrimaryColors : [libKitten primaryColor:artwork]; 68 | UIColor *secondaryColor = dont ? cachedSecondaryColors : [libKitten secondaryColor:artwork]; 69 | UIColor *backgroundColor = dont ? cachedBackgroundColors : [libKitten backgroundColor:artwork]; 70 | 71 | gradient.colors = @[(id)backgroundColor.CGColor, (id)primaryColor.CGColor]; 72 | 73 | if(enableArtworkBasedColors) { 74 | headUnitView.rewindButton.tintColor = secondaryColor; 75 | headUnitView.playPauseButton.tintColor = secondaryColor; 76 | headUnitView.skipButton.tintColor = secondaryColor; 77 | } 78 | 79 | } 80 | 81 | } else { 82 | 83 | cachedPrimaryColors = nil; 84 | cachedSecondaryColors = nil; 85 | cachedBackgroundColors = nil; 86 | 87 | gradient.colors = nil; 88 | headUnitView.rewindButton.tintColor = UIColor.whiteColor; 89 | headUnitView.playPauseButton.tintColor = UIColor.whiteColor; 90 | headUnitView.skipButton.tintColor = UIColor.whiteColor; 91 | 92 | } 93 | 94 | }); 95 | 96 | } 97 | 98 | 99 | - (void)viewDidLoad { // add gradient, Litten's Klei gradients, thank you 100 | 101 | %orig; 102 | if(!enableKleiColors) return; 103 | 104 | NSArray *gradientColors = @[(id)UIColor.clearColor.CGColor, (id)UIColor.clearColor.CGColor]; 105 | 106 | if(gradient) return; 107 | 108 | gradient = [CAGradientLayer layer]; 109 | gradient.frame = self.view.bounds; 110 | gradient.colors = gradientColors; 111 | gradient.locations = @[@(-0.5), @(1.5)]; 112 | gradient.startPoint = CGPointMake(0.0, 0.5); 113 | gradient.endPoint = CGPointMake(0.5, 1.0); 114 | [self.view.layer insertSublayer:gradient atIndex:0]; 115 | 116 | [self setColors]; 117 | 118 | // add notification observer to dynamically change artwork 119 | [NSNotificationCenter.defaultCenter removeObserver:self name:@"kleiUpdateColors" object:nil]; 120 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(setColors) name:@"kleiUpdateColors" object:nil]; 121 | 122 | } 123 | 124 | 125 | %end 126 | 127 | 128 | %hook SPTNowPlayingBackgroundViewController 129 | 130 | 131 | - (UIColor *)color { // OLED view to now playing UI or custom colors 132 | 133 | if(!enableNowPlayingUIBGColor) return %orig; 134 | return [GcColorPickerUtils colorWithHex: nowPlayingUIBGColor]; 135 | 136 | } 137 | 138 | 139 | %end 140 | 141 | 142 | /*****************************/ 143 | // ! | Miscellaneous Settings | 144 | /*****************************/ 145 | 146 | 147 | %hook RootSettingsViewController 148 | 149 | 150 | - (BOOL)isPlayingRemotely { 151 | 152 | if(!spoofIsPlayingRemotely) return %orig; 153 | return NO; 154 | 155 | } 156 | 157 | 158 | %end 159 | 160 | %hook GLUEGradientView 161 | 162 | 163 | - (void)didMoveToWindow { // OLED Spotify 164 | 165 | %orig; 166 | if(oledSpotify) self.alpha = 0; 167 | 168 | } 169 | 170 | 171 | %end 172 | 173 | 174 | %hook SPTHomeView 175 | 176 | 177 | - (void)didMoveToWindow { 178 | 179 | %orig; 180 | if(oledSpotify) self.backgroundColor = UIColor.blackColor; 181 | 182 | } 183 | 184 | 185 | %end 186 | 187 | 188 | %hook SPTHomeGradientBackgroundView 189 | 190 | 191 | - (void)didMoveToWindow { 192 | 193 | %orig; 194 | if(oledSpotify) self.backgroundColor = UIColor.blackColor; 195 | 196 | } 197 | 198 | 199 | %end 200 | 201 | 202 | %hook ShortcutPlaylistButton 203 | 204 | 205 | - (void)didMoveToWindow { 206 | 207 | %orig; 208 | if(!oledSpotify) return; 209 | 210 | for(UIView *shorcutView in [self subviews]) 211 | 212 | if([shorcutView isKindOfClass:UIView.class]) shorcutView.backgroundColor = UIColor.blackColor; 213 | 214 | } 215 | 216 | 217 | %end 218 | 219 | 220 | %hook SPTNowPlayingBarViewController 221 | 222 | 223 | - (void)viewDidLoad { // OLED mini now playing bar 224 | 225 | %orig; 226 | if(!oledSpotify) return; 227 | 228 | self.contentView.backgroundColor = UIColor.blackColor; 229 | 230 | } 231 | 232 | 233 | %end 234 | 235 | 236 | %hook SPTBarGradientView 237 | 238 | 239 | - (void)didMoveToWindow { 240 | 241 | %orig; 242 | if(oledSpotify) self.hidden = YES; 243 | 244 | } 245 | 246 | 247 | %end 248 | 249 | 250 | %hook UITabBar 251 | 252 | 253 | - (void)didMoveToWindow { 254 | 255 | %orig; 256 | if(!oledSpotify) return; 257 | 258 | UITabBarAppearance *tabBar = [UITabBarAppearance new]; 259 | tabBar.backgroundColor = UIColor.blackColor; 260 | self.standardAppearance = tabBar; 261 | 262 | } 263 | 264 | 265 | %end 266 | 267 | 268 | %hook GLUEEmptyStateView 269 | 270 | 271 | - (void)didMoveToWindow { // OLED Search page (when you tap on search) 272 | 273 | %orig; 274 | if(oledSpotify) self.backgroundColor = UIColor.blackColor; 275 | 276 | } 277 | 278 | 279 | %end 280 | 281 | 282 | %hook SPTSearch2ViewController 283 | 284 | 285 | - (void)viewDidLoad { // OLED Search page (with history) 286 | 287 | %orig; 288 | if(oledSpotify) self.view.backgroundColor = UIColor.blackColor; 289 | 290 | } 291 | 292 | 293 | %end 294 | 295 | 296 | %hook SPTUIBlurView 297 | 298 | 299 | - (void)didMoveToWindow { 300 | 301 | %orig; 302 | if(!oledSpotify) return; 303 | 304 | self.backgroundColor = UIColor.blackColor; 305 | for(UIVisualEffectView *effectView in self.subviews) [effectView removeFromSuperview]; 306 | 307 | } 308 | 309 | 310 | %end 311 | 312 | 313 | %hook UITabBarButtonLabel 314 | 315 | 316 | - (void)setText:(NSString *)text { // Hide Tab Bar button's labels 317 | 318 | if(!hideTabBarLabels) return %orig; 319 | 320 | } 321 | 322 | 323 | %end 324 | 325 | 326 | %hook ConnectButton 327 | 328 | 329 | - (void)didMoveToWindow { // Connect Button in main page 330 | 331 | %orig; 332 | if(hideConnectButton) [self setHidden: YES]; 333 | 334 | } 335 | 336 | 337 | %end 338 | 339 | 340 | %hook SPTSearchUISearchControls 341 | 342 | 343 | - (void)didMoveToWindow { // Hide Cancel Button in search page 344 | 345 | %orig; 346 | if(hideCancelButton) MSHookIvar(self, "_cancelButton").hidden = YES; 347 | 348 | } 349 | 350 | 351 | %end 352 | 353 | 354 | %hook PlayWhatYouLoveText 355 | 356 | 357 | - (void)didMoveToWindow { // Hide "Play what you love text" string 358 | 359 | %orig; 360 | if(hidePlayWhatYouLoveText) [self setHidden: YES]; 361 | 362 | } 363 | 364 | 365 | %end 366 | 367 | 368 | %hook ClearRecentSearchesButton 369 | 370 | 371 | - (void)didMoveToWindow { // Hide Clear Recent Searches Button 372 | 373 | %orig; 374 | if(hideClearRecentSearchesButton) [self setHidden: YES]; 375 | 376 | } 377 | 378 | 379 | %end 380 | 381 | 382 | %hook PlaylistsController 383 | 384 | 385 | - (void)freeTierPlaylistModel:(id)arg1 playlistModelEntityDidChange:(id)arg2 { // save an instance of PlaylistsController 386 | 387 | %orig; 388 | playlistController = self; 389 | 390 | } 391 | 392 | 393 | %end 394 | 395 | 396 | %hook SPTEncoreLabel 397 | 398 | 399 | - (void)setText:(NSString *)text { // smh, hooking the source of this didn't work for some reason, so the view it is :deadAf: 400 | 401 | %orig; 402 | if(!showSongCount) return; 403 | 404 | UIViewController *ancestor = [self _viewControllerForAncestor]; 405 | if(![ancestor isKindOfClass:%c(SPTFreeTierPlaylistEncoreHeaderViewController)]) return; 406 | 407 | NSUInteger totalCount = [playlistController numberOfItems]; 408 | 409 | if(([self.text containsString:@"h"] 410 | || [self.text containsString:@"m"]) 411 | && ![self.text containsString:@"Enhance"]) 412 | 413 | %orig([NSString stringWithFormat:@"%@, %lu songs", text, totalCount]); 414 | 415 | } 416 | 417 | 418 | %end 419 | 420 | 421 | %hook SPTFreeTierPlaylistEncoreHeaderControllerImplementation 422 | 423 | 424 | - (BOOL)isEnhanceable { // Hide Enhance Button 425 | 426 | if(!hideEnhanceButton) return %orig; 427 | return NO; 428 | 429 | } 430 | 431 | 432 | %end 433 | 434 | 435 | %hook SPTFreeTierPlaylistAdditionalCallToActionAddSongsImplementation 436 | 437 | 438 | - (BOOL)enabled { // Hide "Add Songs" button in playlist 439 | 440 | if(!hideAddSongsButton) return %orig; 441 | return NO; 442 | 443 | } 444 | 445 | 446 | %end 447 | 448 | 449 | %hook SPTProgressView 450 | 451 | 452 | + (void)showGaiaContextMenuProgressViewWithTitle:(id)arg1 { // No Pop-Up when queuing 453 | 454 | if(!hideQueuePopUp) return %orig; 455 | 456 | } 457 | 458 | 459 | %end 460 | 461 | 462 | %hook SPTSnackbarView 463 | 464 | 465 | - (id)initWithContentView:(id)arg1 { // No Pop-Up when liking songs 466 | 467 | if(!noPopUp) return %orig; 468 | return nil; 469 | 470 | } 471 | 472 | 473 | %end 474 | 475 | 476 | %hook SPTSignupParameterShufflerImplementation 477 | 478 | 479 | - (id)createShuffledKeyListFromParameters:(id)arg1 { // True Shuffle (experimental) 480 | 481 | if(!trueShuffle) return %orig; 482 | return nil; 483 | 484 | } 485 | 486 | 487 | %end 488 | 489 | 490 | %hook SPTSignupParameterShufflerImplementation 491 | 492 | 493 | - (id)shuffleEntriesFromQueryParameters:(id)arg1 { 494 | 495 | if(!trueShuffle) return %orig; 496 | return nil; 497 | 498 | } 499 | 500 | 501 | %end 502 | 503 | 504 | %hook SPTSignupParameterShufflerImplementation 505 | 506 | 507 | - (id)createHashFromParameterValues:(id)arg1 { 508 | 509 | if(!trueShuffle) return %orig; 510 | return nil; 511 | 512 | } 513 | 514 | 515 | %end 516 | 517 | 518 | %hook SPTSignupParameterShufflerImplementation 519 | 520 | 521 | - (id)md5FromString:(id)arg1 { 522 | 523 | if(!trueShuffle) return %orig; 524 | return nil; 525 | 526 | } 527 | 528 | 529 | %end 530 | 531 | 532 | %hook SPTSignupParameterShufflerEntry 533 | 534 | 535 | - (id)initWithKey:(id)arg1 value:(id)arg2 { 536 | 537 | if(!trueShuffle) return %orig; 538 | return nil; 539 | 540 | } 541 | 542 | 543 | - (void)updateIndex:(NSInteger)arg1 andSecret:(id)arg2 { 544 | 545 | if(!trueShuffle) return %orig; 546 | 547 | } 548 | 549 | 550 | - (NSInteger)compareKey:(id)arg1 { 551 | 552 | if(!trueShuffle) return %orig; 553 | return -100; 554 | 555 | } 556 | 557 | 558 | - (NSInteger)compareUsingSecretAndThenIndex:(id)arg1 { 559 | 560 | if(!trueShuffle) return %orig; 561 | return -100; 562 | 563 | } 564 | 565 | 566 | - (id)key { 567 | 568 | if(!trueShuffle) return %orig; 569 | return nil; 570 | 571 | } 572 | 573 | 574 | - (void)setKey:(id)arg1 { 575 | 576 | if(!trueShuffle) return %orig; 577 | 578 | } 579 | 580 | 581 | - (id)value { 582 | 583 | if(!trueShuffle) return %orig; 584 | return nil; 585 | 586 | } 587 | 588 | 589 | - (void)setValue:(id)arg1 { 590 | 591 | if(!trueShuffle) return %orig; 592 | 593 | } 594 | 595 | 596 | - (id)secret { 597 | 598 | if(!trueShuffle) return %orig; 599 | return nil; 600 | 601 | } 602 | 603 | 604 | 605 | - (void)setSecret:(id)arg1 { 606 | 607 | if(!trueShuffle) return %orig; 608 | 609 | } 610 | 611 | 612 | - (NSInteger)index { 613 | 614 | if(!trueShuffle) return %orig; 615 | return 0; 616 | 617 | } 618 | 619 | 620 | - (void)setIndex:(NSInteger)arg1 { 621 | 622 | if(!trueShuffle) return %orig; 623 | 624 | } 625 | 626 | 627 | %end 628 | 629 | 630 | %hook SPTStatusBarManager 631 | 632 | 633 | - (void)setStatusBarHiddenImmediate:(BOOL)arg1 withAnimation:(NSInteger)arg2 { // Show Status Bar 634 | 635 | if(!showStatusBar) return %orig; 636 | %orig(NO, arg2); 637 | 638 | } 639 | 640 | 641 | %end 642 | 643 | 644 | %hook SPTStorylinesEnabledManager 645 | 646 | 647 | - (BOOL)storylinesEnabledForTrack:(id)arg1 { // Disable Storylines 648 | 649 | if(!disableStorylines) return %orig; 650 | return NO; 651 | 652 | } 653 | 654 | 655 | %end 656 | 657 | 658 | %hook SPTLyricsV2TestManagerImplementation 659 | 660 | 661 | - (BOOL)isFeatureEnabled { // Lyrics for all tracks 662 | 663 | if(!enableLyricsForAllTracks) return %orig; 664 | return YES; 665 | 666 | } 667 | 668 | 669 | %end 670 | 671 | 672 | %hook SPTLyricsV2Service 673 | 674 | 675 | - (BOOL)lyricsAvailableForTrack:(id)arg1 { 676 | 677 | if(!enableLyricsForAllTracks) return %orig; 678 | return YES; 679 | 680 | } 681 | 682 | 683 | %end 684 | 685 | 686 | %hook SPTGeniusService 687 | 688 | 689 | - (BOOL)isTrackGeniusEnabled:(id)arg1 { // Disable Genius Lyrics 690 | 691 | if(!disableGeniusLyrics) return %orig; 692 | return NO; 693 | 694 | } 695 | 696 | 697 | %end 698 | 699 | 700 | /*********************/ 701 | // ! | Now Playing UI | 702 | /*********************/ 703 | 704 | %hook SPTNowPlayingTitleButton 705 | 706 | 707 | - (void)didMoveToWindow { // Hide Close Button 708 | 709 | %orig; 710 | if(hideCloseButton) self.hidden = YES; 711 | 712 | } 713 | 714 | 715 | %end 716 | 717 | 718 | %hook SPTNowPlayingNavigationBarViewV2 719 | 720 | 721 | - (void)didMoveToWindow { // Hide Playlist Title 722 | 723 | %orig; 724 | if(hidePlaylistNameText) MSHookIvar(self, "_titleLabel").hidden = YES; 725 | 726 | } 727 | 728 | 729 | %end 730 | 731 | 732 | %hook SPTContextMenuAccessoryButton 733 | 734 | 735 | - (void)didMoveToWindow { // Hide Context Menu Button 736 | 737 | %orig; 738 | if(hideContextMenuButton) self.hidden = YES; 739 | 740 | } 741 | 742 | 743 | %end 744 | 745 | 746 | %hook SPTNowPlayingHeartButtonViewController 747 | 748 | 749 | - (id)initWithModel:(id)arg1 auxiliaryActionsHandler:(id)arg2 testManager:(id)arg3 { // Hide Like Button 750 | 751 | if(!hideLikeButton) return %orig; 752 | return nil; 753 | 754 | } 755 | 756 | 757 | %end 758 | 759 | 760 | %hook _UISlideriOSVisualElement 761 | 762 | 763 | - (void)setAlpha:(double)arg1 { // Hide Knob View 764 | 765 | if(hideSliderKnob) MSHookIvar(self, "_thumbView").alpha = 0; 766 | else %orig; 767 | 768 | } 769 | 770 | 771 | %end 772 | 773 | 774 | %hook SPTNowPlayingSliderV2 775 | 776 | 777 | - (void)didMoveToWindow { // Hide Time Slider 778 | 779 | %orig; 780 | if(hideTimeSlider) self.hidden = YES; 781 | 782 | } 783 | 784 | 785 | %end 786 | 787 | 788 | %hook SPTNowPlayingDurationViewV2 789 | 790 | 791 | - (void)didMoveToWindow { // Hide elapsed and remaining time labels 792 | 793 | if(hideElapsedTime) MSHookIvar(self, "_timeTakenLabel").hidden = YES; 794 | if(hideRemainingTime) MSHookIvar(self, "_timeRemainingLabel").hidden = YES; 795 | else %orig; 796 | 797 | } 798 | 799 | 800 | %end 801 | 802 | 803 | %hook SPTNowPlayingShuffleButtonViewController 804 | 805 | 806 | - (void)setShuffleButton:(id)arg1 { // Hide Shuffle Button 807 | 808 | if(!hideShuffleButton) return %orig; 809 | 810 | } 811 | 812 | 813 | %end 814 | 815 | 816 | %hook PlayButton 817 | 818 | 819 | - (void)setAlpha:(double)arg1 { // Hide Play/Pause Button 820 | 821 | if(hidePlayPauseButton) %orig(0); 822 | else %orig; 823 | 824 | } 825 | 826 | 827 | %end 828 | 829 | 830 | %hook SPTNowPlayingRepeatButtonViewController 831 | 832 | 833 | - (void)setRepeatButton:(id)arg1 { // Hide Repeat Button 834 | 835 | if(!hideRepeatButton) return %orig; 836 | 837 | } 838 | 839 | 840 | %end 841 | 842 | 843 | %hook SPTNowPlayingConnectButtonViewController 844 | 845 | 846 | - (id)initWithAuxiliaryActionsHandler:(id)arg1 connectIntegration:(id)arg2 theme:(id)arg3 { // Hide Devices Button 847 | 848 | if(!hideDevicesButton) return %orig; 849 | return nil; 850 | 851 | } 852 | 853 | 854 | %end 855 | 856 | 857 | %hook SPTNowPlayingBanButtonViewController 858 | 859 | 860 | - (id)initWithModel:(id)arg1 861 | auxiliaryActionsHandler:(id)arg2 862 | testManager:(id)arg3 863 | theme:(id)arg4 { // Hide Feedback Button 864 | 865 | if(!hideFeedbackButton) return %orig; 866 | return nil; 867 | 868 | } 869 | 870 | 871 | %end 872 | 873 | 874 | %hook SPTNowPlayingShareButtonViewController 875 | 876 | 877 | - (id)initWithAuxiliaryActionsHandler:(id)arg1 shareButtonFactory:(id)arg2 { // Hide Share Button 878 | 879 | if(!hideShareButton) return %orig; 880 | return nil; 881 | 882 | } 883 | 884 | 885 | %end 886 | 887 | 888 | %hook SPTNowPlayingQueueButtonViewController 889 | 890 | 891 | - (id)initWithAuxiliaryActionsHandler:(id)arg1 queueButtonFactory:(id)arg2 { // Hide Queue Button 892 | 893 | if(!hideQueueButton) return %orig; 894 | return nil; 895 | 896 | } 897 | 898 | 899 | %end 900 | 901 | 902 | %hook SPTNowPlayingSkipPreviousButtonViewController 903 | 904 | 905 | - (void)setPreviousButton:(id)arg1 { 906 | 907 | if(!hideBackButton && !hidePreviousTrackButton) return %orig; 908 | 909 | } 910 | 911 | 912 | %end 913 | 914 | 915 | %hook SPTNowPlayingSkipNextButtonViewController 916 | 917 | 918 | - (void)setNextButton:(id)arg1 { 919 | 920 | if(!hideForwardButton && !hideNextTrackButton) return %orig; 921 | 922 | } 923 | 924 | 925 | %end 926 | 927 | 928 | %hook SPTNowPlayingSleepTimerButtonViewController 929 | 930 | 931 | - (void)setSleepTimerButton:(id)arg1 { 932 | 933 | if(!hideMoonButton) return %orig; 934 | 935 | } 936 | 937 | 938 | %end 939 | 940 | 941 | /******************/ 942 | // ! | ++ Features | 943 | /******************/ 944 | 945 | %hook SPTNowPlayingHeadUnitView 946 | 947 | 948 | %property (nonatomic, strong) UIButton *rewindButton; 949 | %property (nonatomic, strong) UIButton *skipButton; 950 | %property (nonatomic, strong) UIButton *playPauseButton; 951 | %property (nonatomic, strong) UIStackView *buttonsStackView; 952 | 953 | 954 | - (id)initWithFrame:(CGRect)frame { // get an instance of SPTNowPlayingHeadUnitView 955 | 956 | id orig = %orig; 957 | 958 | headUnitView = self; 959 | if(enableSpotifyUI) [self setupSpotifyUI]; 960 | 961 | return orig; 962 | 963 | } 964 | 965 | 966 | %new 967 | 968 | - (void)setupSpotifyUI { 969 | 970 | loadPrefs(); 971 | 972 | UIImage *rewindButtonImage = [[UIImage systemImageNamed:@"backward.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 973 | UIImage *skipButtonImage = [[UIImage systemImageNamed:@"forward.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 974 | 975 | self.buttonsStackView = [UIStackView new]; 976 | self.buttonsStackView.axis = UILayoutConstraintAxisHorizontal; 977 | self.buttonsStackView.spacing = 10; 978 | self.buttonsStackView.alignment = UIStackViewAlignmentCenter; 979 | self.buttonsStackView.distribution = UIStackViewDistributionFill; 980 | self.buttonsStackView.translatesAutoresizingMaskIntoConstraints = NO; 981 | [self addSubview:self.buttonsStackView]; 982 | 983 | self.rewindButton = [self createButtonWithImage:rewindButtonImage forSelector:@selector(didTapRewindButton)]; 984 | self.playPauseButton = [self createButtonWithImage:nil forSelector:@selector(didTapPlayPauseButton)]; 985 | self.skipButton = [self createButtonWithImage:skipButtonImage forSelector:@selector(didTapSkipButton)]; 986 | 987 | [self.playPauseButton setPreferredSymbolConfiguration:[UIImageSymbolConfiguration configurationWithPointSize:35] forImageInState:UIControlStateNormal]; 988 | [self setupSpotifyUIConstraints]; 989 | 990 | } 991 | 992 | 993 | %new 994 | 995 | - (void)setupSpotifyUIConstraints { 996 | 997 | [self.buttonsStackView.centerXAnchor constraintEqualToAnchor: self.centerXAnchor].active = YES; 998 | [self.buttonsStackView.centerYAnchor constraintEqualToAnchor: self.centerYAnchor].active = YES; 999 | [self.buttonsStackView.heightAnchor constraintEqualToConstant: 100].active = YES; 1000 | [self setupSizeConstraintsForButton: self.rewindButton]; 1001 | [self setupSizeConstraintsForButton: self.skipButton]; 1002 | 1003 | } 1004 | 1005 | 1006 | %new 1007 | 1008 | - (void)didTapRewindButton { 1009 | 1010 | [self sendMRCommandAndTriggerHapticsIfRequested: kMRPreviousTrack]; 1011 | 1012 | } 1013 | 1014 | 1015 | %new 1016 | 1017 | - (void)didTapPlayPauseButton { 1018 | 1019 | [self sendMRCommandAndTriggerHapticsIfRequested: kMRTogglePlayPause]; 1020 | 1021 | } 1022 | 1023 | 1024 | %new 1025 | 1026 | - (void)didTapSkipButton { 1027 | 1028 | [self sendMRCommandAndTriggerHapticsIfRequested: kMRNextTrack]; 1029 | 1030 | } 1031 | 1032 | 1033 | %new 1034 | 1035 | - (void)sendMRCommandAndTriggerHapticsIfRequested:(MRCommand)command { 1036 | 1037 | MRMediaRemoteSendCommand(command, nil); 1038 | if(!enableHaptics) return; 1039 | 1040 | switch(hapticsStrength) { 1041 | case 0: AudioServicesPlaySystemSound(1519); break; 1042 | case 1: AudioServicesPlaySystemSound(1520); break; 1043 | case 2: AudioServicesPlaySystemSound(1521); break; 1044 | } 1045 | 1046 | } 1047 | 1048 | 1049 | %new 1050 | 1051 | - (UIButton *)createButtonWithImage:(UIImage *_Nullable)image forSelector:(SEL)selector { 1052 | 1053 | UIButton *button = [UIButton new]; 1054 | if(!enableArtworkBasedColors) button.tintColor = UIColor.whiteColor; 1055 | button.adjustsImageWhenHighlighted = NO; 1056 | button.translatesAutoresizingMaskIntoConstraints = NO; 1057 | [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; 1058 | [button setImage:image forState:UIControlStateNormal]; 1059 | [self.buttonsStackView addArrangedSubview: button]; 1060 | return button; 1061 | 1062 | } 1063 | 1064 | 1065 | %new 1066 | 1067 | - (void)setupSizeConstraintsForButton:(UIButton *)button { 1068 | 1069 | [button.widthAnchor constraintEqualToConstant: 100].active = YES; 1070 | [button.heightAnchor constraintEqualToConstant: 90].active = YES; 1071 | 1072 | } 1073 | 1074 | 1075 | %end 1076 | 1077 | 1078 | %hook SPTPlayerState 1079 | 1080 | 1081 | - (BOOL)isPaused { 1082 | 1083 | if(!enableSpotifyUI) return %orig; 1084 | 1085 | UIImage *playButtonImage = [[UIImage systemImageNamed:@"play.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 1086 | UIImage *pauseButtonImage = [[UIImage systemImageNamed:@"pause.fill"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 1087 | 1088 | BOOL value = %orig; 1089 | 1090 | dispatch_async(dispatch_get_main_queue(), ^{ 1091 | 1092 | [UIView transitionWithView:headUnitView.playPauseButton duration:0.5 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ 1093 | 1094 | if(value) [headUnitView.playPauseButton setImage:playButtonImage forState:UIControlStateNormal]; 1095 | else [headUnitView.playPauseButton setImage:pauseButtonImage forState:UIControlStateNormal]; 1096 | 1097 | } completion:nil]; 1098 | 1099 | }); 1100 | 1101 | return value; 1102 | 1103 | } 1104 | 1105 | 1106 | %end 1107 | 1108 | 1109 | %hook SPTNowPlayingViewController 1110 | 1111 | 1112 | - (void)viewDidLoad { // add gesture to save canvas 1113 | 1114 | %orig; 1115 | if(!saveCanvas) return; 1116 | 1117 | canvasContentLayerVC = self; 1118 | 1119 | UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTapCanvas)]; 1120 | doubleTap.numberOfTapsRequired = 2; 1121 | [self.view addGestureRecognizer:doubleTap]; 1122 | 1123 | } 1124 | 1125 | 1126 | static void setupAlertControllerWithMessageAndURLString(NSString *messageString, NSString *urlString) { 1127 | 1128 | UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"PerfectSpotify" message:messageString preferredStyle:UIAlertControllerStyleAlert]; 1129 | UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"Yes" style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { 1130 | 1131 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.005 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 1132 | [UIApplication.sharedApplication _openURL: [NSURL URLWithString: urlString]]; 1133 | }); 1134 | 1135 | }]; 1136 | 1137 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Not now" style:UIAlertActionStyleCancel handler:nil]; 1138 | 1139 | [alertController addAction:confirmAction]; 1140 | [alertController addAction:cancelAction]; 1141 | 1142 | [canvasContentLayerVC presentViewController:alertController animated:YES completion:nil]; 1143 | 1144 | } 1145 | 1146 | static void saveToFilzaAlertController() { 1147 | 1148 | NSFileManager *fileM = [NSFileManager defaultManager]; 1149 | NSString *documentsPath = [[fileM URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject].URLByDeletingLastPathComponent.path; 1150 | 1151 | NSString *pathInFilza = [@"filza://view" stringByAppendingString: documentsPath]; 1152 | NSString *completePath = [[pathInFilza stringByAppendingString:@"/Documents/Canvas/"] stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; 1153 | 1154 | setupAlertControllerWithMessageAndURLString( 1155 | @"Canvas downloaded succesfully. Do you want to view it in Filza?", 1156 | completePath 1157 | ); 1158 | 1159 | } 1160 | 1161 | static void saveToGalleryAlertController() { 1162 | 1163 | setupAlertControllerWithMessageAndURLString( 1164 | @"Canvas downloaded succesfully. Do you want to open gallery?", 1165 | @"photos-redirect://" 1166 | ); 1167 | 1168 | } 1169 | 1170 | static void getCanvas() { 1171 | 1172 | NSFileManager *fileM = [NSFileManager defaultManager]; 1173 | 1174 | NSString *documentsPath = [[fileM URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject].URLByDeletingLastPathComponent.path; 1175 | NSString *completePath = [documentsPath stringByAppendingPathComponent:@"/Library/Caches/Canvases/"]; 1176 | NSString *newPath = [documentsPath stringByAppendingPathComponent:@"/Documents/Canvas/"]; 1177 | NSString *canvasDirectory = [documentsPath stringByAppendingPathComponent:@"/Documents/Canvas/"]; 1178 | 1179 | NSError *error; 1180 | NSError *dirError; 1181 | 1182 | NSDirectoryEnumerator *directoryEnumerator = [fileM enumeratorAtPath:completePath]; 1183 | NSDate *lastDate = [NSDate dateWithTimeIntervalSinceNow:-300]; // check for the canvas cached within the last 5 minutes 1184 | 1185 | for(NSString *path in directoryEnumerator) { 1186 | 1187 | NSDictionary *attributes = [directoryEnumerator fileAttributes]; 1188 | NSDate *lastModificationDate = [attributes objectForKey:NSFileModificationDate]; 1189 | 1190 | if([lastDate earlierDate:lastModificationDate] == lastDate) { 1191 | 1192 | // NSLog(@"PSS:%@ was modified within the last 5 minutes", path); 1193 | 1194 | switch(saveCanvasDestination) { 1195 | 1196 | case 0: 1197 | 1198 | BOOL isDir; 1199 | 1200 | if(![fileM fileExistsAtPath:canvasDirectory isDirectory:&isDir]) 1201 | [fileM createDirectoryAtPath:canvasDirectory withIntermediateDirectories:NO attributes:nil error:&dirError]; 1202 | 1203 | [fileM copyItemAtPath:[completePath stringByAppendingPathComponent:path] toPath:[newPath stringByAppendingPathComponent:path] error:&error]; 1204 | 1205 | if(error) { 1206 | SPTPopupDialog *notSoProudPopup = [%c(SPTPopupDialog) popupWithTitle:@"PerfectSpotify" message:@"Oops, looks like there was an errow downloading this canvas, most likely because you already downloaded it. Otherwise try retrying." dismissButtonTitle:@"Got it"]; 1207 | [[%c(SPTPopupManager) sharedManager].presentationQueue addObject:notSoProudPopup]; 1208 | [[%c(SPTPopupManager) sharedManager] presentNextQueuedPopup]; 1209 | // NSLog(@"PS:Your dumb af code caused this error %@", error); 1210 | return; 1211 | } 1212 | 1213 | saveToFilzaAlertController(); 1214 | break; 1215 | 1216 | case 1: 1217 | 1218 | UISaveVideoAtPathToSavedPhotosAlbum([completePath stringByAppendingPathComponent:path], canvasContentLayerVC, nil, nil); 1219 | saveToGalleryAlertController(); 1220 | break; 1221 | 1222 | } 1223 | 1224 | } 1225 | 1226 | } 1227 | 1228 | } 1229 | 1230 | 1231 | %new 1232 | 1233 | - (void)didDoubleTapCanvas { 1234 | 1235 | getCanvas(); 1236 | 1237 | } 1238 | 1239 | 1240 | %end 1241 | %end 1242 | 1243 | 1244 | /******************/ 1245 | // ! | SpringBoard | 1246 | /******************/ 1247 | 1248 | %group PSpotifySpringBoard 1249 | 1250 | 1251 | static void launchPSpotify() { 1252 | 1253 | NSString *urlString = kOrionExists || kShuffleExists ? @"prefs:root=Tweaks&path=PerfectSpotify" : @"prefs:root=PerfectSpotify"; 1254 | [UIApplication.sharedApplication _openURL: [NSURL URLWithString: urlString]]; 1255 | 1256 | } 1257 | 1258 | 1259 | %hook SBIconView 1260 | 1261 | 1262 | - (void)setApplicationShortcutItems:(NSArray *)items { 1263 | 1264 | loadPrefs(); 1265 | 1266 | if(![self.icon.applicationBundleID isEqualToString:@"com.spotify.client"]) return %orig; 1267 | 1268 | NSString *editHSShortcutString = @"com.apple.springboardhome.application-shortcut-item.rearrange-icons"; 1269 | NSString *shareAppShortcutString = @"com.apple.springboardhome.application-shortcut-item.share"; 1270 | NSString *removeAppShortcutString = @"com.apple.springboardhome.application-shortcut-item.remove-app"; 1271 | 1272 | NSString *sptSearchShortcutString = @"com.spotify.shortcutItem.search"; 1273 | NSString *sptRecentlyPlayedShortcutString = @"com.spotify.shortcutItem.recentlyplayed"; 1274 | 1275 | NSMutableArray *shortcutsArray = [items mutableCopy]; 1276 | 1277 | for(SBSApplicationShortcutItem *shortcutItem in items) { 1278 | 1279 | if(!self.icon.isApplicationIcon) return; 1280 | 1281 | if([shortcutItem.type isEqualToString:editHSShortcutString]) { 1282 | 1283 | if(removeEditHSShortcut) [shortcutsArray removeObject:shortcutItem]; 1284 | 1285 | } 1286 | 1287 | else if([shortcutItem.type isEqualToString:shareAppShortcutString]) { 1288 | 1289 | if(removeShareAppShortcut) [shortcutsArray removeObject:shortcutItem]; 1290 | 1291 | } 1292 | 1293 | else if([shortcutItem.type isEqualToString:removeAppShortcutString]) { 1294 | 1295 | if(removeRemoveAppShortcut) [shortcutsArray removeObject:shortcutItem]; 1296 | 1297 | } 1298 | 1299 | else if([shortcutItem.type isEqualToString:sptSearchShortcutString]) { 1300 | 1301 | if(removeSpotifySearchShortcut) [shortcutsArray removeObject:shortcutItem]; 1302 | 1303 | } 1304 | 1305 | else if([shortcutItem.type isEqualToString:sptRecentlyPlayedShortcutString]) { 1306 | 1307 | if(removeSpotifyRecentlyPlayedShortcut) [shortcutsArray removeObject:shortcutItem]; 1308 | 1309 | } 1310 | 1311 | } 1312 | 1313 | if(addPSpotifyShortcut) { 1314 | 1315 | UIImage *image = [UIImage systemImageNamed:@"music.quarternote.3"]; 1316 | 1317 | SBSApplicationShortcutItem *PSpotify = [%c(SBSApplicationShortcutItem) new]; 1318 | PSpotify.icon = [[%c(SBSApplicationShortcutCustomImageIcon) alloc] initWithImageData:UIImagePNGRepresentation(image) dataType:0 isTemplate:1]; 1319 | PSpotify.type = @"me.luki.pspotify.item"; 1320 | PSpotify.localizedTitle = [NSString stringWithFormat:@"PSpotify"]; 1321 | 1322 | [shortcutsArray addObject:PSpotify]; 1323 | 1324 | } 1325 | 1326 | %orig(shortcutsArray); 1327 | 1328 | } 1329 | 1330 | 1331 | + (void)activateShortcut:(SBSApplicationShortcutItem *)item 1332 | withBundleIdentifier:(NSString *)bundleID 1333 | forIconView:(id)iconView { 1334 | 1335 | if(![item.type isEqualToString:@"me.luki.pspotify.item"]) return %orig; 1336 | 1337 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.001 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 1338 | launchPSpotify(); 1339 | }); 1340 | 1341 | } 1342 | 1343 | 1344 | - (void)didMoveToSuperview { 1345 | 1346 | %orig; 1347 | 1348 | [NSNotificationCenter.defaultCenter removeObserver:self]; 1349 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(setApplicationShortcutItems:) name:@"updateShortcutItems" object:nil]; 1350 | 1351 | } 1352 | 1353 | 1354 | %end 1355 | %end 1356 | 1357 | 1358 | %ctor { 1359 | 1360 | loadPrefs(); 1361 | %init(PSpotifySpringBoard); 1362 | 1363 | if(kIsCurrentApp(@"com.apple.springboard")) return; 1364 | 1365 | %init(PerfectSpotify, 1366 | ConnectButton=kClass(@"ConnectUIFeatureImpl.ConnectButtonView"), 1367 | ClearRecentSearchesButton=kClass(@"SPTTing.ChipView"), 1368 | PlayWhatYouLoveText=kClass(@"SPTTing.EmptyState"), 1369 | PlayButton=kClass(@"EncoreConsumerMobile.PlayButtonView"), 1370 | PlaylistsController=kClass(@"PlaylistUXPlatform_PlaylistMigrationImpl.PLItemsViewModelImplementation"), 1371 | ShortcutPlaylistButton=kClass(@"EncoreMobile.InteractableLayoutBackingButton") 1372 | ); 1373 | 1374 | } 1375 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PerfectSpotify 2 | 3 | 4 | 5 | ## Miscellaneous 6 | 7 | - OLED Spotify 8 | - Hide Tab Bar Labels 9 | - Hide Connect Button 10 | 11 | - Hide Cancel Button 12 | - Hide Play What You Love Text 13 | - Hide Clear Recent Searches Button 14 | 15 | - Show Songs Count 16 | - Hide Enhance Button 17 | - Hide Add Songs Button 18 | - No Pop-Up When Queuing 19 | - No Pop-Up When Liking Songs 20 | 21 | - True Shuffle 22 | - Show Status Bar 23 | - Disable Storylines 24 | - Lyrics For All Tracks 25 | - Disable Genius Lyrics 26 | - Spoof Playing Remotely Status 27 | 28 | ## Now Playing UI 29 | 30 | - Klei Colors (Gradients) 31 | - Now Playing UI Background Color 32 | 33 | - Hide Close Button 34 | - Hide Playlist Name 35 | - Hide Context Button 36 | - Hide Like Button 37 | - Hide Slider Knob 38 | - Hide Time Slider 39 | - Hide Elapsed Time 40 | - Hide Remaining Time 41 | - Hide Shuffle Button 42 | - Hide Previous Track Button 43 | - Hide Play/Pause Button 44 | - Hide Next Track Button 45 | - Hide Repeat Button 46 | - Hide Devices Button 47 | - Hide Feedback Button 48 | - Hide Share Button 49 | - Hide Queue Button 50 | 51 | - Hide Speed Button 52 | - Hide Back Button 53 | - Hide Forward Button 54 | - Hide Moon Button 55 | 56 | - Center Artist/Song Title 57 | - Align Text To The Top 58 | 59 | ## Experimental Features 60 | 61 | - Enable Spotify UI 62 | - Enable Haptics 63 | - Enable Artwork Based Colors 64 | 65 | - Save Canvas 66 | 67 | ## Socials 68 | 69 | * [Twitter](https://twitter.com/Lukii120) 70 | 71 | ## LICENSE 72 | 73 | * [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/2.0/) 74 | 75 | ## Assets LICENSE 76 | 77 | * Under no means shall the visual assets of this repository – i.e., all photo-, picto-, icono-, and videographic material – (if any) be altered and/or redistributed for any independent commercial or non-commercial intent beyond its original function in this project. Permissible usage of such content is restricted solely to its express application in this repository and any forks that retain the material in its original, unaltered form only. 78 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: me.luki.perfectspotify 2 | Name: PerfectSpotify 3 | Version: 2.3~EOL 4 | Architecture: iphoneos-arm 5 | Description: Spotify, but "perfect" 6 | Maintainer: Luki 7 | Author: Luki 8 | Section: Tweaks 9 | Depends: mobilesubstrate, preferenceloader, com.mrgcgamer.libgcuniversal, love.litten.libkitten 10 | --------------------------------------------------------------------------------