├── .gitattributes
├── .gitignore
├── 0-Shortcuts
├── Credits.txt
└── Save MobileGestalt.shortcut
├── LICENSE
├── README.md
├── compile.py
├── controllers
├── __init__.py
├── aar
│ ├── LICENSE
│ ├── __init__.py
│ └── aar.py
├── files_handler.py
├── path_handler.py
├── plist_handler.py
├── video_handler.py
├── web_request_handler.py
└── xml_handler.py
├── credits
├── LeminLimez.png
├── big_nugget.png
└── small_nugget.png
├── devicemanagement
├── __init__.py
├── constants.py
├── data_singleton.py
├── device_manager.py
└── generate_mga.py
├── documentation.md
├── entitlements.plist
├── exceptions
├── __init__.py
├── nugget_exception.py
└── posterboard_exceptions.py
├── files
├── SSLconf
│ └── TrustStore.sqlite3
├── eligibility
│ ├── Config.plist
│ └── eligibility.plist
└── posterboard
│ ├── 1F20C883-EA98-4CCE-9923-0C9A01359721
│ ├── com.apple.posterkit.provider.descriptor.identifier
│ ├── providerInfo.plist
│ └── versions
│ │ └── 0
│ │ └── contents
│ │ ├── .com.apple.posterkit.provider.contents.configurableOptions.plist
│ │ ├── 0EFB6A0F-7052-4D24-8859-AB22BADF2E93
│ │ ├── input.segmentation
│ │ │ └── asset.resource
│ │ │ │ └── contents.plist
│ │ ├── output.layerStack
│ │ │ └── Contents.json
│ │ └── style.plist
│ │ ├── DescriptorModel.plist
│ │ ├── com.apple.posterkit.provider.contents.galleryOptions
│ │ └── com.apple.posterkit.provider.contents.userInfo
│ ├── VideoCAML
│ ├── com.apple.posterkit.provider.descriptor.identifier
│ ├── com.apple.posterkit.role.identifier
│ ├── providerInfo.plist
│ └── versions
│ │ └── 1
│ │ └── contents
│ │ ├── 9183.Custom-810w-1080h@2x~ipad.wallpaper
│ │ ├── 9183.Custom_Floating-810w-1080h@2x~ipad.ca
│ │ │ ├── index.xml
│ │ │ └── main.caml
│ │ └── Wallpaper.plist
│ │ └── com.apple.posterkit.provider.contents.userInfo
│ └── contents.plist
├── gui
├── __init__.py
├── apply_worker.py
├── custom_qt_elements
│ ├── multicombobox.py
│ └── resizable_image_label.py
├── dialogs.py
├── main_window.py
├── pages
│ ├── __init__.py
│ ├── main
│ │ ├── home.py
│ │ └── settings.py
│ ├── page.py
│ └── tools
│ │ ├── daemons.py
│ │ ├── eligibility.py
│ │ ├── featureflags.py
│ │ ├── gestalt.py
│ │ ├── internal.py
│ │ ├── posterboard.py
│ │ ├── risky.py
│ │ └── springboard.py
├── pb_tutorial1.png
├── pb_tutorial2.png
└── transparent.png
├── icon
├── app-indicator.svg
├── arrow-clockwise.svg
├── arrow.clockwise.svg
├── brush.svg
├── caret-down-fill.svg
├── check-circle.svg
├── chevron.down.svg
├── chevron.up.svg
├── compass.svg
├── currency-dollar.svg
├── discord.svg
├── export.svg
├── file-earmark-zip.svg
├── flag.svg
├── folder.svg
├── gear.svg
├── geo-alt.svg
├── github.svg
├── globe.svg
├── hdd.svg
├── heart-fill.svg
├── house.svg
├── import.svg
├── iphone-island.svg
├── pencil.svg
├── phone.svg
├── photo-stack.svg
├── photo.svg
├── plus.svg
├── questionmark.circle.svg
├── shippingbox.svg
├── star.svg
├── toggles.svg
├── trash.svg
├── twitter.svg
├── wallpaper.svg
├── wifi.svg
└── x-lg.svg
├── main_app.py
├── mainwindow_ui.py
├── nugget.ico
├── qt
├── mainwindow.ui
├── mainwindow_ui.py
└── ui_mainwindow.py
├── requirements.txt
├── resources.qrc
├── resources_rc.py
├── restore
├── __init__.py
├── backup.py
├── mbdb.py
└── restore.py
├── tweaks
├── __init__.py
├── basic_plist_locations.py
├── custom_gestalt_tweaks.py
├── daemons_tweak.py
├── eligibility_tweak.py
├── posterboard
│ ├── posterboard_tweak.py
│ ├── template_file.py
│ ├── template_options
│ │ ├── __init__.py
│ │ ├── picker_option.py
│ │ ├── remove_option.py
│ │ ├── replace_option.py
│ │ ├── set_option.py
│ │ └── template_options.py
│ └── tendie_file.py
├── tweak_classes.py
└── tweaks.py
└── version.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | build
3 | com.apple.MobileGestalt.plist
4 | .DS_Store
5 | __pycache__
6 | *.spec
7 | /dist
8 |
--------------------------------------------------------------------------------
/0-Shortcuts/Credits.txt:
--------------------------------------------------------------------------------
1 | [LeminLimez](https://github.com/leminlimez) for [Nugget](https://github.com/leminlimez/Nugget)
--------------------------------------------------------------------------------
/0-Shortcuts/Save MobileGestalt.shortcut:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/0-Shortcuts/Save MobileGestalt.shortcut
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ![Artboard][NuggetLogo]
2 |
3 | # Nugget
4 | Unlock your device's full potential!
5 |
6 | Customize your device with animated wallpapers, disable pesky daemons, and more!
7 |
8 | Make sure you have installed the [requirements](#requirements) if you are on Windows or Linux.
9 |
10 | > [!NOTE]
11 | > Please back up your data before using! Nugget may cause unforseen problems, so it is better to be same than sorry. We are not responsible for any damange done to your device.
12 |
13 | ## Features
14 |
15 | iOS 17.0+
16 |
17 | - PosterBoard: Animated wallpapers and descriptors.
18 | - Community wallpapers can be found [here][WallpapersWebsite]
19 | - Converting videos to wallpapers
20 | - Customizing community-made wallpapers via batter files
21 | - See documentation on the structure of tendies and batter files in [documentation.md](documentation.md)
22 | - Springboard Options (from [Cowabunga Lite][CowabungaLite])
23 | - Set Lock Screen Footnote
24 | - Disable Lock After Respring
25 | - Disable Screen Dimming While Charging
26 | - Disable Low Battery Alerts
27 | - Internal Options (from [Cowabunga Lite][CowabungaLite])
28 | - Build Version in Status Bar
29 | - Force Right to Left
30 | - Force Metal HUD Debug
31 | - iMessage Diagnostics
32 | - IDS Diagnostics
33 | - VC Diagnostics
34 | - App Store Debug Gesture
35 | - Notes App Debug Mode
36 | - Disable Daemons:
37 | - OTAd
38 | - UsageTrackingAgent
39 | - Game Center
40 | - Screen Time Agent
41 | - Logs, Dumps, and Crash Reports
42 | - ATWAKEUP
43 | - Tipsd
44 | - VPN
45 | - Chinese WLAN service
46 | - HealthKit
47 | - AirPrint
48 | - Assistive Touch
49 | - iCloud
50 | - Internet Tethering (aka Personal Hotspot)
51 | - PassBook
52 | - Spotlight
53 | - Voice Control
54 | - Risky (Hidden) Options:
55 | - Disable thermalmonitord
56 | - OTA Killer
57 | - Custom Resolution
58 |
59 |
60 | iOS 17.0 - 18.1.1
61 |
62 | - Enable Dynamic Island on any device
63 | - Enable iPhone X gestures on iPhone SEs
64 | - Change Device Model Name (ie what shows in the Settings app)
65 | - Enable Boot Chime
66 | - Enable Charge Limit
67 | - Enable Tap to Wake on unsupported devices (ie iPhone SEs)
68 | - Enable Collision SOS
69 | - Enable Stage Manager
70 | - Disable the Wallpaper Parallax
71 | - Disable Region Restrictions (ie. Shutter Sound)
72 | - Note: This does not include enabling EU sideloading outside the EU. That will come later.
73 | - Show the Apple Pencil options in Settings app
74 | - Show the Action Button options in Settings app
75 | - Show Internal Storage info (Might cause problems on some devices, use at your own risk)
76 | - EU Enabler (iOS 17.6-)
77 |
78 |
79 | iOS 18.0 - iOS 18.0.1
80 |
81 | - Feature Flags (iOS 18.1b4-):
82 | - Enabling lock screen clock animation, lock screen page duplication button, and more!
83 | - Disabling the new iOS 18 Photos UI (iOS 18.0 betas only, unknown which patched it)
84 |
85 |
86 | iOS 18.0 - iOS 18.1.1
87 |
88 | - Enable iPhone 16 camera button page in the Settings app
89 | - Enable AOD & AOD Vibrancy on any device
90 |
91 |
92 | iOS 18.1 - 18.1.1
93 |
94 | - AI Enabler
95 | - Device Spoofing
96 |
97 |
98 | ## Requirements:
99 |
100 | Windows
101 |
102 | - Either [Apple Devices (from Microsoft Store)][AppleDevices] App or [iTunes (from Apple website)][iTunes]
103 |
104 |
105 |
106 | Linux
107 |
108 | - [usbmuxd][usbmuxdGitHub]
109 | - [libimobiledevice][libimobiledeviceGitHub]
110 |
111 |
112 |
113 | For Running Python
114 |
115 | - [pymobiledevice3][pymobiledevice3GitHub]
116 | - [PySide6][PySide6Doc]
117 | - Python 3.8 or newer
118 |
119 |
120 | ## Running the Python Program
121 | > [!NOTE]
122 | > It is highly recommended to use a virtual environment:
123 | > ```py
124 | > python3 -m venv .env # only needed once
125 | > ```
126 | macOS/Linux:
127 | ```py
128 | source .env/bin/activate
129 | ```
130 | Windows:
131 | ```py
132 | .env/Scripts/activate.bat
133 | ```
134 | Install Packages:
135 | ```py
136 | pip3 install -r requirements.txt # only needed once
137 | python3 main_app.py
138 | ```
139 | > [!NOTE]
140 | > It may be either `python`/`pip` or `python3`/`pip3` depending on your path.
141 |
142 | The CLI version can be ran with:
143 | ```py
144 | python3 cli_app.py
145 | ```
146 |
147 | ## Getting the File
148 | On iOS 18.1.1 and below, you may need to get the mobilegestalt file that is specific to your device. To do that, follow these steps:
149 | 1. Install the [Shortcuts][ShortcutsApp] app from the iOS app store.
150 | 2. Download this shortcut: [Save MobileGestalt][MobilegestaltShortcut]
151 | 3. Save the file and share it to your computer.
152 | 4. Place it in the same folder as the python file (or specify the path in the program)
153 |
154 | ## Building
155 | To compile `mainwindow.ui` for Python, run the following command:
156 | ```py
157 | pyside6-uic qt/mainwindow.ui -o qt/ui_mainwindow.py
158 | ```
159 |
160 | To compile the resources file for Python, run the following command:
161 | ```py
162 | pyside6-rcc resources.qrc -o resources_rc.py
163 | ```
164 |
165 | The application itself can be compiled by running `compile.py`.
166 |
167 | ## Sparserestore Info
168 | This uses the sparserestore exploit to write to files outside of the intended restore location, like mobilegestalt. Read the [Getting the File](#getting-the-file) section to learn how to get your mobilegestalt file.
169 |
170 | Sparserestore works on all versions iOS 17.0-18.1.1. There is partial support for iOS 18.2 developer beta 3 and newer not using any exploits.
171 |
172 | > [!NOTE]
173 | > **Mobilegestalt and AI Enabler tweaks are not supported on iOS 18.2+.** It will never be supported, do not make issues asking for when it is supported.
174 |
175 | ## Read More
176 | If you would like to read more about the inner workings of the exploit and iOS restore system, I made a write up which you can read [here][ReadMoreGist].
177 | For clarity, up to iOS 18.2 developer beta 2 (public beta 1) is fully supported by Nugget. I said iOS 18.1.1 because mentioning the betas confused people.
178 |
179 |
180 |
181 | ## Credits
182 | - [LeminLimez](https://github.com/leminlimez) for [Nugget](https://github.com/leminlimez/Nugget)
183 | - [JJTech][JJTechGitHub] for Sparserestore/[TrollRestore][TrollStoreGitHub]
184 | - [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard
185 | - Special thanks to [dootskyre][dootskyreX], [Middo][MiddoX], [dulark][dularkGitHub], forcequitOS, and pingubow for their work on this. It would not have been possible without them!
186 | - Thanks to [Snoolie for aar handling][python-aar-stuffGitHub]!
187 | - Thanks to [SerStars][SerStarsX] for creating [the website][WallpapersWebsite]!
188 | - [disfordottie][disfordottieX] for some global flag features
189 | - [Mikasa-san][Mikasa-sanGitHub] for [Quiet Daemon][QuietDaemonGitHub]
190 | - [sneakyf1shy][sneakyf1shyGitHub] for [AI Eligibility][AIEligibilityGist] (iOS 18.1 beta 4 and below)
191 | - [lrdsnow][lrdsnowGitHub] for [EU Enabler][EUEnablerGitHub]
192 | - [pymobiledevice3][pymobiledevice3GitHub] for restoring and device algorithms.
193 | - [PySide6][PySide6Doc] for the GUI library.
194 |
195 | [NuggetLogo]: https://raw.githubusercontent.com/leminlimez/Nugget/refs/heads/main/credits/small_nugget.png
196 | [CowabungaLite]: https://github.com/leminlimez/CowabungaLite
197 | [WallpapersWebsite]: https://cowabun.ga/wallpapers
198 | [AppleDevices]: https://apps.microsoft.com/detail/9np83lwlpz9k
199 | [iTunes]: https://support.apple.com/en-us/106372
200 | [usbmuxdGitHub]: https://github.com/libimobiledevice/usbmuxd
201 | [libimobiledeviceGitHub]: https://github.com/libimobiledevice/libimobiledevice
202 | [ShortcutsApp]: https://apps.apple.com/us/app/shortcuts/id915249334
203 | [MobilegestaltShortcut]: https://www.icloud.com/shortcuts/d6f0a136ddda4714a80750512911c53b
204 | [ReadMoreGist]: https://gist.github.com/leminlimez/c602c067349140fe979410ef69d39c28
205 |
206 | [JJTechGitHub]: https://github.com/JJTech0130
207 | [TrollStoreGitHub]: https://github.com/JJTech0130/TrollRestore
208 | [PosterRestoreDiscord]: https://discord.gg/gWtzTVhMvh
209 | [dootskyreX]: https://x.com/dootskyre
210 | [MiddoX]: https://x.com/MWRevamped
211 | [dularkGitHub]: https://github.com/dularkian
212 | [SerStarsX]: https://x.com/SerStars_lol
213 | [disfordottieX]: https://x.com/disfordottie
214 | [Mikasa-sanGitHub]: https://github.com/Mikasa-san
215 | [QuietDaemonGitHub]: https://github.com/Mikasa-san/QuietDaemon
216 | [sneakyf1shyGitHub]: https://github.com/f1shy-dev
217 | [lrdsnowGitHub]: https://github.com/Lrdsnow
218 | [EUEnablerGitHub]: https://github.com/Lrdsnow/EUEnabler
219 | [pymobiledevice3GitHub]: https://github.com/doronz88/pymobiledevice3
220 | [PySide6Doc]: https://doc.qt.io/qtforpython-6/
221 | [python-aar-stuffGitHub]: https://github.com/0xilis/python-aar-stuff
222 | [AIEligibilityGist]: https://gist.github.com/f1shy-dev/23b4a78dc283edd30ae2b2e6429129b5
223 |
--------------------------------------------------------------------------------
/compile.py:
--------------------------------------------------------------------------------
1 | from sys import platform
2 | import os
3 |
4 | import PyInstaller.__main__
5 |
6 | # configuration
7 | universal: bool = False # issues with cross compiling since cpython only gives a single arch
8 |
9 | args = [
10 | 'main_app.py',
11 | # '--hidden-import=ipsw_parser',
12 | '--hidden-import=zeroconf',
13 | '--hidden-import=pyimg4',
14 | '--hidden-import=zeroconf._utils.ipaddress',
15 | '--hidden-import=zeroconf._handlers.answers',
16 | '--add-data=files/:./files',
17 | '--copy-metadata=pyimg4',
18 | '--onedir',
19 | '--noconfirm',
20 | '--name=Nugget',
21 | '--icon=nugget.ico',
22 | '--optimize=2'
23 | ]
24 |
25 | if platform == "darwin":
26 | # add --windowed arg for macOS
27 | args.append('--windowed')
28 | args.append('--osx-bundle-identifier=com.leemin.Nugget')
29 | if universal:
30 | args.append('--target-arch=universal2')
31 | # codesigning resources
32 | try:
33 | import secrets.compile_config as compile_config
34 | args.append('--osx-entitlements-file=entitlements.plist')
35 | args.append(f"--codesign-identity={compile_config.CODESIGN_HASH}")
36 | except ImportError:
37 | print("No compile_config found, ignoring codesign...")
38 | elif os.name == 'nt':
39 | # add windows version info
40 | args.append('--version-file=version.txt')
41 |
42 | PyInstaller.__main__.run(args)
43 |
--------------------------------------------------------------------------------
/controllers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/controllers/__init__.py
--------------------------------------------------------------------------------
/controllers/aar/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 0xilis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/controllers/aar/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/controllers/aar/__init__.py
--------------------------------------------------------------------------------
/controllers/aar/aar.py:
--------------------------------------------------------------------------------
1 | '''
2 | Created by Snoolie K (0xilis) on 2025 March 18.
3 | Licensed under MIT. Please credit if using.
4 | '''
5 |
6 | import struct
7 |
8 | # make a .aar of one contexts.plist file
9 | def wrap_in_aar(input_file, input_mov, output_file):
10 | # Since I only need to care about a contents.plist (and in the future the mov file), I am hardcoding the header...
11 | header = bytearray.fromhex("4141303125005459503146504154500E00636F6E74656E74732E706C697374444154418E13")
12 |
13 | with open(input_file, 'rb') as f:
14 | file_data = f.read()
15 |
16 | file_size = len(file_data)
17 |
18 | if file_size <= 0xFFFF:
19 | # Our size can fit in a 2 byte AA_FIELD_TYPE_BLOB, yay
20 | header[-2:] = struct.pack(' FRAME_LIMIT:
59 | raise VideoLengthException(FRAME_LIMIT)
60 | try:
61 | # creating a folder named data
62 | if not os.path.exists(assets_path):
63 | os.makedirs(assets_path, exist_ok=True)
64 | # if not created then raise error
65 | except OSError:
66 | print ('Error: Creating directory of data')
67 |
68 | # frame
69 | currentframe = 0
70 | width = int(cam.get(cv2.CAP_PROP_FRAME_WIDTH))
71 | height = int(cam.get(cv2.CAP_PROP_FRAME_HEIGHT))
72 |
73 | with open(os.path.join(output_file, "main.caml"), "w") as caml:
74 | # write caml header
75 | fps = cam.get(cv2.CAP_PROP_FPS)
76 | duration = frame_count / fps
77 | caml.write(f"""
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | \n""")
89 | while(True):
90 | # reading from frame
91 | ret,frame = cam.read()
92 |
93 | if ret:
94 | # if video is still left continue creating images
95 | name = 'assets/' + str(currentframe) + '.jpg'
96 | if update_label:
97 | update_label('Creating...' + name)
98 | print('Creating...' + name)
99 |
100 | # writing the extracted images
101 | cv2.imwrite(os.path.join(output_file.removeprefix(u"\\\\?\\"), name), frame)
102 | caml.write(f"\t\t\t\n")
103 |
104 | # increasing counter so that it will
105 | # show how many frames are created
106 | currentframe += 1
107 | else:
108 | break
109 | caml.write("""
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 | """)
150 |
151 | # Release all space and windows once done
152 | cam.release()
153 | cv2.destroyAllWindows()
154 |
155 | # Write the other caml
156 | with open(os.path.join(output_file, "index.xml"), "w") as index:
157 | index.write(f"""
158 |
159 |
160 |
161 | assetManifest
162 | assetManifest.caml
163 | documentHeight
164 | {height}
165 | documentResizesToView
166 |
167 | documentWidth
168 | {width}
169 | dynamicGuidesEnabled
170 |
171 | geometryFlipped
172 |
173 | guidesEnabled
174 |
175 | interactiveMouseEventsEnabled
176 |
177 | interactiveShowsCursor
178 |
179 | interactiveTouchEventsEnabled
180 |
181 | loopEnd
182 | 0.0
183 | loopStart
184 | 0.0
185 | loopingEnabled
186 |
187 | multitouchDisablesMouse
188 |
189 | multitouchEnabled
190 |
191 | presentationMouseEventsEnabled
192 |
193 | presentationShowsCursor
194 |
195 | presentationTouchEventsEnabled
196 |
197 | rootDocument
198 | main.caml
199 | savesWindowFrame
200 |
201 | scalesToFitInPlayer
202 |
203 | showsTouches
204 |
205 | snappingEnabled
206 |
207 | timelineMarkers
208 | [(null)]
209 | touchesColor
210 | 1 1 0 0.8
211 | unitsInPixelsInPlayer
212 |
213 |
214 |
215 | """)
--------------------------------------------------------------------------------
/controllers/web_request_handler.py:
--------------------------------------------------------------------------------
1 | from requests import get, RequestException
2 | from json import JSONDecodeError
3 | from devicemanagement.constants import Version
4 |
5 | Nugget_Repo = "leminlimez/Nugget/releases/latest"
6 |
7 | last_fetched_version: str = None
8 |
9 | def is_update_available(version: str, build: int) -> bool:
10 | # check github for if version < tag (or == tag but build > 0)
11 | latest_version = get_latest_version()
12 | if latest_version != None:
13 | if build > 0 and latest_version == version: # on beta version when there is a public release
14 | return True
15 | elif Version(latest_version) > Version(version):
16 | return True
17 | return False
18 |
19 | def get_latest_version() -> str:
20 | global last_fetched_version
21 | # get the cached version
22 | if last_fetched_version != None:
23 | return last_fetched_version
24 | # fetch with web requests
25 | try:
26 | response = get(f"https://api.github.com/repos/{Nugget_Repo}")
27 | response.raise_for_status() # To raise an exception for 4xx/5xx responses
28 |
29 | data = response.json() # Parse the JSON response
30 |
31 | # Check if "tag_name" exists in the response and compare the version
32 | tag_name = data.get("tag_name")
33 | if tag_name:
34 | last_fetched_version = tag_name.replace("v", "") # Remove 'v' from tag_name
35 | return last_fetched_version
36 | except RequestException as e:
37 | print(f"Error fetching data: {e}")
38 | except JSONDecodeError as e:
39 | print(f"Error parsing JSON: {e}")
40 | return None
--------------------------------------------------------------------------------
/controllers/xml_handler.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as tree
2 |
3 | tree.register_namespace('', "http://www.apple.com/CoreAnimation/1.0")
4 |
5 | def parse_equation(eq: str, val: str):
6 | eqns = eq.split(',')
7 | value = [val]
8 | if ' ' in val:
9 | # convert to array
10 | value = val.split(' ')
11 | # map the value to floats
12 | value = list(map(lambda x: float(x), value))
13 | keys = ['x', 'y', 'z', 'a']
14 | results = []
15 | mapped = dict(zip(keys, value))
16 | for eqn in eqns:
17 | results.append(str(eval(eqn, {}, mapped)))
18 | # map back to string
19 | return ' '.join(results)
20 |
21 | def set_xml_value(file: str, id: str, key: str, val: any, use_ca_id: bool = False):
22 | set_xml_values(file=file, id=id, keys=[key], values=[val], use_ca_id=use_ca_id)
23 |
24 | def set_xml_values(file: str, id: str, keys: list[str], values: list[any], use_ca_id: bool = False):
25 | xml = tree.parse(file)
26 | root = xml.getroot()
27 |
28 | # convert bool to integer
29 | for i in range(len(values)):
30 | if isinstance(values[i], bool):
31 | values[i] = int(values[i])
32 |
33 | # set all values with the nugget id passed by param
34 | if use_ca_id:
35 | idKey = "id"
36 | else:
37 | idKey = "nuggetId"
38 | for to_change in root.findall(f".//*[@{idKey}='{id}']"):
39 | eqn = to_change.get("nuggetOffset")
40 | # TODO: Allow offsets for more than just the first value
41 | for i in range(len(keys)):
42 | offsetVal = values[i]
43 | if i == 0 and eqn != None:
44 | offsetVal = parse_equation(eqn, offsetVal)
45 | # find the value type
46 | check_val = to_change.get(keys[i])
47 | if check_val != None and isinstance(check_val, str) and not isinstance(offsetVal, str):
48 | offsetVal = str(offsetVal)
49 | to_change.set(keys[i], offsetVal)
50 | if use_ca_id:
51 | # also look for target id
52 | for to_change in root.findall(f".//*[@targetId='{id}']"):
53 | for i in range(len(keys)):
54 | # find the value type
55 | set_val = values[i]
56 | check_val = to_change.get(keys[i])
57 | if check_val != None and isinstance(check_val, str) and not isinstance(set_val, str):
58 | set_val = str(set_val)
59 | to_change.set(keys[i], set_val)
60 |
61 | # write back to file
62 | xml.write(file, encoding="UTF-8", xml_declaration=True)
63 |
64 | def remove_from_root(root, search):
65 | for parent in root.findall(search + "/.."):
66 | for prop in parent.findall(search):
67 | parent.remove(prop)
68 |
69 | def delete_xml_value(file: str, id: str, use_ca_id: bool = False):
70 | xml = tree.parse(file)
71 | root = xml.getroot()
72 |
73 | # delete all elements with the nugget id passed by param
74 | if use_ca_id:
75 | idKey = "id"
76 | else:
77 | idKey = "nuggetId"
78 | remove_from_root(root, search=f".//*[@{idKey}='{id}']")
79 | if use_ca_id:
80 | # also remove the target ids
81 | remove_from_root(root, search=f".//*[@targetId='{id}']")
82 |
83 | # write back to file
84 | xml.write(file, encoding="UTF-8", xml_declaration=True)
--------------------------------------------------------------------------------
/credits/LeminLimez.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/credits/LeminLimez.png
--------------------------------------------------------------------------------
/credits/big_nugget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/credits/big_nugget.png
--------------------------------------------------------------------------------
/credits/small_nugget.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/credits/small_nugget.png
--------------------------------------------------------------------------------
/devicemanagement/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/devicemanagement/__init__.py
--------------------------------------------------------------------------------
/devicemanagement/constants.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from pymobiledevice3.lockdown import LockdownClient
3 |
4 | class Device:
5 | def __init__(self, uuid: int, usb: bool, name: str, version: str, build: str, model: str, hardware: str, cpu: str, locale: str, ld: LockdownClient):
6 | self.uuid = uuid
7 | self.connected_via_usb = usb
8 | self.name = name
9 | self.version = version
10 | self.build = build
11 | self.model = model
12 | self.hardware = hardware
13 | self.cpu = cpu
14 | self.locale = locale
15 | self.ld = ld
16 |
17 | def is_exploit_fully_patched(self) -> bool:
18 | parsed_ver: Version = Version(self.version)
19 | # mobile gestalt methods are completely patched on iOS 18.2 dev beta 3+
20 | if (parsed_ver < Version("18.2")
21 | or self.build == "22C5109p" or self.build == "22C5125e"):
22 | return False
23 | return True
24 |
25 | def has_exploit(self) -> bool:
26 | parsed_ver: Version = Version(self.version)
27 | # make sure versions past 17.7.1 but before 18.0 aren't supported
28 | if (parsed_ver >= Version("17.7.1") and parsed_ver < Version("18.0")):
29 | return False
30 | if (parsed_ver < Version("18.1")
31 | or self.build == "22B5007p" or self.build == "22B5023e"
32 | or self.build == "22B5034e" or self.build == "22B5045g"):
33 | return True
34 | return False
35 |
36 | def supported(self) -> bool:
37 | return self.has_exploit()
38 |
39 | class Version:
40 | def __init__(self, major: int, minor: int = 0, patch: int = 0):
41 | self.major = major
42 | self.minor = minor
43 | self.patch = patch
44 |
45 | def __init__(self, ver: str):
46 | nums: list[str] = ver.split(".")
47 | self.major = int(nums[0])
48 | self.minor = int(nums[1]) if len(nums) > 1 else 0
49 | self.patch = int(nums[2]) if len(nums) > 2 else 0
50 |
51 | # Comparison Functions
52 | def compare_to(self, other) -> int:
53 | if self.major > other.major:
54 | return 1
55 | elif self.major < other.major:
56 | return -1
57 | if self.minor > other.minor:
58 | return 1
59 | elif self.minor < other.minor:
60 | return -1
61 | if self.patch > other.patch:
62 | return 1
63 | elif self.patch < other.patch:
64 | return -1
65 | return 0
66 |
67 | def __gt__(self, other) -> bool:
68 | return self.compare_to(other) == 1
69 | def __ge__(self, other) -> bool:
70 | comp: int = self.compare_to(other)
71 | return comp == 0 or comp == 1
72 |
73 | def __lt__(self, other) -> bool:
74 | return self.compare_to(other) == -1
75 | def __le__(self, other) -> bool:
76 | comp: int = self.compare_to(other)
77 | return comp == 0 or comp == -1
78 |
79 | def __eq__(self, other) -> bool:
80 | return self.compare_to(other) == 0
81 |
82 | class Tweak(Enum):
83 | StatusBar = 'Status Bar'
84 | SpringboardOptions = 'Springboard Options'
85 | InternalOptions = 'Internal Options'
86 | SkipSetup = 'Setup Options'
87 |
88 | class FileLocation(Enum):
89 | # Control Center
90 | mute = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.control-center.MuteModule.plist"
91 | focus = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.FocusUIModule.plist"
92 | spoken = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.siri.SpokenNotificationsModule.plist"
93 | module_config = "ControlCenter/HomeDomain/Library/ControlCenter/ModuleConfiguration.plist"
94 | replay_kit_audio = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.replaykit.AudioConferenceControlCenterModule.plist"
95 | replay_kit_video = "ControlCenter/ManagedPreferencesDomain/mobile/com.apple.replaykit.VideoConferenceControlCenterModule.plist"
96 |
97 | # Status Bar
98 | status_bar = "StatusBar/HomeDomain/Library/SpringBoard/statusBarOverrides"
99 |
100 | # Springboard Options
101 | springboard = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.springboard.plist"
102 | footnote = "SpringboardOptions/ConfigProfileDomain/Library/ConfigurationProfiles/SharedDeviceConfiguration.plist"
103 | wifi = "SpringboardOptions/SystemPreferencesDomain/SystemConfiguration/com.apple.wifi.plist"
104 | uikit = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.UIKit.plist"
105 | accessibility = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.Accessibility.plist"
106 | wifi_debug = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.MobileWiFi.debug.plist"
107 | airdrop = "SpringboardOptions/ManagedPreferencesDomain/mobile/com.apple.sharingd.plist"
108 |
109 | # Internal Options
110 | global_prefs = "InternalOptions/ManagedPreferencesDomain/mobile/hiddendotGlobalPreferences.plist"
111 | app_store = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.AppStore.plist"
112 | backboardd = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.backboardd.plist"
113 | core_motion = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.CoreMotion.plist"
114 | pasteboard = "InternalOptions/HomeDomain/Library/Preferences/com.apple.Pasteboard.plist"
115 | notes = "InternalOptions/ManagedPreferencesDomain/mobile/com.apple.mobilenotes.plist"
116 | maps = "InternalOptions/AppDomain-com.apple.Maps/Library/Preferences/com.apple.Maps.plist"
117 | weather = "InternalOptions/AppDomain-com.apple.weather/Library/Preferences/com.apple.weather.plist"
118 |
119 | # Setup Options
120 | cloud_config = "SkipSetup/ConfigProfileDomain/Library/ConfigurationProfiles/CloudConfigurationDetails.plist"
--------------------------------------------------------------------------------
/devicemanagement/data_singleton.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from devicemanagement.constants import Device, Tweak
4 |
5 | class DataSingleton:
6 | def __init__(self):
7 | self.current_device: Device
8 | self.device_available: bool = False
9 | self.gestalt_path = None
--------------------------------------------------------------------------------
/devicemanagement/generate_mga.py:
--------------------------------------------------------------------------------
1 | from pymobiledevice3.lockdown import LockdownClient
2 |
3 | def get_model_name(product_type: str) -> str:
4 | return "iPhone something"
5 |
6 | def get_idiom(device_class: str) -> str:
7 | if device_class == 'iPhone':
8 | return 'phone'
9 | else:
10 | return 'pad'
11 |
12 | def generate_mga(ld: LockdownClient) -> dict:
13 | # queries the device values and generates a mobile gestalt dictionary to return
14 | vals = ld.all_values
15 | prod = vals['ProductType']
16 | device_class = vals['DeviceClass']
17 | region = vals['RegionInfo'].split("/")
18 | build = vals['BuildVersion']
19 | model_name = get_model_name(prod)
20 | cache_extra = {
21 | # main dict to query
22 | "+3Uf0Pm5F8Xy7Onyvko0vA": device_class, # DeviceClass
23 | "/YYygAofPDbhrwToVsXdeA": vals['HardwareModel'], # HardwareModel
24 | "0+nc/Udy4WNG8S+Q7a/s1A": prod, # ProductType
25 | # "0GizaJLOyfzgAbxQ/5aniA" ## DeviceHousingColorUncooked
26 | # "4W7X4OWHjri5PGaAGsCWxw" ## MaxH264PlaybackLevel
27 | # "4qfpxrvLtWillIHpIsVgMA" ## SystemImageID
28 | "5pYKlGnYYBzGvAlIU8RjEQ": vals['HardwarePlatform'], # HardwarePlatform
29 | # "913P3Zsei09w0GSSOaBD+w" ## VolumeUpButtonNormalizedCGRect
30 | "91LyMcx4z1w3SGVeqteMnA": vals['BasebandRegionSKU'], # BasebandRegionSKU
31 | # "96GRvvjuBKkU4HzNsYcHPA" ## MinimumSupportediTunesVersion
32 | # "97JDvERpVwO+GHtthIh7hA" ## RegulatoryModelNumber
33 | "9MZ5AdH43csAUajl/dU+IQ": vals['SupportedDeviceFamilies'], # SupportedDeviceFamilies
34 | "9s45ldrCC1WF+7b6C4H2BA": prod, # GSDeviceName
35 | # "AoKnINTLPoKML3ctoP0AZg" ## IOSurfaceFormatDictionary
36 | "DViRIxZ/ZwO007CLcEYvZw": "", # DViRIxZ/ZwO007CLcEYvZw
37 | # "HXTqT3UXOKuTEklxz+wMAA" ## BasebandAPTimeSync
38 | "IMLaTlxS7ITtwfbRfPYWuA": region[1], # DeviceVariantGuess
39 | # "J1QHVh74Nnd6Rqyiq71/yw" ## AVDDecodingRate
40 | # "JHXk7RXOxvlqK+SxkwcM2A" ## LowPowerExpressModesSupported
41 | "JUWcn+5Ss0nvr5w/jk4WEg": device_class, # device-name
42 | # "JhEU414EIaDvAz8ki5DSqw" ## DeviceEnclosureColor
43 | # "LTI8wHvEYKy8zR1IXBW1uQ" ## ArtworkTraitDisplayGamut
44 | "LeSRsiLoJCMhjn6nd6GWbQ": vals['FirmwareVersion'], # FirmwareVersion
45 | # "NUYAz1eq3Flzt7ZQxXC/ng" ## FirstPartyLaunchTimeLimitScale
46 | # "NaA/zJV7myg2w4YNmSe4yQ" ## WifiChipset
47 | # "Nzu4E/VsXjEIa83CkRdZrQ" ## Image4CryptoHashMethod
48 | # "PdprWthPO6YyrO6p1vLRgQ" ## VolumeDownButtonCGRect
49 | # "QbQzuIbef01P4JeoL9EmKg" ## DeviceSceneUpdateTimeLimitScale
50 | # "SbXytSPZXB1jQ8GLZOxCPw" ## VolumeDownButtonNormalizedCGRect
51 | # "TZ/0j62wM3D0CuRt+Nc/Lw" ## ProductHash
52 | # "VuGdqp8UBpi9vPWHlPluVQ" ## CompatibleAppVariants
53 | # "WPEkba78QeFFU/wgqpOx6w" ## UserIntentPhysicalButtonNormalizedCGRect
54 | "Z/dqyWS6OZTRy10UcmUAhw": model_name, # marketing-name
55 | # "aD51uqjUwgRKjAC04BCrxg" ## VolumeUpButtonCGRect
56 | "bbtR9jQx50Fv5Af/affNtA": model_name, # PhysicalHardwareNameString
57 | # "c7fCSBIbX1mFaRoKT5zTIw" ## WifiVendor
58 | # "emXA9B552rnSoI7xXE91DA" ## DeviceLaunchTimeLimitScale
59 | # "gBw7IWiBnLHaA+lBrZBgWw" ## CameraMaxBurstLength
60 | # "gD8SNRcHQeIxCAvsp+2vjA" ## WSKU
61 | "h63QSdBCiT/z0WU6rdQv6Q": region[0], # RegionCode
62 | "h9jDsbgj7xIVeIQ8S3/X3Q": prod, # ProductType
63 | "ivIu8YTDnBSrYv/SN4G8Ag": vals['ProductName'], # ProductName
64 | # "k+KTni1jrwErpcDMEnn3aw" ## MobileDeviceMinimumVersion
65 | "k7QIBwZJJOVw+Sej/8h8VA": vals['CPUArchitecture'], # CPUArchitecture
66 | "mZfUC7qo4pURNhyMHZ62RQ": build, # BuildVersion
67 | "mumHZHMLEfAuTkkd28fHlQ": vals['DeviceColor'], # DeviceColor
68 | # "nSo8opze5rFk+EdBoR6tBw" ## RestrictedCountryCodes
69 | # "oBbtJ8x+s1q0OkaiocPuog" ## MainScreenStaticInfo
70 | "oPeik/9e8lQWMszEjbPzng": { # ArtworkTraits
71 | "ArtworkDeviceIdiom": get_idiom(device_class),
72 | "ArtworkDeviceProductDescription": model_name,
73 | # "ArtworkDeviceScaleFactor"
74 | # "ArtworkDeviceSubType"
75 | # "ArtworkDisplayGamut"
76 | # "ArtworkDynamicDisplayMode"
77 | # "CompatibleDeviceFallback"
78 | # "DevicePerformanceMemoryClass"
79 | # "GraphicsFeatureSetClass"
80 | # "GraphicsFeatureSetFallbacks"
81 | },
82 | "oYicEKzVTz4/CxxE05pEgQ": vals['HardwareModel'], # TargetSubType
83 | # "pB5sZVvnp+QjZQtt2KfQvA" ## BasebandChipset
84 | # "pMeQxE5szZTjLMk10TisDQ" ## UserIntentPhysicalButtonCGRect
85 | "qNNddlUK+B/YlooNoymwgA": vals['ProductVersion'], # ProductVersion
86 | # "qwXfFvH5jPXPxrny0XuGtQ" ## BuildID
87 | # "rJkMAGeVLdhP5+10G5hVcA" ## UserIntentPhysicalButtonCGRectString
88 | "rkqlwPcRHwixY4gapPjanw": device_class, # DeviceName
89 | "vme9Buk6XiWFCXoHApxNFA": device_class, # MarketingDeviceFamilyName
90 | # "wYMBabAO8VguyDDVgCsPdg" ## WiFiChipsetRevision
91 | "xUHcyT2/HE8oi/4LaOI+Sw": vals['PartitionType'], # PartitionType
92 | # "xojWvSTQWT7Icy+xfVzjAQ" ## FramebufferIdentifier
93 | # "yUqD8AXE/c+IggkuYoxrqA" ## ChromeIdentifier
94 | # "ybGkijAwLTwevankfVzsDQ" ## MainScreenCanvasSizes
95 | "yjP8DgByZmLk04Ta6f6DWQ": "iOS", # PartitionStyle
96 | "zHeENZu+wbg7PUprwNwBWg": vals['RegionInfo'], # RegionInfo
97 | }
98 | return {
99 | # "CacheData": b"",
100 | "CacheExtra": cache_extra,
101 | # "CacheUUID": "",
102 | "CacheVersion": build # build number
103 | }
--------------------------------------------------------------------------------
/entitlements.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | com.apple.security.cs.allow-jit
7 |
8 | com.apple.security.cs.allow-unsigned-executable-memory
9 |
10 | com.apple.security.cs.disable-library-validation
11 |
12 |
13 |
--------------------------------------------------------------------------------
/exceptions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/exceptions/__init__.py
--------------------------------------------------------------------------------
/exceptions/nugget_exception.py:
--------------------------------------------------------------------------------
1 | class NuggetException(Exception):
2 | def __init__(self, message: str):
3 | self.message = message
4 | super().__init__(self.message)
5 |
6 | def __str__(self):
7 | return self.message
--------------------------------------------------------------------------------
/exceptions/posterboard_exceptions.py:
--------------------------------------------------------------------------------
1 | from .nugget_exception import NuggetException
2 |
3 | class VideoLengthException(NuggetException):
4 | def __init__(self, frame_limit: int):
5 | super().__init__(f"Videos must be under {frame_limit} frames to loop. Either reduce the frame rate or make it shorter.")
6 |
7 | class PBTemplateException(Exception):
8 | def __init__(self, file: str, message: str):
9 | self.message = message
10 | super().__init__(self.message)
11 | self.filename = file
12 | def __str__(self):
13 | return self.message
--------------------------------------------------------------------------------
/files/SSLconf/TrustStore.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/SSLconf/TrustStore.sqlite3
--------------------------------------------------------------------------------
/files/eligibility/Config.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/eligibility/Config.plist
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/com.apple.posterkit.provider.descriptor.identifier:
--------------------------------------------------------------------------------
1 | 88D1F444-E2B3-43C4-BB15-790C67EDEA56
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/providerInfo.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/providerInfo.plist
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/.com.apple.posterkit.provider.contents.configurableOptions.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/.com.apple.posterkit.provider.contents.configurableOptions.plist
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/input.segmentation/asset.resource/contents.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/input.segmentation/asset.resource/contents.plist
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/output.layerStack/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "version" : 10,
3 | "layers" : [
4 | {
5 | "frame" : {
6 | "Width" : 1042.4546142208774,
7 | "Height" : 2868.5230711043873,
8 | "Y" : -0.52307110438732707,
9 | "X" : 174.92435703479578
10 | },
11 | "filename" : "portrait-layer_background.HEIC",
12 | "zPosition" : 5,
13 | "identifier" : "background"
14 | },
15 | {
16 | "frame" : {
17 | "Width" : 1182.5,
18 | "Height" : 2569.25,
19 | "Y" : 149.37503513892489,
20 | "X" : 68.749995738267913
21 | },
22 | "filename" : "portrait-layer_settling-video.MOV",
23 | "zPosition" : 5,
24 | "identifier" : "settling-video"
25 | }
26 | ],
27 | "properties" : {
28 | "portraitLayout" : {
29 | "clockIntersection" : 2,
30 | "deviceResolution" : {
31 | "Width" : 750,
32 | "Height" : 1334
33 | },
34 | "visibleFrame" : {
35 | "Width" : 886.43178410794587,
36 | "Height" : 2569.25,
37 | "Y" : 149.37496486107506,
38 | "X" : 252.31008241168968
39 | },
40 | "timeFrame" : {
41 | "Width" : 763.51324337831068,
42 | "Height" : 227.26499250374812,
43 | "Y" : 2135.0546874997553,
44 | "X" : 313.76935277650728
45 | },
46 | "clockLayerOrder" : "ClockAboveForeground",
47 | "hasTopEdgeContact" : false,
48 | "inactiveFrame" : {
49 | "Width" : 913.02473763118462,
50 | "Height" : 2646.3274999999999,
51 | "Y" : 149.39630414678933,
52 | "X" : 239.01360565007042
53 | },
54 | "imageSize" : {
55 | "Width" : 1320,
56 | "Height" : 2868
57 | },
58 | "parallaxPadding" : {
59 | "Width" : 61.45927036481757,
60 | "Height" : 151.28435782108946
61 | }
62 | },
63 | "settlingEffectEnabled" : true,
64 | "depthEnabled" : false,
65 | "clockAreaLuminance" : 0.64877637987012982,
66 | "parallaxDisabled" : false
67 | }
68 | }
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/0EFB6A0F-7052-4D24-8859-AB22BADF2E93/style.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | colorSuggestions
6 |
7 | kind
8 | Original
9 | parameters
10 |
11 | clockColor
12 |
13 | 1
14 | 1
15 | 1
16 |
17 | clockVibrancy
18 | 0.0
19 | headroomLook
20 | 2
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/DescriptorModel.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/DescriptorModel.plist
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.galleryOptions:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.galleryOptions
--------------------------------------------------------------------------------
/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.userInfo:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/1F20C883-EA98-4CCE-9923-0C9A01359721/versions/0/contents/com.apple.posterkit.provider.contents.userInfo
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/com.apple.posterkit.provider.descriptor.identifier:
--------------------------------------------------------------------------------
1 | 9183
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/com.apple.posterkit.role.identifier:
--------------------------------------------------------------------------------
1 | PRPosterRoleLockScreen
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/providerInfo.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/VideoCAML/providerInfo.plist
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Floating-810w-1080h@2x~ipad.ca/index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | assetManifest
6 | assetManifest.caml
7 | documentHeight
8 | 3176
9 | documentResizesToView
10 |
11 | documentWidth
12 | 3176
13 | dynamicGuidesEnabled
14 |
15 | geometryFlipped
16 |
17 | guidesEnabled
18 |
19 | interactiveMouseEventsEnabled
20 |
21 | interactiveShowsCursor
22 |
23 | interactiveTouchEventsEnabled
24 |
25 | loopEnd
26 | 0.0
27 | loopStart
28 | 0.0
29 | loopingEnabled
30 |
31 | multitouchDisablesMouse
32 |
33 | multitouchEnabled
34 |
35 | presentationMouseEventsEnabled
36 |
37 | presentationShowsCursor
38 |
39 | presentationTouchEventsEnabled
40 |
41 | rootDocument
42 | main.caml
43 | savesWindowFrame
44 |
45 | scalesToFitInPlayer
46 |
47 | showsTouches
48 |
49 | snappingEnabled
50 |
51 | timelineMarkers
52 | [(null)]
53 | touchesColor
54 | 1 1 0 0.8
55 | unitsInPixelsInPlayer
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Floating-810w-1080h@2x~ipad.ca/main.caml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/Wallpaper.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | appearanceAware
6 |
7 | assets
8 |
9 | lockAndHome
10 |
11 | default
12 |
13 | backgroundAnimationFileName
14 | 9183.Custom_Background-810w-1080h@2x~ipad.ca
15 | floatingAnimationFileNameKey
16 | 9183.Custom_Floating-810w-1080h@2x~ipad.ca
17 | identifier
18 | 9183
19 | name
20 | Chip
21 | type
22 | LayeredAnimation
23 |
24 |
25 |
26 | contentVersion
27 | 2.01
28 | family
29 | Chip
30 | identifier
31 | 9183
32 | logicalScreenClass
33 | 810w-1080h@2x~ipad
34 | name
35 | Chip
36 | preferredProminentColor
37 |
38 | dark
39 | #00000
40 | default
41 | #FFFFFF
42 |
43 | version
44 | 1
45 |
46 |
47 |
--------------------------------------------------------------------------------
/files/posterboard/VideoCAML/versions/1/contents/com.apple.posterkit.provider.contents.userInfo:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | posterEnvironmentOverrides
6 |
7 | e30=
8 |
9 | wallpaperRepresentingFileName
10 | 9183.Custom-810w-1080h@2x~ipad.wallpaper
11 | wallpaperRepresentingIdentifier
12 | 9999
13 |
14 |
15 |
--------------------------------------------------------------------------------
/files/posterboard/contents.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/files/posterboard/contents.plist
--------------------------------------------------------------------------------
/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/__init__.py
--------------------------------------------------------------------------------
/gui/apply_worker.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtCore import Signal, QThread
2 | from PySide6.QtWidgets import QMessageBox
3 |
4 | class ApplyAlertMessage:
5 | def __init__(self, txt: str, title: str = "Error!", icon = QMessageBox.Critical, detailed_txt: str = None):
6 | self.txt = txt
7 | self.title = title
8 | self.icon = icon
9 | self.detailed_txt = detailed_txt
10 |
11 | class ApplyThread(QThread):
12 | progress = Signal(str)
13 | alert = Signal(ApplyAlertMessage)
14 |
15 | def update_label(self, txt: str):
16 | self.progress.emit(txt)
17 | def alert_window(self, msg: ApplyAlertMessage):
18 | self.alert.emit(msg)
19 |
20 | def __init__(self, manager, resetting: bool = False):
21 | super().__init__()
22 | self.manager = manager
23 | self.resetting = resetting
24 |
25 | def do_work(self):
26 | self.manager.apply_changes(self.resetting, self.update_label, self.alert_window)
27 |
28 | def run(self):
29 | self.do_work()
30 |
31 | class RefreshDevicesThread(QThread):
32 | alert = Signal(ApplyAlertMessage)
33 |
34 | def alert_window(self, msg: ApplyAlertMessage):
35 | self.alert.emit(msg)
36 |
37 | def __init__(self, manager, settings):
38 | super().__init__()
39 | self.manager = manager
40 | self.settings = settings
41 |
42 | def do_work(self):
43 | self.manager.get_devices(self.settings, self.alert_window)
44 |
45 | def run(self):
46 | self.do_work()
--------------------------------------------------------------------------------
/gui/custom_qt_elements/multicombobox.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtGui import QStandardItemModel, QStandardItem
2 | from PySide6.QtWidgets import QComboBox
3 | from PySide6.QtCore import Qt
4 |
5 |
6 | class MultiComboBox(QComboBox):
7 | def __init__(self, parent=None, updateAction=lambda x: None):
8 | super().__init__(parent)
9 | self.setEditable(True)
10 | self.lineEdit().setReadOnly(True)
11 | self.setModel(QStandardItemModel(self))
12 | self.updateAction = updateAction
13 |
14 | # Connect to the dataChanged signal to update the text
15 | self.model().dataChanged.connect(self.updateText)
16 |
17 | def addItem(self, text: str, data=None):
18 | item = QStandardItem()
19 | item.setText(text)
20 | item.setEnabled(True)
21 | item.setFlags(Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable)
22 | item.setData(Qt.CheckState.Unchecked, Qt.ItemDataRole.CheckStateRole)
23 | self.model().appendRow(item)
24 |
25 | def addItems(self, items_list: list):
26 | for text in items_list:
27 | self.addItem(text)
28 |
29 | def deselectAll(self):
30 | for i in range(self.model().rowCount()):
31 | self.model().item(i).setCheckState(Qt.CheckState.Unchecked)
32 |
33 | def selectIndices(self, indices: list[int]):
34 | for idx in indices:
35 | self.model().item(idx).setCheckState(Qt.CheckState.Checked)
36 |
37 | def updateText(self):
38 | selected_items = [self.model().item(i).text() for i in range(self.model().rowCount())
39 | if self.model().item(i).checkState() == Qt.CheckState.Checked]
40 | if len(selected_items) == 0:
41 | self.lineEdit().setText(" None")
42 | elif len(selected_items) == 1:
43 | self.lineEdit().setText(f" {selected_items[0]}")
44 | else:
45 | self.lineEdit().setText(f" ({len(selected_items)})")
46 | if self.updateAction != None:
47 | self.updateAction(selected_items)
48 |
49 | def showPopup(self):
50 | super().showPopup()
51 | # Set the state of each item in the dropdown
52 | for i in range(self.model().rowCount()):
53 | item = self.model().item(i)
54 | combo_box_view = self.view()
55 | combo_box_view.setRowHidden(i, False)
56 | check_box = combo_box_view.indexWidget(item.index())
57 | if check_box:
58 | check_box.setChecked(item.checkState() == Qt.CheckState.Checked)
59 |
60 | def hidePopup(self):
61 | # Update the check state of each item based on the checkbox state
62 | for i in range(self.model().rowCount()):
63 | item = self.model().item(i)
64 | combo_box_view = self.view()
65 | check_box = combo_box_view.indexWidget(item.index())
66 | if check_box:
67 | item.setCheckState(Qt.CheckState.Checked if check_box.isChecked() else Qt.CheckState.Unchecked)
68 | super().hidePopup()
--------------------------------------------------------------------------------
/gui/custom_qt_elements/resizable_image_label.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtWidgets import QLabel
2 | from PySide6.QtGui import QPixmap
3 | from PySide6.QtCore import Qt, QSize
4 |
5 | class ResizableImageLabel(QLabel):
6 | def __init__(self, parent=None):
7 | super().__init__(parent)
8 | self.original_pixmap = None
9 | # self.setScaledContents(True)
10 |
11 | def setPixmap(self, pixmap: QPixmap):
12 | self.original_pixmap = pixmap
13 | self.update_scaled_pixmap()
14 |
15 | def resizeEvent(self, event):
16 | if self.original_pixmap:
17 | self.update_scaled_pixmap()
18 | super().resizeEvent(event)
19 |
20 | def update_scaled_pixmap(self):
21 | if self.original_pixmap:
22 | scaled_pixmap = self.original_pixmap.scaled(
23 | self.width(),
24 | self.height(),
25 | Qt.AspectRatioMode.KeepAspectRatio,
26 | Qt.TransformationMode.SmoothTransformation
27 | )
28 | super().setPixmap(scaled_pixmap)
--------------------------------------------------------------------------------
/gui/dialogs.py:
--------------------------------------------------------------------------------
1 | from PySide6.QtWidgets import QDialog, QDialogButtonBox, QLabel, QVBoxLayout, QHBoxLayout, QWidget, QToolButton, QSizePolicy
2 | from PySide6.QtGui import QFont, QIcon, QPixmap
3 | from PySide6.QtCore import QSize
4 |
5 | from webbrowser import open_new_tab
6 |
7 | from controllers.web_request_handler import Nugget_Repo, get_latest_version
8 |
9 | class GestaltDialog(QDialog):
10 | def __init__(self, device_manager, gestalt_label, selected_file, parent=None):
11 | super().__init__(parent)
12 | self.device_manager = device_manager
13 | self.gestalt_label = gestalt_label
14 | self.selected_file = selected_file
15 |
16 | QBtn = (
17 | QDialogButtonBox.Ok | QDialogButtonBox.Cancel
18 | )
19 |
20 | self.buttonBox = QDialogButtonBox(QBtn)
21 | self.buttonBox.accepted.connect(self.accept)
22 | self.buttonBox.rejected.connect(self.reject)
23 |
24 | layout = QVBoxLayout()
25 | message = QLabel("The gestalt file looks like it was made for a different device.\nAre you sure you want to use this one?")
26 | layout.addWidget(message)
27 | layout.addWidget(self.buttonBox)
28 | self.setLayout(layout)
29 |
30 | def accept(self):
31 | self.device_manager.data_singleton.gestalt_path = self.selected_file
32 | self.gestalt_label.setText(self.selected_file)
33 | super().accept()
34 |
35 |
36 | class PBHelpDialog(QDialog):
37 | def __init__(self, parent=None):
38 | super().__init__(parent)
39 | QBtn = (
40 | QDialogButtonBox.Ok
41 | )
42 | self.buttonBox = QDialogButtonBox(QBtn)
43 | self.buttonBox.accepted.connect(self.accept)
44 | self.setWindowTitle("PosterBoard Info")
45 |
46 | layout = QVBoxLayout()
47 | message = QLabel("Descriptors will be under the Collections section when adding a new wallpaper.\n\nIf the wallpapers don't appear in the menu, you either have to wait a bit for them to load,\nor you've reached the maximum amount of wallpapers (15) and have to wipe them.")
48 | layout.addWidget(message)
49 |
50 | imgBox = QWidget()
51 | imgBox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
52 | imgBox.setStyleSheet("QWidget { background: none; padding: 0px; border: none; }")
53 | hlayout = QHBoxLayout()
54 | tut1 = QToolButton()
55 | tut1.setIconSize(QSize(138, 300))
56 | tut1.setStyleSheet("QToolButton { background: none; padding: 0px; border: none; }")
57 | tut1.setIcon(QIcon(QPixmap(":/gui/pb_tutorial1.png")))
58 | hlayout.addWidget(tut1)
59 | tut2 = QToolButton()
60 | tut2.setIconSize(QSize(138, 300))
61 | tut2.setStyleSheet("QToolButton { background: none; padding: 0px; border: none; }")
62 | tut2.setIcon(QIcon(QPixmap(":/gui/pb_tutorial2.png")))
63 | hlayout.addWidget(tut2)
64 | imgBox.setLayout(hlayout)
65 |
66 | layout.addWidget(imgBox)
67 | layout.addWidget(self.buttonBox)
68 | self.setLayout(layout)
69 |
70 |
71 | class UpdateAppDialog(QDialog):
72 | def __init__(self, parent=None):
73 | super().__init__(parent)
74 |
75 | QBtn = (
76 | QDialogButtonBox.Ok | QDialogButtonBox.Cancel
77 | )
78 |
79 | self.buttonBox = QDialogButtonBox(QBtn)
80 | self.buttonBox.accepted.connect(self.accept)
81 | self.buttonBox.rejected.connect(self.reject)
82 |
83 | layout = QVBoxLayout()
84 | title = QLabel("Update Available")
85 | title_font = QFont()
86 | title_font.setBold(True)
87 | title.setFont(title_font)
88 |
89 | message_text = ""
90 | latest_version = get_latest_version()
91 | if latest_version != None:
92 | message_text += f"Nugget v{latest_version} is available. "
93 | message_text += "Would you like to go to the download on GitHub?"
94 | message = QLabel(message_text)
95 |
96 | layout.addWidget(title)
97 | layout.addWidget(message)
98 | layout.addWidget(self.buttonBox)
99 | self.setLayout(layout)
100 |
101 | def accept(self):
102 | # open up the repo page
103 | open_new_tab(f"https://github.com/{Nugget_Repo}")
104 | super().accept()
--------------------------------------------------------------------------------
/gui/pages/__init__.py:
--------------------------------------------------------------------------------
1 | from .main.home import HomePage as Home
2 | from .tools.posterboard import PosterboardPage as Posterboard
3 | from .tools.gestalt import GestaltPage as MobileGestalt
4 | from .tools.eligibility import EligibilityPage as Eligibility
5 | from .tools.featureflags import FeatureFlagsPage as FeatureFlags
6 | from .tools.springboard import SpringboardPage as Springboard
7 | from .tools.internal import InternalPage as Internal
8 | from .tools.daemons import DaemonsPage as Daemons
9 | from .tools.risky import RiskyPage as Risky
10 | from .main.settings import SettingsPage as Settings
--------------------------------------------------------------------------------
/gui/pages/main/home.py:
--------------------------------------------------------------------------------
1 | import webbrowser
2 |
3 | from ..page import Page
4 | from qt.ui_mainwindow import Ui_Nugget
5 |
6 | from devicemanagement.constants import Version
7 |
8 | class HomePage(Page):
9 | def __init__(self, window, ui: Ui_Nugget):
10 | super().__init__()
11 | self.window = window
12 | self.ui = ui
13 | self.show_uuid = False
14 |
15 | def load_page(self):
16 | ## HOME PAGE ACTIONS
17 | self.ui.phoneVersionLbl.linkActivated.connect(self.toggle_version_label)
18 |
19 | ## HOME PAGE LINKS
20 | self.ui.bigNuggetBtn.clicked.connect(self.on_bigNuggetBtn_clicked)
21 | self.ui.starOnGithubBtn.clicked.connect(self.on_githubBtn_clicked)
22 |
23 | self.ui.leminGithubBtn.clicked.connect(self.on_leminGitHubBtn_clicked)
24 | self.ui.leminTwitterBtn.clicked.connect(self.on_leminTwitterBtn_clicked)
25 | self.ui.leminKoFiBtn.clicked.connect(self.on_leminKoFiBtn_clicked)
26 |
27 | self.ui.posterRestoreBtn.clicked.connect(self.on_posterRestoreBtn_clicked)
28 | self.ui.snoolieBtn.clicked.connect(self.on_snoolieBtn_clicked)
29 | self.ui.disfordottieBtn.clicked.connect(self.on_disfordottieBtn_clicked)
30 | self.ui.mikasaBtn.clicked.connect(self.on_mikasaBtn_clicked)
31 |
32 | self.ui.libiBtn.clicked.connect(self.on_libiBtn_clicked)
33 | self.ui.jjtechBtn.clicked.connect(self.on_jjtechBtn_clicked)
34 | self.ui.qtBtn.clicked.connect(self.on_qtBtn_clicked)
35 |
36 | self.ui.discordBtn.clicked.connect(self.on_discordBtn_clicked)
37 |
38 | ## ACTIONS
39 | def updatePhoneInfo(self):
40 | # name label
41 | self.ui.phoneNameLbl.setText(self.window.device_manager.get_current_device_name())
42 | # version label
43 | ver = self.window.device_manager.get_current_device_version()
44 | build = self.window.device_manager.get_current_device_build()
45 | self.show_uuid = False
46 | if ver != "":
47 | self.show_version_text(version=ver, build=build)
48 | else:
49 | self.ui.phoneVersionLbl.setText("Please connect a device.")
50 |
51 | def toggle_version_label(self):
52 | if self.show_uuid:
53 | self.show_uuid = False
54 | ver = self.window.device_manager.get_current_device_version()
55 | build = self.window.device_manager.get_current_device_build()
56 | if ver != "":
57 | self.show_version_text(version=ver, build=build)
58 | else:
59 | self.show_uuid = True
60 | uuid = self.window.device_manager.get_current_device_uuid()
61 | if uuid != "":
62 | self.ui.phoneVersionLbl.setText(f"{uuid}")
63 |
64 | def show_version_text(self, version: str, build: str):
65 | support_str: str = "Supported!"
66 | if Version(version) < Version("17.0"):
67 | support_str = "Not Supported."
68 | elif self.window.device_manager.get_current_device_patched():
69 | # sparserestore fully patched
70 | support_str = "Partially Supported."
71 | self.ui.phoneVersionLbl.setText(f"iOS {version} ({build}) {support_str}")
72 |
73 | ## HOME PAGE LINKS
74 | def on_bigMilkBtn_clicked(self):
75 | webbrowser.open_new_tab("https://cowabun.ga")
76 |
77 | def on_leminGitHubBtn_clicked(self):
78 | webbrowser.open_new_tab("https://github.com/leminlimez")
79 | def on_leminTwitterBtn_clicked(self):
80 | webbrowser.open_new_tab("https://twitter.com/LeminLimez")
81 | def on_leminKoFiBtn_clicked(self):
82 | webbrowser.open_new_tab("https://ko-fi.com/leminlimez")
83 |
84 | def on_posterRestoreBtn_clicked(self):
85 | webbrowser.open_new_tab("https://discord.gg/gWtzTVhMvh")
86 | def on_snoolieBtn_clicked(self):
87 | webbrowser.open_new_tab("https://github.com/0xilis/python-aar-stuff")
88 | def on_disfordottieBtn_clicked(self):
89 | webbrowser.open_new_tab("https://twitter.com/disfordottie")
90 | def on_mikasaBtn_clicked(self):
91 | webbrowser.open_new_tab("https://github.com/Mikasa-san/QuietDaemon")
92 |
93 | def on_libiBtn_clicked(self):
94 | webbrowser.open_new_tab("https://github.com/doronz88/pymobiledevice3")
95 | def on_jjtechBtn_clicked(self):
96 | webbrowser.open_new_tab("https://github.com/JJTech0130/TrollRestore")
97 | def on_qtBtn_clicked(self):
98 | webbrowser.open_new_tab("https://www.qt.io/product/development-tools")
99 |
100 | def on_discordBtn_clicked(self):
101 | webbrowser.open_new_tab("https://discord.gg/MN8JgqSAqT")
102 | def on_githubBtn_clicked(self):
103 | webbrowser.open_new_tab("https://github.com/leminlimez/Nugget")
104 | def on_bigNuggetBtn_clicked(self):
105 | webbrowser.open_new_tab("https://cowabun.ga")
--------------------------------------------------------------------------------
/gui/pages/main/settings.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 | from controllers.video_handler import set_ignore_frame_limit
6 |
7 | class SettingsPage(Page):
8 | def __init__(self, window, ui: Ui_Nugget):
9 | super().__init__()
10 | self.window = window
11 | self.ui = ui
12 |
13 | def load_page(self):
14 | self.ui.allowWifiApplyingChk.toggled.connect(self.on_allowWifiApplyingChk_toggled)
15 | self.ui.autoRebootChk.toggled.connect(self.on_autoRebootChk_toggled)
16 | self.ui.showRiskyChk.toggled.connect(self.on_showRiskyChk_toggled)
17 | self.ui.showAllSpoofableChk.toggled.connect(self.on_showAllSpoofableChk_toggled)
18 |
19 | self.ui.revertRdarChk.toggled.connect(self.on_revertRdarChk_toggled)
20 |
21 | self.ui.skipSetupChk.toggled.connect(self.on_skipSetupChk_toggled)
22 | self.ui.supervisionChk.toggled.connect(self.on_supervisionChk_toggled)
23 | self.ui.supervisionOrganization.textEdited.connect(self.on_supervisionOrgTxt_textEdited)
24 | self.ui.resetPairBtn.clicked.connect(self.on_resetPairBtn_clicked)
25 |
26 | ## ACTIONS
27 | def on_allowWifiApplyingChk_toggled(self, checked: bool):
28 | self.window.device_manager.apply_over_wifi = checked
29 | # save the setting
30 | self.window.settings.setValue("apply_over_wifi", checked)
31 | def on_showRiskyChk_toggled(self, checked: bool):
32 | self.window.device_manager.allow_risky_tweaks = checked
33 | # save the setting
34 | self.window.settings.setValue("show_risky_tweaks", checked)
35 | # toggle the button visibility
36 | if checked:
37 | self.ui.advancedPageBtn.show()
38 | self.ui.ignorePBFrameLimitChk.show()
39 | try:
40 | self.ui.resetPBDrp.removeItem(4)
41 | except:
42 | pass
43 | self.ui.resetPBDrp.addItem("PB Extensions")
44 | else:
45 | self.ui.advancedPageBtn.hide()
46 | self.ui.ignorePBFrameLimitChk.hide()
47 | try:
48 | self.ui.resetPBDrp.removeItem(4)
49 | except:
50 | pass
51 | def on_ignorePBFrameLimitChk_toggled(self, checked: bool):
52 | set_ignore_frame_limit(checked)
53 | # save the setting
54 | self.window.settings.setValue("ignore_pb_frame_limit", checked)
55 | def on_showAllSpoofableChk_toggled(self, checked: bool):
56 | self.window.device_manager.show_all_spoofable_models = checked
57 | # save the setting
58 | self.window.settings.setValue("show_all_spoofable_models", checked)
59 | # refresh the list of spoofable models
60 | self.window.setup_spoofedModelDrp_models()
61 | def on_autoRebootChk_toggled(self, checked: bool):
62 | self.window.device_manager.auto_reboot = checked
63 | # save the setting
64 | self.window.settings.setValue("auto_reboot", checked)
65 |
66 | def on_revertRdarChk_toggled(self, checked: bool):
67 | tweaks["RdarFix"].set_enabled(checked)
68 |
69 | def on_skipSetupChk_toggled(self, checked: bool):
70 | self.window.device_manager.skip_setup = checked
71 | # save the setting
72 | self.window.settings.setValue("skip_setup", checked)
73 | # hide/show the warning label
74 | if checked:
75 | self.ui.skipSetupOnLbl.show()
76 | else:
77 | self.ui.skipSetupOnLbl.hide()
78 | def on_supervisionOrgTxt_textEdited(self, text: str):
79 | self.window.device_manager.organization_name = text
80 | self.window.settings.setValue("organization_name", text)
81 | def on_supervisionChk_toggled(self, checked: bool):
82 | self.window.device_manager.supervised = checked
83 | # save the setting
84 | self.window.settings.setValue("supervised", checked)
85 |
86 | # Device Options
87 | def on_resetPairBtn_clicked(self):
88 | self.window.device_manager.reset_device_pairing()
--------------------------------------------------------------------------------
/gui/pages/page.py:
--------------------------------------------------------------------------------
1 | class Page:
2 | def __init__(self):
3 | self.loaded = False
4 |
5 | def load(self):
6 | if not self.loaded:
7 | self.load_page()
8 | self.loaded = True
9 |
10 | def load_page(self):
11 | raise NotImplementedError
--------------------------------------------------------------------------------
/gui/pages/tools/daemons.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 | from tweaks.daemons_tweak import Daemon
6 |
7 | class DaemonsPage(Page):
8 | def __init__(self, ui: Ui_Nugget):
9 | super().__init__()
10 | self.ui = ui
11 |
12 | def load_page(self):
13 | self.ui.modifyDaemonsChk.toggled.connect(self.on_modifyDaemonsChk_clicked)
14 | self.ui.thermalmonitordChk.toggled.connect(self.on_thermalmonitordChk_clicked)
15 | self.ui.otadChk.toggled.connect(self.on_otadChk_clicked)
16 | self.ui.usageTrackingAgentChk.toggled.connect(self.on_usageTrackingAgentChk_clicked)
17 |
18 | self.ui.gameCenterChk.toggled.connect(self.on_gameCenterChk_clicked)
19 | self.ui.screenTimeChk.toggled.connect(self.on_screenTimeChk_clicked)
20 | self.ui.clearScreenTimeAgentChk.toggled.connect(self.on_clearScreenTimeAgentChk_clicked)
21 | self.ui.crashReportsChk.toggled.connect(self.on_crashReportsChk_clicked)
22 | self.ui.atwakeupChk.toggled.connect(self.on_atwakeupChk_clicked)
23 | self.ui.tipsChk.toggled.connect(self.on_tipsChk_clicked)
24 | self.ui.vpndChk.toggled.connect(self.on_vpndChk_clicked)
25 | self.ui.wapicChk.toggled.connect(self.on_wapicChk_clicked)
26 | self.ui.healthdChk.toggled.connect(self.on_healthdChk_clicked)
27 |
28 | self.ui.airprintChk.toggled.connect(self.on_airprintChk_clicked)
29 | self.ui.assistiveTouchChk.toggled.connect(self.on_assistiveTouchChk_clicked)
30 | self.ui.icloudChk.toggled.connect(self.on_icloudChk_clicked)
31 | self.ui.hotspotChk.toggled.connect(self.on_hotspotChk_clicked)
32 | self.ui.passbookChk.toggled.connect(self.on_passbookChk_clicked)
33 | self.ui.spotlightChk.toggled.connect(self.on_spotlightChk_clicked)
34 | self.ui.voiceControlChk.toggled.connect(self.on_voiceControlChk_clicked)
35 |
36 | ## ACTIONS
37 | def on_modifyDaemonsChk_clicked(self, checked: bool):
38 | tweaks["Daemons"].set_enabled(checked)
39 | self.ui.daemonsPageContent.setDisabled(not checked)
40 |
41 | def on_thermalmonitordChk_clicked(self, checked: bool):
42 | tweaks["Daemons"].set_multiple_values(Daemon.thermalmonitord.value, value=checked)
43 | if checked:
44 | # set the modify toggle checked so it actually applies
45 | self.on_modifyDaemonsChk_clicked(True)
46 | def on_otadChk_clicked(self, checked: bool):
47 | tweaks["Daemons"].set_multiple_values(Daemon.OTA.value, value=checked)
48 | def on_usageTrackingAgentChk_clicked(self, checked: bool):
49 | tweaks["Daemons"].set_multiple_values(Daemon.UsageTrackingAgent.value, value=checked)
50 | def on_gameCenterChk_clicked(self, checked: bool):
51 | tweaks["Daemons"].set_multiple_values(Daemon.GameCenter.value, value=checked)
52 | def on_screenTimeChk_clicked(self, checked: bool):
53 | tweaks["Daemons"].set_multiple_values(Daemon.ScreenTime.value, value=checked)
54 | def on_clearScreenTimeAgentChk_clicked(self, checked: bool):
55 | tweaks["ClearScreenTimeAgentPlist"].set_enabled(checked)
56 | def on_crashReportsChk_clicked(self, checked: bool):
57 | tweaks["Daemons"].set_multiple_values(Daemon.CrashReports.value, value=checked)
58 | def on_atwakeupChk_clicked(self, checked: bool):
59 | tweaks["Daemons"].set_multiple_values(Daemon.ATWAKEUP.value, value=checked)
60 | def on_tipsChk_clicked(self, checked: bool):
61 | tweaks["Daemons"].set_multiple_values(Daemon.Tips.value, value=checked)
62 | def on_vpndChk_clicked(self, checked: bool):
63 | tweaks["Daemons"].set_multiple_values(Daemon.VPN.value, value=checked)
64 | def on_wapicChk_clicked(self, checked: bool):
65 | tweaks["Daemons"].set_multiple_values(Daemon.ChineseLAN.value, value=checked)
66 | def on_healthdChk_clicked(self, checked: bool):
67 | tweaks["Daemons"].set_multiple_values(Daemon.HealthKit.value, value=checked)
68 |
69 | def on_airprintChk_clicked(self, checked: bool):
70 | tweaks["Daemons"].set_multiple_values(Daemon.AirPrint.value, value=checked)
71 | def on_assistiveTouchChk_clicked(self, checked: bool):
72 | tweaks["Daemons"].set_multiple_values(Daemon.AssistiveTouch.value, value=checked)
73 | def on_icloudChk_clicked(self, checked: bool):
74 | tweaks["Daemons"].set_multiple_values(Daemon.iCloud.value, value=checked)
75 | def on_hotspotChk_clicked(self, checked: bool):
76 | tweaks["Daemons"].set_multiple_values(Daemon.InternetTethering.value, value=checked)
77 | def on_passbookChk_clicked(self, checked: bool):
78 | tweaks["Daemons"].set_multiple_values(Daemon.PassBook.value, value=checked)
79 | def on_spotlightChk_clicked(self, checked: bool):
80 | tweaks["Daemons"].set_multiple_values(Daemon.Spotlight.value, value=checked)
81 | def on_voiceControlChk_clicked(self, checked: bool):
82 | tweaks["Daemons"].set_multiple_values(Daemon.VoiceControl.value, value=checked)
--------------------------------------------------------------------------------
/gui/pages/tools/eligibility.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 |
6 | class EligibilityPage(Page):
7 | def __init__(self, window, ui: Ui_Nugget):
8 | super().__init__()
9 | self.window = window
10 | self.ui = ui
11 |
12 | def load_page(self):
13 | self.ui.euEnablerEnabledChk.toggled.connect(self.on_euEnablerEnabledChk_toggled)
14 | self.ui.methodChoiceDrp.activated.connect(self.on_methodChoiceDrp_activated)
15 | self.ui.regionCodeTxt.textEdited.connect(self.on_regionCodeTxt_textEdited)
16 |
17 | self.ui.enableAIChk.toggled.connect(self.on_enableAIChk_toggled)
18 | self.ui.eligFileChk.toggled.connect(self.on_eligFileChk_toggled)
19 | self.ui.languageTxt.hide() # to be removed later
20 | self.ui.languageLbl.hide() # to be removed later
21 | self.ui.languageTxt.textEdited.connect(self.on_languageTxt_textEdited)
22 | self.ui.spoofedModelDrp.activated.connect(self.on_spoofedModelDrp_activated)
23 | self.ui.spoofHardwareChk.toggled.connect(self.on_spoofHardwareChk_toggled)
24 | self.ui.spoofCPUChk.toggled.connect(self.on_spoofCPUChk_toggled)
25 |
26 | self.ui.aiEnablerContent.hide()
27 |
28 | ## ACTIONS
29 | def on_euEnablerEnabledChk_toggled(self, checked: bool):
30 | tweaks["EUEnabler"].set_enabled(checked)
31 | def on_methodChoiceDrp_activated(self, index: int):
32 | tweaks["EUEnabler"].set_selected_option(index)
33 | def on_regionCodeTxt_textEdited(self, text: str):
34 | tweaks["EUEnabler"].set_region_code(text)
35 |
36 | def on_enableAIChk_toggled(self, checked: bool):
37 | # tweaks["AIEligibility"].set_enabled(checked)
38 | tweaks["AIGestalt"].set_enabled(checked)
39 | # change the visibility of stuff
40 | if checked:
41 | self.ui.aiEnablerContent.show()
42 | else:
43 | self.ui.aiEnablerContent.hide()
44 |
45 | def on_eligFileChk_toggled(self, checked: bool):
46 | tweaks["AIEligibility"].set_enabled(checked)
47 | if checked:
48 | self.ui.languageTxt.show()
49 | self.ui.languageLbl.show()
50 | else:
51 | self.ui.languageTxt.hide()
52 | self.ui.languageLbl.hide()
53 |
54 | def on_languageTxt_textEdited(self, text: str):
55 | tweaks["AIEligibility"].set_language_code(text)
56 |
57 | def on_spoofedModelDrp_activated(self, index: int):
58 | idx_to_apply = index
59 | if not self.window.device_manager.show_all_spoofable_models and not self.window.device_manager.get_current_device_model().startswith("iPhone"):
60 | # offset the index for ipads
61 | idx_to_apply += 6
62 | tweaks["SpoofModel"].set_selected_option(idx_to_apply, is_enabled=(index != 0))
63 | tweaks["SpoofHardware"].set_selected_option(idx_to_apply, is_enabled=(index != 0 and self.ui.spoofHardwareChk.isChecked()))
64 | tweaks["SpoofCPU"].set_selected_option(idx_to_apply, is_enabled=(index != 0 and self.ui.spoofCPUChk.isChecked()))
65 | def on_spoofHardwareChk_toggled(self, checked: bool):
66 | tweaks["SpoofHardware"].set_enabled(checked and tweaks["SpoofHardware"].selected_option != 0)
67 | def on_spoofCPUChk_toggled(self, checked: bool):
68 | tweaks["SpoofCPU"].set_enabled(checked and tweaks["SpoofCPU"].selected_option != 0)
--------------------------------------------------------------------------------
/gui/pages/tools/featureflags.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 |
6 | class FeatureFlagsPage(Page):
7 | def __init__(self, ui: Ui_Nugget):
8 | super().__init__()
9 | self.ui = ui
10 |
11 | def load_page(self):
12 | self.ui.clockAnimChk.toggled.connect(self.on_clockAnimChk_toggled)
13 | self.ui.lockscreenChk.toggled.connect(self.on_lockscreenChk_clicked)
14 | self.ui.photosChk.toggled.connect(self.on_photosChk_clicked)
15 | self.ui.aiChk.toggled.connect(self.on_aiChk_clicked)
16 |
17 | ## ACTIONS
18 | def on_clockAnimChk_toggled(self, checked: bool):
19 | tweaks["ClockAnim"].set_enabled(checked)
20 | def on_lockscreenChk_clicked(self, checked: bool):
21 | tweaks["Lockscreen"].set_enabled(checked)
22 |
23 | def on_photosChk_clicked(self, checked: bool):
24 | tweaks["PhotoUI"].set_enabled(checked)
25 | def on_aiChk_clicked(self, checked: bool):
26 | tweaks["AI"].set_enabled(checked)
--------------------------------------------------------------------------------
/gui/pages/tools/gestalt.py:
--------------------------------------------------------------------------------
1 | from PySide6 import QtCore, QtWidgets, QtGui
2 |
3 | from ..page import Page
4 | from qt.ui_mainwindow import Ui_Nugget
5 |
6 | from tweaks.tweaks import tweaks
7 | from tweaks.custom_gestalt_tweaks import CustomGestaltTweaks, ValueTypeStrings
8 |
9 | class GestaltPage(Page):
10 | def __init__(self, window, ui: Ui_Nugget):
11 | super().__init__()
12 | self.window = window
13 | self.ui = ui
14 |
15 | def load_page(self):
16 | self.ui.dynamicIslandDrp.activated.connect(self.on_dynamicIslandDrp_activated)
17 | self.ui.rdarFixChk.clicked.connect(self.on_rdarFixChk_clicked)
18 | self.ui.modelNameChk.toggled.connect(self.on_modelNameChk_clicked)
19 | self.ui.modelNameTxt.textEdited.connect(self.on_modelNameTxt_textEdited)
20 |
21 | self.ui.bootChimeChk.clicked.connect(self.on_bootChimeChk_clicked)
22 | self.ui.chargeLimitChk.clicked.connect(self.on_chargeLimitChk_clicked)
23 | self.ui.tapToWakeChk.clicked.connect(self.on_tapToWakeChk_clicked)
24 | self.ui.iphone16SettingsChk.clicked.connect(self.on_iphone16SettingsChk_clicked)
25 | self.ui.parallaxChk.clicked.connect(self.on_parallaxChk_clicked)
26 | self.ui.stageManagerChk.clicked.connect(self.on_stageManagerChk_clicked)
27 | self.ui.enableMedusaChk.clicked.connect(self.on_enableMedusaChk_clicked)
28 | self.ui.ipadAppsChk.clicked.connect(self.on_ipadAppsChk_clicked)
29 | self.ui.shutterChk.clicked.connect(self.on_shutterChk_clicked)
30 | self.ui.findMyFriendsChk.clicked.connect(self.on_findMyFriendsChk_clicked)
31 | self.ui.pencilChk.clicked.connect(self.on_pencilChk_clicked)
32 | self.ui.actionButtonChk.clicked.connect(self.on_actionButtonChk_clicked)
33 |
34 | self.ui.internalInstallChk.clicked.connect(self.on_internalInstallChk_clicked)
35 | self.ui.internalStorageChk.clicked.connect(self.on_internalStorageChk_clicked)
36 | self.ui.collisionSOSChk.clicked.connect(self.on_collisionSOSChk_clicked)
37 | self.ui.aodChk.clicked.connect(self.on_aodChk_clicked)
38 | self.ui.aodVibrancyChk.clicked.connect(self.on_aodVibrancyChk_clicked)
39 |
40 | self.ui.addGestaltKeyBtn.clicked.connect(self.on_addGestaltKeyBtn_clicked)
41 |
42 | def setup_spoofedModelDrp_models(self):
43 | # hide all the models first
44 | for i in range(1, self.ui.spoofedModelDrp.count()):
45 | try:
46 | self.ui.spoofedModelDrp.removeItem(1)
47 | except:
48 | pass
49 | # indexes 1-6 for iPhones, 7-(len(values) - 1) for iPads
50 | # TODO: Make this get fetched from the gui on app startup
51 | spoof_drp_options = ["iPhone 15 Pro (iPhone16,1)", "iPhone 15 Pro Max (iPhone16,2)", "iPhone 16 (iPhone17,3)", "iPhone 16 Plus (iPhone17,4)", "iPhone 16 Pro (iPhone17,1)", "iPhone 16 Pro Max (iPhone17,2)", "iPad Mini (A17 Pro) (W) (iPad16,1)", "iPad Mini (A17 Pro) (C) (iPad16,2)", "iPad Pro (13-inch) (M4) (W) (iPad16,5)", "iPad Pro (13-inch) (M4) (C) (iPad16,6)", "iPad Pro (11-inch) (M4) (W) (iPad16,3)", "iPad Pro (11-inch) (M4) (C) (iPad16,4)", "iPad Pro (12.9-inch) (M2) (W) (iPad14,5)", "iPad Pro (12.9-inch) (M2) (C) (iPad14,6)", "iPad Pro (11-inch) (M2) (W) (iPad14,3)", "iPad Pro (11-inch) (M2) (C) (iPad14,4)", "iPad Air (13-inch) (M2) (W) (iPad14,10)", "iPad Air (13-inch) (M2) (C) (iPad14,11)", "iPad Air (11-inch) (M2) (W) (iPad14,8)", "iPad Air (11-inch) (M2) (C) (iPad14,9)", "iPad Pro (11-inch) (M1) (W) (iPad13,4)", "iPad Pro (11-inch) (M1) (C) (iPad13,5)", "iPad Pro (12.9-inch) (M1) (W) (iPad13,8)", "iPad Pro (12.9-inch) (M1) (C) (iPad13,9)", "iPad Air (M1) (W) (iPad13,16)", "iPad Air (M1) (C) (iPad13,17)"]
52 | if self.window.device_manager.show_all_spoofable_models or self.window.device_manager.get_current_device_model().startswith("iPhone"):
53 | # re-enable iPhone spoof models
54 | self.ui.spoofedModelDrp.addItems(spoof_drp_options[:6])
55 | if self.window.device_manager.show_all_spoofable_models or self.window.device_manager.get_current_device_model().startswith("iPad"):
56 | # re-enable iPad spoof models
57 | self.ui.spoofedModelDrp.addItems(spoof_drp_options[6:])
58 |
59 | ## ACTIONS
60 | def set_rdar_fix_label(self):
61 | rdar_title = tweaks["RdarFix"].get_rdar_title()
62 | if rdar_title == "hide":
63 | self.ui.rdarFixChk.hide()
64 | else:
65 | self.ui.rdarFixChk.show()
66 | self.ui.rdarFixChk.setText(f"{rdar_title} (modifies resolution)")
67 |
68 | def on_dynamicIslandDrp_activated(self, index: int):
69 | if index == 0:
70 | tweaks["DynamicIsland"].set_enabled(False)
71 | tweaks["RdarFix"].set_di_type(-1)
72 | else:
73 | # disable X gestures on devices other than iPhone SEs
74 | # the lazy way, better option would be to remove it from the menu but I didn't want to rework all that
75 | model = self.window.device_manager.get_current_device_model()
76 | if index != 1 or (model == "iPhone12,8" or model == "iPhone14,6"):
77 | tweaks["DynamicIsland"].set_selected_option(index - 1)
78 | else:
79 | tweaks["DynamicIsland"].set_enabled(False)
80 | tweaks["RdarFix"].set_di_type(tweaks["DynamicIsland"].value[tweaks["DynamicIsland"].get_selected_option()])
81 | self.set_rdar_fix_label()
82 | def on_rdarFixChk_clicked(self, checked: bool):
83 | tweaks["RdarFix"].set_enabled(checked)
84 |
85 | def on_modelNameChk_clicked(self, checked: bool):
86 | tweaks["ModelName"].set_enabled(checked)
87 | def on_modelNameTxt_textEdited(self, text: str):
88 | tweaks["ModelName"].set_value(text, toggle_enabled=False)
89 |
90 | def on_bootChimeChk_clicked(self, checked: bool):
91 | tweaks["BootChime"].set_enabled(checked)
92 | def on_chargeLimitChk_clicked(self, checked: bool):
93 | tweaks["ChargeLimit"].set_enabled(checked)
94 | def on_tapToWakeChk_clicked(self, checked: bool):
95 | tweaks["TapToWake"].set_enabled(checked)
96 | def on_iphone16SettingsChk_clicked(self, checked: bool):
97 | tweaks["CameraButton"].set_enabled(checked)
98 | def on_parallaxChk_clicked(self, checked: bool):
99 | tweaks["Parallax"].set_enabled(checked)
100 |
101 | def on_stageManagerChk_clicked(self, checked: bool):
102 | tweaks["StageManager"].set_enabled(checked)
103 | def on_enableMedusaChk_clicked(self, checked: bool):
104 | tweaks["Medusa"].set_enabled(checked)
105 | def on_ipadAppsChk_clicked(self, checked: bool):
106 | tweaks["iPadApps"].set_enabled(checked)
107 | def on_shutterChk_clicked(self, checked: bool):
108 | # TODO: allow the user to select the region
109 | tweaks["Shutter"].set_enabled(checked)
110 | def on_findMyFriendsChk_clicked(self, checked: bool):
111 | tweaks["FindMyFriends"].set_enabled(checked)
112 | def on_pencilChk_clicked(self, checked: bool):
113 | tweaks["Pencil"].set_enabled(checked)
114 | def on_actionButtonChk_clicked(self, checked: bool):
115 | tweaks["ActionButton"].set_enabled(checked)
116 |
117 | def on_internalInstallChk_clicked(self, checked: bool):
118 | tweaks["InternalInstall"].set_enabled(checked)
119 | def on_internalStorageChk_clicked(self, checked: bool):
120 | tweaks["InternalStorage"].set_enabled(checked)
121 |
122 | def on_collisionSOSChk_clicked(self, checked: bool):
123 | tweaks["CollisionSOS"].set_enabled(checked)
124 | def on_aodChk_clicked(self, checked: bool):
125 | tweaks["AOD"].set_enabled(checked)
126 | def on_aodVibrancyChk_clicked(self, checked: bool):
127 | tweaks["AODVibrancy"].set_enabled(checked)
128 |
129 | def update_custom_gestalt_value_type(self, id, idx, valueField: QtWidgets.QLineEdit):
130 | new_str = CustomGestaltTweaks.set_tweak_value_type(id, idx)
131 | # update the value
132 | valueField.setText(new_str)
133 |
134 | def delete_custom_gestalt_key(self, id: int, widget: QtWidgets.QWidget):
135 | CustomGestaltTweaks.deactivate_tweak(id)
136 | self.ui.customKeysLayout.removeWidget(widget)
137 | widget.setParent(None)
138 |
139 | def on_addGestaltKeyBtn_clicked(self):
140 | # create a blank gestalt value with default value of 1
141 | key_identifier = CustomGestaltTweaks.create_tweak()
142 |
143 | widget = QtWidgets.QWidget()
144 | widget.setFixedHeight(35)
145 | widget.setStyleSheet("QWidget { background: none; border: 1px solid #3b3b3b; border-radius: 8px; }")
146 | hlayout = QtWidgets.QHBoxLayout(widget)
147 | hlayout.setContentsMargins(9, 2, 9, 2)
148 |
149 | # create the key field
150 | keyField = QtWidgets.QLineEdit(widget)
151 | # keyField.setMaximumWidth(200)
152 | keyField.setPlaceholderText("Key")
153 | keyField.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
154 | keyField.setTextMargins(5, 0, 5, 0)
155 | keyField.textEdited.connect(lambda txt, id=key_identifier: CustomGestaltTweaks.set_tweak_key(id, txt))
156 | hlayout.addWidget(keyField)
157 |
158 | # create the delete button
159 | delBtn = QtWidgets.QToolButton(widget)
160 | delBtn.setIcon(QtGui.QIcon(":/icon/trash.svg"))
161 | delBtn.setStyleSheet("QToolButton { margin-right: 8px; background: none; border: none; }\nQToolButton:pressed { background: none; color: #FFFFFF; }")
162 | delBtn.clicked.connect(lambda _, id=key_identifier, w=widget: self.delete_custom_gestalt_key(id, w))
163 | hlayout.addWidget(delBtn)
164 |
165 | # create the type dropdown
166 | valueTypeDrp = QtWidgets.QComboBox(widget)
167 | valueTypeDrp.setStyleSheet("QComboBox {\n background-color: #3b3b3b;\n border: none;\n color: #e8e8e8;\n font-size: 14px;\n padding-left: 8px;\n border-radius: 8px;\n}\n\nQComboBox::drop-down {\n image: url(:/icon/caret-down-fill.svg);\n icon-size: 16px;\n subcontrol-position: right center;\n margin-right: 8px;\n}\n\nQComboBox QAbstractItemView {\n background-color: #3b3b3b;\n outline: none;\n margin-top: 1px;\n}\n\nQComboBox QAbstractItemView::item {\n background-color: #3b3b3b;\n color: #e8e8e8;\n padding-left: 8px;\n}\n\nQComboBox QAbstractItemView::item:hover {\n background-color: #535353;\n color: #ffffff;\n}")
168 | valueTypeDrp.setFixedWidth(120)
169 | valueTypeDrp.addItems(ValueTypeStrings)
170 | valueTypeDrp.setCurrentIndex(0)
171 |
172 | # create the value edit field
173 | valueField = QtWidgets.QLineEdit(widget)
174 | valueField.setMaximumWidth(175)
175 | valueField.setPlaceholderText("Value")
176 | valueField.setText("1")
177 | valueField.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight)
178 | valueField.setTextMargins(5, 0, 5, 0)
179 | valueField.textEdited.connect(lambda txt, id=key_identifier: CustomGestaltTweaks.set_tweak_value(id, txt))
180 |
181 | valueTypeDrp.activated.connect(lambda idx, id=key_identifier, vf=valueField: self.update_custom_gestalt_value_type(id, idx, vf))
182 | hlayout.addWidget(valueTypeDrp)
183 | hlayout.addWidget(valueField)
184 |
185 | # add it to the main widget
186 | widget.setDisabled(False)
187 | self.ui.customKeysLayout.addWidget(widget)
--------------------------------------------------------------------------------
/gui/pages/tools/internal.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 |
6 | class InternalPage(Page):
7 | def __init__(self, ui: Ui_Nugget):
8 | super().__init__()
9 | self.ui = ui
10 |
11 | def load_page(self):
12 | self.ui.buildVersionChk.toggled.connect(self.on_buildVersionChk_clicked)
13 | self.ui.RTLChk.toggled.connect(self.on_RTLChk_clicked)
14 | self.ui.metalHUDChk.toggled.connect(self.on_metalHUDChk_clicked)
15 | self.ui.iMessageChk.toggled.connect(self.on_iMessageChk_clicked)
16 | self.ui.IDSChk.toggled.connect(self.on_IDSChk_clicked)
17 | self.ui.VCChk.toggled.connect(self.on_VCChk_clicked)
18 | self.ui.appStoreChk.toggled.connect(self.on_appStoreChk_clicked)
19 | self.ui.notesChk.toggled.connect(self.on_notesChk_clicked)
20 | self.ui.showTouchesChk.toggled.connect(self.on_showTouchesChk_clicked)
21 | self.ui.hideRespringChk.toggled.connect(self.on_hideRespringChk_clicked)
22 | self.ui.enableWakeVibrateChk.toggled.connect(self.on_enableWakeVibrateChk_clicked)
23 | self.ui.pasteSoundChk.toggled.connect(self.on_pasteSoundChk_clicked)
24 | self.ui.notifyPastesChk.toggled.connect(self.on_notifyPastesChk_clicked)
25 |
26 | ## ACTIONS
27 | def on_buildVersionChk_clicked(self, checked: bool):
28 | tweaks["SBBuildNumber"].set_enabled(checked)
29 | def on_RTLChk_clicked(self, checked: bool):
30 | tweaks["RTL"].set_enabled(checked)
31 | def on_metalHUDChk_clicked(self, checked: bool):
32 | tweaks["MetalForceHudEnabled"].set_enabled(checked)
33 | def on_iMessageChk_clicked(self, checked: bool):
34 | tweaks["iMessageDiagnosticsEnabled"].set_enabled(checked)
35 | def on_IDSChk_clicked(self, checked: bool):
36 | tweaks["IDSDiagnosticsEnabled"].set_enabled(checked)
37 | def on_VCChk_clicked(self, checked: bool):
38 | tweaks["VCDiagnosticsEnabled"].set_enabled(checked)
39 |
40 | def on_appStoreChk_clicked(self, checked: bool):
41 | tweaks["AppStoreDebug"].set_enabled(checked)
42 | def on_notesChk_clicked(self, checked: bool):
43 | tweaks["NotesDebugMode"].set_enabled(checked)
44 |
45 | def on_showTouchesChk_clicked(self, checked: bool):
46 | tweaks["BKDigitizerVisualizeTouches"].set_enabled(checked)
47 | def on_hideRespringChk_clicked(self, checked: bool):
48 | tweaks["BKHideAppleLogoOnLaunch"].set_enabled(checked)
49 | def on_enableWakeVibrateChk_clicked(self, checked: bool):
50 | tweaks["EnableWakeGestureHaptic"].set_enabled(checked)
51 | def on_pasteSoundChk_clicked(self, checked: bool):
52 | tweaks["PlaySoundOnPaste"].set_enabled(checked)
53 | def on_notifyPastesChk_clicked(self, checked: bool):
54 | tweaks["AnnounceAllPastes"].set_enabled(checked)
--------------------------------------------------------------------------------
/gui/pages/tools/risky.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 |
6 | class RiskyPage(Page):
7 | def __init__(self, ui: Ui_Nugget):
8 | super().__init__()
9 | self.ui = ui
10 |
11 | def load_page(self):
12 | self.ui.disableOTAChk.toggled.connect(self.on_disableOTAChk_clicked)
13 | self.ui.enableResolutionChk.toggled.connect(self.on_enableResolutionChk_clicked)
14 | self.ui.resHeightTxt.textEdited.connect(self.on_resHeightTxt_textEdited)
15 | self.ui.resWidthTxt.textEdited.connect(self.on_resWidthTxt_textEdited)
16 | self.ui.resChangerContent.hide()
17 | self.ui.resHeightWarningLbl.hide()
18 | self.ui.resWidthWarningLbl.hide()
19 |
20 | ## ACTIONS
21 | def on_disableOTAChk_clicked(self, checked: bool):
22 | tweaks["DisableOTAFile"].set_enabled(checked)
23 |
24 | def on_enableResolutionChk_clicked(self, checked: bool):
25 | tweaks["CustomResolution"].set_enabled(checked)
26 | # toggle the ui content
27 | if checked:
28 | self.ui.resChangerContent.show()
29 | else:
30 | self.ui.resChangerContent.hide()
31 | def on_resHeightTxt_textEdited(self, txt: str):
32 | if txt == "":
33 | # remove the canvas_height value
34 | tweaks["CustomResolution"].value.pop("canvas_height", None)
35 | self.ui.resHeightWarningLbl.hide()
36 | return
37 | try:
38 | val = int(txt)
39 | tweaks["CustomResolution"].value["canvas_height"] = val
40 | self.ui.resHeightWarningLbl.hide()
41 | except:
42 | self.ui.resHeightWarningLbl.show()
43 | def on_resWidthTxt_textEdited(self, txt: str):
44 | if txt == "":
45 | # remove the canvas_width value
46 | tweaks["CustomResolution"].value.pop("canvas_width", None)
47 | self.ui.resWidthWarningLbl.hide()
48 | return
49 | try:
50 | val = int(txt)
51 | tweaks["CustomResolution"].value["canvas_width"] = val
52 | self.ui.resWidthWarningLbl.hide()
53 | except:
54 | self.ui.resWidthWarningLbl.show()
--------------------------------------------------------------------------------
/gui/pages/tools/springboard.py:
--------------------------------------------------------------------------------
1 | from ..page import Page
2 | from qt.ui_mainwindow import Ui_Nugget
3 |
4 | from tweaks.tweaks import tweaks
5 |
6 | class SpringboardPage(Page):
7 | def __init__(self, ui: Ui_Nugget):
8 | super().__init__()
9 | self.ui = ui
10 |
11 | def load_page(self):
12 | self.ui.footnoteTxt.textEdited.connect(self.on_footnoteTxt_textEdited)
13 | self.ui.disableLockRespringChk.toggled.connect(self.on_disableLockRespringChk_clicked)
14 | self.ui.disableDimmingChk.toggled.connect(self.on_disableDimmingChk_clicked)
15 | self.ui.disableBatteryAlertsChk.toggled.connect(self.on_disableBatteryAlertsChk_clicked)
16 | self.ui.disableCrumbChk.toggled.connect(self.on_disableCrumbChk_clicked)
17 | self.ui.enableSupervisionTextChk.toggled.connect(self.on_enableSupervisionTextChk_clicked)
18 | self.ui.enableAirPlayChk.toggled.connect(self.on_enableAirPlayChk_clicked)
19 |
20 | ## ACTIONS
21 | def on_footnoteTxt_textEdited(self, text: str):
22 | tweaks["LockScreenFootnote"].set_value(text, toggle_enabled=True)
23 | def on_disableLockRespringChk_clicked(self, checked: bool):
24 | tweaks["SBDontLockAfterCrash"].set_enabled(checked)
25 | def on_disableDimmingChk_clicked(self, checked: bool):
26 | tweaks["SBDontDimOrLockOnAC"].set_enabled(checked)
27 | def on_disableBatteryAlertsChk_clicked(self, checked: bool):
28 | tweaks["SBHideLowPowerAlerts"].set_enabled(checked)
29 | def on_disableCrumbChk_clicked(self, checked: bool):
30 | tweaks["SBNeverBreadcrumb"].set_enabled(checked)
31 | def on_enableSupervisionTextChk_clicked(self, checked: bool):
32 | tweaks["SBShowSupervisionTextOnLockScreen"].set_enabled(checked)
33 | def on_enableAirPlayChk_clicked(self, checked: bool):
34 | tweaks["AirplaySupport"].set_enabled(checked)
--------------------------------------------------------------------------------
/gui/pb_tutorial1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/pb_tutorial1.png
--------------------------------------------------------------------------------
/gui/pb_tutorial2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/pb_tutorial2.png
--------------------------------------------------------------------------------
/gui/transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/gui/transparent.png
--------------------------------------------------------------------------------
/icon/app-indicator.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/arrow-clockwise.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/arrow.clockwise.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/brush.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/caret-down-fill.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/icon/check-circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/chevron.down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/chevron.up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/compass.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/currency-dollar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/export.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
13 |
--------------------------------------------------------------------------------
/icon/file-earmark-zip.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/flag.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/folder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/gear.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/geo-alt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/globe.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/hdd.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/heart-fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/house.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/import.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
13 |
--------------------------------------------------------------------------------
/icon/iphone-island.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
13 |
--------------------------------------------------------------------------------
/icon/pencil.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
13 |
--------------------------------------------------------------------------------
/icon/phone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/photo-stack.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
15 |
--------------------------------------------------------------------------------
/icon/photo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
13 |
--------------------------------------------------------------------------------
/icon/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/questionmark.circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
13 |
--------------------------------------------------------------------------------
/icon/shippingbox.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
12 |
--------------------------------------------------------------------------------
/icon/star.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/toggles.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/wallpaper.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/icon/wifi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icon/x-lg.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/main_app.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from PySide6 import QtGui, QtWidgets
3 |
4 | from gui.main_window import MainWindow
5 | from devicemanagement.device_manager import DeviceManager
6 | from tweaks.tweaks import tweaks
7 |
8 | if __name__ == "__main__":
9 | app = QtWidgets.QApplication([])
10 | dm = DeviceManager()
11 |
12 | # set icon
13 | icon = QtGui.QIcon("nugget.ico")
14 | app.setWindowIcon(icon)
15 |
16 | widget = MainWindow(device_manager=dm)
17 | widget.resize(800, 600)
18 | widget.show()
19 |
20 | # import files from args
21 | for arg in sys.argv:
22 | if arg.endswith('.tendies'):
23 | # add tendies file
24 | tweaks["PosterBoard"].add_tendie(arg)
25 | elif arg.endswith('.batter'):
26 | # add batter file
27 | tweaks["PosterBoard"].add_template(arg)
28 |
29 | sys.exit(app.exec())
--------------------------------------------------------------------------------
/nugget.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/nugget.ico
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pymobiledevice3
2 | PySide6-Essentials
3 | PyInstaller
4 | ffmpeg
5 | ffmpeg-python
6 | opencv-python
7 |
--------------------------------------------------------------------------------
/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | credits/LeminLimez.png
4 | icon/app-indicator.svg
5 | icon/arrow-clockwise.svg
6 | icon/brush.svg
7 | icon/caret-down-fill.svg
8 | icon/check-circle.svg
9 | icon/compass.svg
10 | icon/gear.svg
11 | icon/hdd.svg
12 | icon/house.svg
13 | icon/phone.svg
14 | icon/toggles.svg
15 | icon/wifi.svg
16 | icon/x-lg.svg
17 | icon/discord.svg
18 | icon/heart-fill.svg
19 | icon/github.svg
20 | icon/twitter.svg
21 | icon/file-earmark-zip.svg
22 | icon/folder.svg
23 | icon/trash.svg
24 | icon/currency-dollar.svg
25 | icon/geo-alt.svg
26 | icon/star.svg
27 | icon/pencil.svg
28 | icon/import.svg
29 | icon/plus.svg
30 | icon/iphone-island.svg
31 | icon/flag.svg
32 | credits/big_nugget.png
33 | icon/photo-stack.svg
34 | icon/photo.svg
35 | icon/shippingbox.svg
36 | icon/wallpaper.svg
37 | icon/questionmark.circle.svg
38 | icon/globe.svg
39 | gui/pb_tutorial1.png
40 | gui/pb_tutorial2.png
41 | icon/arrow.clockwise.svg
42 | icon/chevron.down.svg
43 | icon/chevron.up.svg
44 | gui/transparent.png
45 | icon/export.svg
46 |
47 |
48 |
--------------------------------------------------------------------------------
/restore/__init__.py:
--------------------------------------------------------------------------------
1 | from tempfile import TemporaryDirectory
2 | from pathlib import Path
3 |
4 | from pymobiledevice3.lockdown import create_using_usbmux
5 | from pymobiledevice3.services.mobilebackup2 import Mobilebackup2Service
6 | from pymobiledevice3.exceptions import PyMobileDevice3Exception
7 | from pymobiledevice3.services.diagnostics import DiagnosticsService
8 | from pymobiledevice3.lockdown import LockdownClient
9 |
10 | from . import backup
11 |
12 | def reboot_device(reboot: bool = False, lockdown_client: LockdownClient = None):
13 | if reboot and lockdown_client != None:
14 | print("Success! Rebooting your device...")
15 | with DiagnosticsService(lockdown_client) as diagnostics_service:
16 | diagnostics_service.restart()
17 | print("Remember to turn Find My back on!")
18 |
19 | def perform_restore(backup: backup.Backup, reboot: bool = False, lockdown_client: LockdownClient = None, progress_callback = lambda x: None):
20 | try:
21 | with TemporaryDirectory() as backup_dir:
22 | backup.write_to_directory(Path(backup_dir))
23 |
24 | if lockdown_client == None:
25 | lockdown_client = create_using_usbmux()
26 | with Mobilebackup2Service(lockdown_client) as mb:
27 | mb.restore(backup_dir, system=True, reboot=False, copy=False, source=".", progress_callback=progress_callback, skip_apps=True)
28 | # reboot the device
29 | reboot_device(reboot, lockdown_client)
30 | except PyMobileDevice3Exception as e:
31 | if "Find My" in str(e):
32 | print("Find My must be disabled in order to use this tool.")
33 | print("Disable Find My from Settings (Settings -> [Your Name] -> Find My) and then try again.")
34 | raise e
35 | elif "crash_on_purpose" not in str(e):
36 | raise e
37 | else:
38 | reboot_device(reboot, lockdown_client)
--------------------------------------------------------------------------------
/restore/backup.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from datetime import datetime
3 | import plistlib
4 | from pathlib import Path
5 | from base64 import b64decode
6 | from hashlib import sha1
7 | from . import mbdb
8 | from .mbdb import _FileMode
9 | from random import randbytes
10 | from typing import Optional
11 |
12 | # Default nugget file right
13 | # RWX:RX:RX
14 | DEFAULT = _FileMode.S_IRUSR | _FileMode.S_IWUSR | _FileMode.S_IXUSR | _FileMode.S_IRGRP | _FileMode.S_IXGRP | _FileMode.S_IROTH | _FileMode.S_IXOTH
15 |
16 | @dataclass
17 | class BackupFile:
18 | path: str
19 | domain: str
20 |
21 | def to_record(self) -> mbdb.MbdbRecord:
22 | raise NotImplementedError()
23 |
24 | @dataclass
25 | class ConcreteFile(BackupFile):
26 | contents: bytes
27 | src_path: Optional[str] = None
28 | owner: int = 0
29 | group: int = 0
30 | inode: Optional[int] = None
31 | mode: _FileMode = DEFAULT
32 |
33 | hash: bytes = None
34 | size: int = None
35 |
36 | def read_contents(self) -> bytes:
37 | contents = self.contents
38 | if self.contents == None:
39 | with open(self.src_path, "rb") as in_file:
40 | contents = in_file.read()
41 | # prepopulate hash and size
42 | self.hash = sha1(contents).digest()
43 | self.size = len(contents)
44 | return contents
45 |
46 | def to_record(self) -> mbdb.MbdbRecord:
47 | if self.inode is None:
48 | self.inode = int.from_bytes(randbytes(8), "big")
49 | if self.hash == None or self.size == None:
50 | self.read_contents()
51 | return mbdb.MbdbRecord(
52 | domain=self.domain,
53 | filename=self.path,
54 | link="",
55 | hash=self.hash,
56 | key=b"",
57 | mode=self.mode | _FileMode.S_IFREG,
58 | #unknown2=0,
59 | #unknown3=0,
60 | inode=self.inode,
61 | user_id=self.owner,
62 | group_id=self.group,
63 | mtime=int(datetime.now().timestamp()),
64 | atime=int(datetime.now().timestamp()),
65 | ctime=int(datetime.now().timestamp()),
66 | size=self.size,
67 | flags=4,
68 | properties=[]
69 | )
70 |
71 | @dataclass
72 | class Directory(BackupFile):
73 | owner: int = 0
74 | group: int = 0
75 | mode: _FileMode = DEFAULT
76 |
77 | def to_record(self) -> mbdb.MbdbRecord:
78 | return mbdb.MbdbRecord(
79 | domain=self.domain,
80 | filename=self.path,
81 | link="",
82 | hash=b"",
83 | key=b"",
84 | mode=self.mode | _FileMode.S_IFDIR,
85 | #unknown2=0,
86 | #unknown3=0,
87 | inode=0, # inode is not respected for directories
88 | user_id=self.owner,
89 | group_id=self.group,
90 | mtime=int(datetime.now().timestamp()),
91 | atime=int(datetime.now().timestamp()),
92 | ctime=int(datetime.now().timestamp()),
93 | size=0,
94 | flags=4,
95 | properties=[]
96 | )
97 |
98 | @dataclass
99 | class SymbolicLink(BackupFile):
100 | target: str
101 | owner: int = 0
102 | group: int = 0
103 | inode: Optional[int] = None
104 | mode: _FileMode = DEFAULT
105 |
106 | def to_record(self) -> mbdb.MbdbRecord:
107 | if self.inode is None:
108 | self.inode = int.from_bytes(randbytes(8), "big")
109 | return mbdb.MbdbRecord(
110 | domain=self.domain,
111 | filename=self.path,
112 | link=self.target,
113 | hash=b"",
114 | key=b"",
115 | mode=self.mode | _FileMode.S_IFLNK,
116 | #unknown2=0,
117 | #unknown3=0,
118 | inode=self.inode,
119 | user_id=self.owner,
120 | group_id=self.group,
121 | mtime=int(datetime.now().timestamp()),
122 | atime=int(datetime.now().timestamp()),
123 | ctime=int(datetime.now().timestamp()),
124 | size=0,
125 | flags=4,
126 | properties=[]
127 | )
128 |
129 | @dataclass
130 | class AppBundle:
131 | identifier: str
132 | path: str
133 | container_content_class: str
134 | version: str = 804
135 |
136 | @dataclass
137 | class Backup:
138 | files: list[BackupFile]
139 | apps: list[AppBundle]
140 |
141 | def write_to_directory(self, directory: Path):
142 | for file in self.files:
143 | if isinstance(file, ConcreteFile):
144 | #print("Writing", file.path, "to", directory / sha1((file.domain + "-" + file.path).encode()).digest().hex())
145 | with open(directory / sha1((file.domain + "-" + file.path).encode()).digest().hex(), "wb") as f:
146 | f.write(file.read_contents())
147 |
148 | with open(directory / "Manifest.mbdb", "wb") as f:
149 | f.write(self.generate_manifest_db().to_bytes())
150 |
151 | with open(directory / "Status.plist", "wb") as f:
152 | f.write(self.generate_status())
153 |
154 | with open(directory / "Manifest.plist", "wb") as f:
155 | f.write(self.generate_manifest())
156 |
157 | with open(directory / "Info.plist", "wb") as f:
158 | f.write(plistlib.dumps({}))
159 |
160 |
161 | def generate_manifest_db(self): # Manifest.mbdb
162 | records = []
163 | for file in self.files:
164 | records.append(file.to_record())
165 | return mbdb.Mbdb(records=records)
166 |
167 | def generate_status(self) -> bytes: # Status.plist
168 | return plistlib.dumps({
169 | "BackupState": "new",
170 | "Date": datetime.fromisoformat("1970-01-01T00:00:00+00:00"),
171 | "IsFullBackup": False,
172 | "SnapshotState": "finished",
173 | "UUID": "00000000-0000-0000-0000-000000000000",
174 | "Version": "2.4"
175 | })
176 |
177 | def generate_manifest(self) -> bytes: # Manifest.plist
178 | plist = {
179 | "BackupKeyBag": b64decode("""
180 | VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABDud41d1b9NBICR1BH9JfVtSE1D
181 | SwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV1JBUAAA
182 | AAQAAAAAU0FMVAAAABRY5Ne2bthGQ5rf4O3gikep1e6tZUlURVIAAAAEAAAnEFVVSUQA
183 | AAAQB7R8awiGR9aba1UuVahGPENMQVMAAAAEAAAAAVdSQVAAAAAEAAAAAktUWVAAAAAE
184 | AAAAAFdQS1kAAAAoN3kQAJloFg+ukEUY+v5P+dhc/Welw/oucsyS40UBh67ZHef5ZMk9
185 | UVVVSUQAAAAQgd0cg0hSTgaxR3PVUbcEkUNMQVMAAAAEAAAAAldSQVAAAAAEAAAAAktU
186 | WVAAAAAEAAAAAFdQS1kAAAAoMiQTXx0SJlyrGJzdKZQ+SfL124w+2Tf/3d1R2i9yNj9z
187 | ZCHNJhnorVVVSUQAAAAQf7JFQiBOS12JDD7qwKNTSkNMQVMAAAAEAAAAA1dSQVAAAAAE
188 | AAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoSEelorROJA46ZUdwDHhMKiRguQyqHukotrxh
189 | jIfqiZ5ESBXX9txi51VVSUQAAAAQfF0G/837QLq01xH9+66vx0NMQVMAAAAEAAAABFdS
190 | QVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAol0BvFhd5bu4Hr75XqzNf4g0fMqZA
191 | ie6OxI+x/pgm6Y95XW17N+ZIDVVVSUQAAAAQimkT2dp1QeadMu1KhJKNTUNMQVMAAAAE
192 | AAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo2N2DZarQ6GPoWRgTiy/t
193 | djKArOqTaH0tPSG9KLbIjGTOcLodhx23xFVVSUQAAAAQQV37JVZHQFiKpoNiGmT6+ENM
194 | QVMAAAAEAAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAofe2QSvDC2cV7
195 | Etk4fSBbgqDx5ne/z1VHwmJ6NdVrTyWi80Sy869DM1VVSUQAAAAQFzkdH+VgSOmTj3yE
196 | cfWmMUNMQVMAAAAEAAAAB1dSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo7kLY
197 | PQ/DnHBERGpaz37eyntIX/XzovsS0mpHW3SoHvrb9RBgOB+WblVVSUQAAAAQEBpgKOz9
198 | Tni8F9kmSXd0sENMQVMAAAAEAAAACFdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kA
199 | AAAo5mxVoyNFgPMzphYhm1VG8Fhsin/xX+r6mCd9gByF5SxeolAIT/ICF1VVSUQAAAAQ
200 | rfKB2uPSQtWh82yx6w4BoUNMQVMAAAAEAAAACVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAA
201 | AFdQS1kAAAAo5iayZBwcRa1c1MMx7vh6lOYux3oDI/bdxFCW1WHCQR/Ub1MOv+QaYFVV
202 | SUQAAAAQiLXvK3qvQza/mea5inss/0NMQVMAAAAEAAAACldSQVAAAAAEAAAAA0tUWVAA
203 | AAAEAAAAAFdQS1kAAAAoD2wHX7KriEe1E31z7SQ7/+AVymcpARMYnQgegtZD0Mq2U55u
204 | xwNr2FVVSUQAAAAQ/Q9feZxLS++qSe/a4emRRENMQVMAAAAEAAAAC1dSQVAAAAAEAAAA
205 | A0tUWVAAAAAEAAAAAFdQS1kAAAAocYda2jyYzzSKggRPw/qgh6QPESlkZedgDUKpTr4Z
206 | Z8FDgd7YoALY1g=="""),
207 | "Lockdown": {},
208 | "SystemDomainsVersion": "20.0",
209 | "Version": "9.1"
210 | }
211 | # add the apps
212 | if len(self.apps) > 0:
213 | plist["Applications"] = {}
214 | for app in self.apps:
215 | appInfo = {
216 | "CFBundleIdentifier": app.identifier,
217 | "CFBundleVersion": app.version,
218 | "ContainerContentClass": app.container_content_class,
219 | "Path": app.path
220 | }
221 | plist["Applications"][app.identifier] = appInfo
222 | return plistlib.dumps(plist)
--------------------------------------------------------------------------------
/restore/mbdb.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from io import BytesIO
3 |
4 | # Mode bitfield
5 | from enum import IntFlag
6 | class _FileMode(IntFlag):
7 | S_IFMT = 0o0170000
8 | S_IFIFO = 0o0010000
9 | S_IFCHR = 0o0020000
10 | S_IFDIR = 0o0040000
11 | S_IFBLK = 0o0060000
12 | S_IFREG = 0o0100000
13 | S_IFLNK = 0o0120000
14 | S_IFSOCK = 0o0140000
15 |
16 | #S_IRWXU = 0o0000700
17 | S_IRUSR = 0o0000400
18 | S_IWUSR = 0o0000200
19 | S_IXUSR = 0o0000100
20 |
21 | #S_IRWXG = 0o0000070
22 | S_IRGRP = 0o0000040
23 | S_IWGRP = 0o0000020
24 | S_IXGRP = 0o0000010
25 |
26 | #S_IRWXO = 0o0000007
27 | S_IROTH = 0o0000004
28 | S_IWOTH = 0o0000002
29 | S_IXOTH = 0o0000001
30 |
31 | S_ISUID = 0o0004000
32 | S_ISGID = 0o0002000
33 | S_ISVTX = 0o0001000
34 |
35 | @dataclass
36 | class MbdbRecord:
37 | domain: str
38 | filename: str
39 | link: str
40 | hash: bytes
41 | key: bytes
42 | mode: _FileMode
43 | inode: int
44 | user_id: int
45 | group_id: int
46 | mtime: int
47 | atime: int
48 | ctime: int
49 | size: int
50 | flags: int
51 | properties: list
52 |
53 | @classmethod
54 | def from_stream(cls, d: BytesIO):
55 | #d = BytesIO(data)
56 |
57 | domain_len = int.from_bytes(d.read(2), "big")
58 | domain = d.read(domain_len).decode("utf-8")
59 |
60 | filename_len = int.from_bytes(d.read(2), "big")
61 | filename = d.read(filename_len).decode("utf-8")
62 |
63 | link_len = int.from_bytes(d.read(2), "big")
64 | link = d.read(link_len).decode("utf-8") if link_len != 0xffff else ""
65 |
66 | hash_len = int.from_bytes(d.read(2), "big")
67 | hash = d.read(hash_len) if hash_len != 0xffff else b""
68 |
69 | key_len = int.from_bytes(d.read(2), "big")
70 | key = d.read(key_len) if key_len != 0xffff else b""
71 |
72 | mode = _FileMode(int.from_bytes(d.read(2), "big"))
73 | #unknown2 = int.from_bytes(d.read(4), "big")
74 | #unknown3 = int.from_bytes(d.read(4), "big")
75 | inode = int.from_bytes(d.read(8), "big")
76 | user_id = int.from_bytes(d.read(4), "big")
77 | group_id = int.from_bytes(d.read(4), "big")
78 | mtime = int.from_bytes(d.read(4), "big")
79 | atime = int.from_bytes(d.read(4), "big")
80 | ctime = int.from_bytes(d.read(4), "big")
81 | size = int.from_bytes(d.read(8), "big")
82 | flags = int.from_bytes(d.read(1), "big")
83 |
84 | properties_count = int.from_bytes(d.read(1), "big")
85 | properties = []
86 |
87 | for _ in range(properties_count):
88 | name_len = int.from_bytes(d.read(2), "big")
89 | name = d.read(name_len).decode("utf-8") if name_len != 0xffff else ""
90 |
91 | value_len = int.from_bytes(d.read(2), "big")
92 | value = d.read(value_len).decode("utf-8") if value_len != 0xffff else ""
93 |
94 | properties.append((name, value))
95 |
96 | return cls(domain, filename, link, hash, key, mode, inode, user_id, group_id, mtime, atime, ctime, size, flags, properties)
97 |
98 | def to_bytes(self) -> bytes:
99 | d = BytesIO()
100 |
101 | d.write(len(self.domain).to_bytes(2, "big"))
102 | d.write(self.domain.encode("utf-8"))
103 |
104 | d.write(len(self.filename).to_bytes(2, "big"))
105 | d.write(self.filename.encode("utf-8"))
106 |
107 | d.write(len(self.link).to_bytes(2, "big"))
108 | d.write(self.link.encode("utf-8"))
109 |
110 | d.write(len(self.hash).to_bytes(2, "big"))
111 | d.write(self.hash)
112 |
113 | d.write(len(self.key).to_bytes(2, "big"))
114 | d.write(self.key)
115 |
116 | d.write(self.mode.to_bytes(2, "big"))
117 | #d.write(self.unknown2.to_bytes(4, "big"))
118 | #d.write(self.unknown3.to_bytes(4, "big"))
119 | d.write(self.inode.to_bytes(8, "big"))
120 | d.write(self.user_id.to_bytes(4, "big"))
121 | d.write(self.group_id.to_bytes(4, "big"))
122 | d.write(self.mtime.to_bytes(4, "big"))
123 | d.write(self.atime.to_bytes(4, "big"))
124 | d.write(self.ctime.to_bytes(4, "big"))
125 | d.write(self.size.to_bytes(8, "big"))
126 | d.write(self.flags.to_bytes(1, "big"))
127 |
128 | d.write(len(self.properties).to_bytes(1, "big"))
129 |
130 | for name, value in self.properties:
131 | d.write(len(name).to_bytes(2, "big"))
132 | d.write(name.encode("utf-8"))
133 |
134 | d.write(len(value).to_bytes(2, "big"))
135 | d.write(value.encode("utf-8"))
136 |
137 | return d.getvalue()
138 |
139 | @dataclass
140 | class Mbdb:
141 | records: list[MbdbRecord]
142 |
143 | @classmethod
144 | def from_bytes(cls, data: bytes):
145 | d = BytesIO(data)
146 |
147 | if d.read(4) != b"mbdb":
148 | raise ValueError("Invalid MBDB file")
149 |
150 | if d.read(2) != b"\x05\x00":
151 | raise ValueError("Invalid MBDB version")
152 |
153 | records = []
154 | while d.tell() < len(data):
155 | records.append(MbdbRecord.from_stream(d))
156 |
157 | return cls(records)
158 |
159 | def to_bytes(self) -> bytes:
160 | d = BytesIO()
161 |
162 | d.write(b"mbdb")
163 | d.write(b"\x05\x00")
164 |
165 | for record in self.records:
166 | d.write(record.to_bytes())
167 |
168 | return d.getvalue()
--------------------------------------------------------------------------------
/restore/restore.py:
--------------------------------------------------------------------------------
1 | from . import backup, perform_restore
2 | from .mbdb import _FileMode
3 | from pymobiledevice3.lockdown import LockdownClient
4 | from pymobiledevice3.services.installation_proxy import InstallationProxyService
5 | import os
6 |
7 | class FileToRestore:
8 | def __init__(self,
9 | contents: str, restore_path: str, contents_path: str = None, domain: str = "",
10 | owner: int = 501, group: int = 501, mode: _FileMode = None
11 | ):
12 | self.contents = contents
13 | self.contents_path = contents_path
14 | self.restore_path = restore_path
15 | self.domain = domain
16 | self.owner = owner
17 | self.group = group
18 | self.mode = mode
19 |
20 | def concat_exploit_file(file: FileToRestore, files_list: list[FileToRestore], last_domain: str) -> str:
21 | base_path = "/var/backup"
22 | # set it to work in the separate volumes (prevents a bootloop)
23 | if file.restore_path.startswith("/var/mobile/"):
24 | # required on iOS 17.0+ since /var/mobile is on a separate partition
25 | base_path = "/var/mobile/backup"
26 | elif file.restore_path.startswith("/private/var/mobile/"):
27 | base_path = "/private/var/mobile/backup"
28 | elif file.restore_path.startswith("/private/var/"):
29 | base_path = "/private/var/backup"
30 | # don't append the directory if it has already been added (restore will fail)
31 | path, name = os.path.split(file.restore_path)
32 | domain_path = f"SysContainerDomain-../../../../../../../..{base_path}{path}/"
33 | new_last_domain = last_domain
34 | if last_domain != domain_path:
35 | files_list.append(backup.Directory(
36 | "",
37 | f"{domain_path}",
38 | owner=file.owner,
39 | group=file.group
40 | ))
41 | new_last_domain = domain_path
42 | files_list.append(backup.ConcreteFile(
43 | "",
44 | f"{domain_path}{name}",
45 | owner=file.owner,
46 | group=file.group,
47 | contents=file.contents
48 | ))
49 | return new_last_domain
50 |
51 | def concat_regular_file(file: FileToRestore, files_list: list[FileToRestore], last_domain: str, last_path: str):
52 | path, name = os.path.split(file.restore_path)
53 | paths = path.split("/")
54 | new_last_domain = last_domain
55 | # append the domain first
56 | if last_domain != file.domain:
57 | files_list.append(backup.Directory(
58 | "",
59 | file.domain,
60 | owner=file.owner,
61 | group=file.group
62 | ))
63 | new_last_domain = file.domain
64 | # append each part of the path if it is not already there
65 | full_path = ""
66 | mode = file.mode
67 | if mode == None:
68 | mode = backup.DEFAULT
69 | for path_item in paths:
70 | if full_path != "":
71 | full_path += "/"
72 | full_path += path_item
73 | if not last_path.startswith(full_path):
74 | files_list.append(backup.Directory(
75 | full_path,
76 | file.domain,
77 | owner=file.owner,
78 | group=file.group,
79 | mode=mode
80 | ))
81 | last_path = full_path
82 | # finally, append the file
83 | files_list.append(backup.ConcreteFile(
84 | f"{full_path}/{name}",
85 | file.domain,
86 | owner=file.owner,
87 | group=file.group,
88 | contents=file.contents,
89 | src_path=file.contents_path,
90 | mode=mode
91 | ))
92 | return new_last_domain, full_path
93 |
94 | # files is a list of FileToRestore objects
95 | def restore_files(files: list[FileToRestore], reboot: bool = False, lockdown_client: LockdownClient = None, progress_callback = lambda x: None):
96 | # create the files to be backed up
97 | files_list = [
98 | ]
99 | apps_list = []
100 | active_bundle_ids = []
101 | apps = None
102 | sorted_files = sorted(files, key=lambda x: (x.domain, x.restore_path), reverse=False)
103 | # add the file paths
104 | last_domain = ""
105 | last_path = ""
106 | exploit_only = True
107 | for file in sorted_files:
108 | if file.domain == "":
109 | last_domain = concat_exploit_file(file, files_list, last_domain)
110 | else:
111 | last_domain, last_path = concat_regular_file(file, files_list, last_domain, last_path)
112 | exploit_only = False
113 | # add the app bundle to the list
114 | if last_domain.startswith("AppDomain"):
115 | bundle_id = last_domain.removeprefix("AppDomain-")
116 | if not bundle_id in active_bundle_ids:
117 | if apps == None:
118 | apps = InstallationProxyService(lockdown=lockdown_client).get_apps(application_type="Any", calculate_sizes=False)
119 | app_info = apps[bundle_id]
120 | active_bundle_ids.append(bundle_id)
121 | apps_list.append(backup.AppBundle(
122 | identifier=bundle_id,
123 | path=app_info["Container"],
124 | version=app_info["CFBundleVersion"],
125 | container_content_class="Data/Application"
126 | ))
127 |
128 | # crash the restore to skip the setup (only works for exploit files)
129 | if exploit_only:
130 | files_list.append(backup.ConcreteFile("", "SysContainerDomain-../../../../../../../.." + "/crash_on_purpose", contents=b""))
131 |
132 | # create the backup
133 | back = backup.Backup(files=files_list, apps=apps_list)
134 |
135 | perform_restore(backup=back, reboot=reboot, lockdown_client=lockdown_client, progress_callback=progress_callback)
136 |
137 |
138 | # DEPRECIATED
139 | def restore_file(fp: str, restore_path: str, restore_name: str, reboot: bool = False, lockdown_client: LockdownClient = None):
140 | # open the file and read the contents
141 | contents = open(fp, "rb").read()
142 |
143 | base_path = "/var/backup"
144 | if restore_path.startswith("/var/mobile/"):
145 | # required on iOS 17.0+ since /var/mobile is on a separate partition
146 | base_path = "/var/mobile/backup"
147 |
148 | # create the backup
149 | back = backup.Backup(files=[
150 | # backup.Directory("", "HomeDomain"),
151 | # backup.Directory("Library", "HomeDomain"),
152 | # backup.Directory("Library/Preferences", "HomeDomain"),
153 | # backup.ConcreteFile("Library/Preferences/temp", "HomeDomain", owner=501, group=501, contents=contents, inode=0),
154 | backup.Directory(
155 | "",
156 | f"SysContainerDomain-../../../../../../../..{base_path}{restore_path}",
157 | owner=501,
158 | group=501
159 | ),
160 | backup.ConcreteFile(
161 | "",
162 | f"SysContainerDomain-../../../../../../../..{base_path}{restore_path}{restore_name}",
163 | owner=501,
164 | group=501,
165 | contents=contents#b"",
166 | # inode=0
167 | ),
168 | # backup.ConcreteFile(
169 | # "",
170 | # "SysContainerDomain-../../../../../../../../var/.backup.i/var/root/Library/Preferences/temp",
171 | # owner=501,
172 | # group=501,
173 | # contents=b"",
174 | # ), # Break the hard link
175 | backup.ConcreteFile("", "SysContainerDomain-../../../../../../../.." + "/crash_on_purpose", contents=b""),
176 | ])
177 |
178 |
179 | perform_restore(backup=back, reboot=reboot, lockdown_client=lockdown_client)
--------------------------------------------------------------------------------
/tweaks/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LoveJessyChen/Nugget-iOS/eb917fe4a75ef61eca7e098b7ad2afcc736a2515/tweaks/__init__.py
--------------------------------------------------------------------------------
/tweaks/basic_plist_locations.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class FileLocation(Enum):
4 | # Mobile Gestalt
5 | resolution = "/var/Managed Preferences/mobile/com.apple.iokit.IOMobileGraphicsFamily.plist"
6 |
7 | # Springboard Options
8 | springboard = "/var/Managed Preferences/mobile/com.apple.springboard.plist"
9 | footnote = "/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/SharedDeviceConfiguration.plist"
10 |
11 | # Internal Options
12 | globalPreferences = "/var/Managed Preferences/mobile/.GlobalPreferences.plist"
13 | appStore = "/var/Managed Preferences/mobile/com.apple.AppStore.plist"
14 | backboardd = "/var/Managed Preferences/mobile/com.apple.backboardd.plist"
15 | coreMotion = "/var/Managed Preferences/mobile/com.apple.CoreMotion.plist"
16 | pasteboard = "/var/Managed Preferences/mobile/com.apple.Pasteboard.plist"
17 | notes = "/var/Managed Preferences/mobile/com.apple.mobilenotes.plist"
18 |
19 | # Daemons
20 | disabledDaemons = "/var/db/com.apple.xpc.launchd/disabled.plist"
21 | screentime = "/var/mobile/Library/Preferences/ScreenTimeAgent.plist"
22 |
23 | # Risky Options
24 | ota = "/var/Managed Preferences/mobile/com.apple.MobileAsset.plist"
25 |
26 | # support for older versions of python that cannot enumerate over enums
27 | FileLocationsList: list[FileLocation] = [
28 | FileLocation.resolution,
29 | FileLocation.springboard, FileLocation.footnote,
30 | FileLocation.globalPreferences, FileLocation.appStore, FileLocation.backboardd, FileLocation.coreMotion, FileLocation.pasteboard, FileLocation.notes
31 | ]
32 | RiskyFileLocationsList: list[FileLocation] = [
33 | FileLocation.ota
34 | ]
--------------------------------------------------------------------------------
/tweaks/custom_gestalt_tweaks.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from json import loads
3 | from .tweak_classes import MobileGestaltTweak
4 |
5 | class ValueType(Enum):
6 | Integer = "Integer"
7 | Float = "Float"
8 | String = "String"
9 | Array = "Array"
10 | Dictionary = "Dictionary"
11 |
12 | ValueTypeStrings: list[ValueType] = [
13 | ValueType.Integer.value, ValueType.Float.value,
14 | ValueType.String.value,
15 | ValueType.Array.value, ValueType.Dictionary.value
16 | ]
17 |
18 | class CustomGestaltTweak:
19 | def __init__(self, tweak: MobileGestaltTweak, value_type: ValueType):
20 | self.tweak = tweak
21 | self.value_type = value_type
22 | self.deactivated = False
23 |
24 | # TODO: change everything to not return the dict since it is passed by reference
25 | def apply_tweak(self, plist: dict) -> dict:
26 | if self.deactivated or self.tweak.key == "":
27 | # key was not set, don't apply (maybe user added it by accident)
28 | return plist
29 | self.tweak.enabled = True
30 | # set the value to be as the specified value type
31 | if self.value_type == ValueType.Integer:
32 | self.tweak.value = int(self.tweak.value)
33 | elif self.value_type == ValueType.Float:
34 | self.tweak.value = float(self.tweak.value)
35 | elif self.value_type == ValueType.Array or self.value_type == ValueType.Dictionary:
36 | # json convert string to array/dict
37 | self.tweak.value = loads(self.tweak.value)
38 |
39 | # apply the tweak after updating the value
40 | plist = self.tweak.apply_tweak(plist)
41 | return plist
42 |
43 |
44 | class CustomGestaltTweaks:
45 | custom_tweaks: list[CustomGestaltTweak] = []
46 |
47 | def create_tweak(key: str="", value: str="1", value_type: ValueType = ValueType.Integer) -> int:
48 | new_tweak = MobileGestaltTweak(key, value=value)
49 | CustomGestaltTweaks.custom_tweaks.append(CustomGestaltTweak(new_tweak, value_type))
50 | # return the tweak id
51 | return len(CustomGestaltTweaks.custom_tweaks) - 1
52 |
53 | def set_tweak_key(id: int, key: str):
54 | CustomGestaltTweaks.custom_tweaks[id].tweak.key = key
55 |
56 | def set_tweak_value(id: int, value: str):
57 | CustomGestaltTweaks.custom_tweaks[id].tweak.value = value
58 |
59 | def set_tweak_value_type(id: int, value_type) -> str:
60 | new_value_type = value_type
61 | if isinstance(value_type, str):
62 | # based on string value
63 | new_value_type = ValueType(value_type)
64 | elif isinstance(value_type, int):
65 | # based on index of the string
66 | new_value_type = ValueType(ValueTypeStrings[value_type])
67 |
68 | CustomGestaltTweaks.custom_tweaks[id].value_type = new_value_type
69 | # update the value to be of the new type
70 | new_value = 1
71 | new_str = "1"
72 | if new_value_type == ValueType.Float:
73 | new_value = 1.0
74 | new_str = "1.0"
75 | elif new_value_type == ValueType.String:
76 | new_value = ""
77 | new_str = ""
78 | elif new_value_type == ValueType.Array:
79 | new_value = []
80 | new_str = "[ ]"
81 | elif new_value_type == ValueType.Dictionary:
82 | new_value = {}
83 | new_str = "{ }"
84 | CustomGestaltTweaks.custom_tweaks[id].tweak.value = new_value
85 | return new_str
86 |
87 | def deactivate_tweak(id: int):
88 | CustomGestaltTweaks.custom_tweaks[id].deactivated = True
89 | CustomGestaltTweaks.custom_tweaks[id].tweak = None
90 |
91 | def apply_tweaks(plist: dict):
92 | for tweak in CustomGestaltTweaks.custom_tweaks:
93 | plist = tweak.apply_tweak(plist)
94 | return plist
--------------------------------------------------------------------------------
/tweaks/daemons_tweak.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class Daemon(Enum):
4 | thermalmonitord = ["com.apple.thermalmonitord"]
5 | OTA = [
6 | "com.apple.mobile.softwareupdated",
7 | "com.apple.OTATaskingAgent",
8 | "com.apple.softwareupdateservicesd"
9 | ]
10 | UsageTrackingAgent = ["com.apple.UsageTrackingAgent"]
11 | GameCenter = ["com.apple.gamed"]
12 | ScreenTime = [
13 | "com.apple.ScreenTimeAgent",
14 | "com.apple.homed",
15 | "com.apple.familycircled"
16 | ]
17 | CrashReports = [
18 | "com.apple.ReportCrash",
19 | "com.apple.ReportCrash.Jetsam",
20 | "com.apple.ReportMemoryException",
21 | "com.apple.OTACrashCopier",
22 | "com.apple.analyticsd",
23 | "com.apple.wifianalyticsd",
24 | "com.apple.aslmanager",
25 | "com.apple.coresymbolicationd",
26 | "com.apple.crash_mover",
27 | "com.apple.crashreportcopymobile",
28 | "com.apple.DumpBasebandCrash",
29 | "com.apple.DumpPanic",
30 | "com.apple.logd",
31 | "com.apple.logd.admin",
32 | "com.apple.logd.events",
33 | "com.apple.logd.watchdog",
34 | "com.apple.logd_helper",
35 | "com.apple.logd_reporter",
36 | "com.apple.logd_reporter.report_statistics",
37 | "com.apple.system.logger",
38 | "com.apple.hangreporter",
39 | "com.apple.hangtracerd",
40 | "com.apple.spindump",
41 | "com.apple.rtcreportingd",
42 | "com.apple.syslogd"
43 | ]
44 | ATWAKEUP = ["com.apple.atc.atwakeup"]
45 | Tips = ["com.apple.tipsd"]
46 | VPN = ["com.apple.racoon"]
47 | ChineseLAN = [
48 | "com.apple.wapic",
49 | "com.apple.wifi.wapic"
50 | ]
51 | HealthKit = ["com.apple.healthd"]
52 | AirPrint = ["com.apple.printd"]
53 | AssistiveTouch = ["com.apple.assistivetouchd"]
54 | iCloud = ["com.apple.itunescloudd"]
55 | InternetTethering = ["com.apple.MobileInternetSharing"]
56 | PassBook = ["com.apple.passd"]
57 | Spotlight = ["com.apple.searchd"]
58 | VoiceControl = [
59 | "com.apple.assistant_service",
60 | "com.apple.assistantd",
61 | "com.apple.voiced"
62 | ]
63 |
--------------------------------------------------------------------------------
/tweaks/eligibility_tweak.py:
--------------------------------------------------------------------------------
1 | from .tweak_classes import Tweak
2 | from controllers.files_handler import get_bundle_files
3 | from restore.restore import FileToRestore
4 | from devicemanagement.constants import Version
5 |
6 | import plistlib
7 | import sys
8 | from pathlib import Path
9 | from os import path, getcwd
10 |
11 | class InvalidRegionCodeException(Exception):
12 | "Region code must be exactly 2 characters long!"
13 | pass
14 |
15 | def replace_region_code(plist_path: str, original_code: str = "US", new_code: str = "US"):
16 | with open(plist_path, 'rb') as f:
17 | plist_data = plistlib.load(f)
18 |
19 | plist_str = str(plist_data)
20 | updated_plist_str = plist_str.replace(original_code, new_code)
21 | updated_plist_data = eval(updated_plist_str) # Convert string back to dictionary
22 |
23 | return plistlib.dumps(updated_plist_data)
24 |
25 | class EligibilityTweak(Tweak):
26 | def __init__(self):
27 | super().__init__(key=None, value=["Method 1", "Method 2"])
28 | self.code = "US"
29 | self.method = 0 # between 0 and 1
30 |
31 | def set_region_code(self, new_code: str):
32 | if new_code == '':
33 | self.code = "US"
34 | else:
35 | self.code = new_code
36 |
37 | def set_selected_option(self, new_method: int):
38 | self.method = new_method % 2 # force it to be either 0 or 1
39 | self.set_enabled(True)
40 |
41 | def get_selected_option(self) -> int:
42 | return self.method
43 |
44 | def apply_tweak(self) -> list[FileToRestore]:
45 | # credit to lrdsnow for EU Enabler
46 | # https://github.com/Lrdsnow/EUEnabler/blob/main/app.py
47 | if not self.enabled:
48 | return None
49 | print(f"Applying EU Enabler for region \'{self.code}\'...")
50 | # get the plists directory
51 | source_dir = get_bundle_files("files/eligibility")
52 |
53 | # start with eligibility.plist
54 | file_path = path.join(source_dir, 'eligibility.plist')
55 | eligibility_data = replace_region_code(file_path, original_code="US", new_code=self.code)
56 | files_to_restore = [
57 | FileToRestore(
58 | contents=eligibility_data,
59 | restore_path="/var/db/os_eligibility/eligibility.plist",
60 | )
61 | ]
62 |
63 | # next modify config.plist
64 | file_path = path.join(source_dir, 'Config.plist')
65 | config_data = replace_region_code(file_path, original_code="US", new_code=self.code)
66 | if self.method == 0:
67 | files_to_restore.append(
68 | FileToRestore(
69 | contents=config_data,
70 | restore_path="/var/MobileAsset/AssetsV2/com_apple_MobileAsset_OSEligibility/purpose_auto/c55a421c053e10233e5bfc15c42fa6230e5639a9.asset/AssetData/Config.plist",
71 | )
72 | )
73 | elif self.method == 1:
74 | files_to_restore.append(
75 | FileToRestore(
76 | contents=config_data,
77 | restore_path="/var/MobileAsset/AssetsV2/com_apple_MobileAsset_OSEligibility/purpose_auto/247556c634fc4cc4fd742f1b33af9abf194a986e.asset/AssetData/Config.plist",
78 | )
79 | )
80 |
81 | # return the new files to restore
82 | return files_to_restore
83 |
84 |
85 | class AITweak(Tweak):
86 | def __init__(self):
87 | super().__init__(key=None, value="")
88 |
89 | def set_language_code(self, lang: str):
90 | self.value = lang
91 |
92 | def apply_tweak(self) -> FileToRestore:
93 | if not self.enabled:
94 | return None
95 | langs = ["en"]
96 | if self.value != "":
97 | langs.append(self.value)
98 | plist = {
99 | "OS_ELIGIBILITY_DOMAIN_CALCIUM": {
100 | "os_eligibility_answer_source_t": 1,
101 | "os_eligibility_answer_t": 2,
102 | "status": {
103 | "OS_ELIGIBILITY_INPUT_CHINA_CELLULAR": 2
104 | }
105 | },
106 | "OS_ELIGIBILITY_DOMAIN_GREYMATTER": {
107 | "context": {
108 | "OS_ELIGIBILITY_CONTEXT_ELIGIBLE_DEVICE_LANGUAGES": langs
109 | },
110 | "os_eligibility_answer_source_t": 1,
111 | "os_eligibility_answer_t": 4,
112 | "status": {
113 | "OS_ELIGIBILITY_INPUT_DEVICE_LANGUAGE": 3,
114 | "OS_ELIGIBILITY_INPUT_DEVICE_REGION_CODE": 3,
115 | "OS_ELIGIBILITY_INPUT_EXTERNAL_BOOT_DRIVE": 3,
116 | "OS_ELIGIBILITY_INPUT_GENERATIVE_MODEL_SYSTEM": 3,
117 | "OS_ELIGIBILITY_INPUT_SHARED_IPAD": 3,
118 | "OS_ELIGIBILITY_INPUT_SIRI_LANGUAGE": 3
119 | }
120 | }
121 | }
122 |
123 | return FileToRestore(contents=plistlib.dumps(plist), restore_path="/var/db/eligibilityd/eligibility.plist")
--------------------------------------------------------------------------------
/tweaks/posterboard/template_file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 | import zipfile
4 | import fnmatch
5 |
6 | from json import load
7 | from typing import Optional
8 | from tempfile import TemporaryDirectory
9 | from shutil import rmtree
10 |
11 | from .tendie_file import TendieFile
12 | from .template_options import OptionType, TemplateOption, ReplaceOption, RemoveOption, SetOption, PickerOption
13 | from exceptions.posterboard_exceptions import PBTemplateException
14 |
15 | CURRENT_FORMAT = 1
16 |
17 | class TemplateFile(TendieFile):
18 | options: list[TemplateOption]
19 | json_path: str
20 | tmp_dir: str = None
21 |
22 | # TODO: Move these to custom operations
23 | description: Optional[str] = None # description to go under the file
24 | resources: list[str] = [] # list of file paths for embedded resources
25 | previews: dict[str, str] = {} # list of resources to use as previews
26 | preview_layout: str = "horizontal" # the direction to lay out the preview images
27 |
28 | banner_text: Optional[str] = None # text to go as a banner
29 | banner_stylesheet: Optional[str] = None # style sheet of the banner
30 | format_version: int = CURRENT_FORMAT # format version of config
31 |
32 | def __init__(self, path: str):
33 | super().__init__(path=path)
34 | self.options = []
35 | self.json_path = None
36 |
37 | # find the config.json file
38 | with zipfile.ZipFile(path, mode="r") as archive:
39 | for option in archive.namelist():
40 | if "config.json" in option.lower() and not "descriptor" in option.lower() and not "container" in option.lower():
41 | self.json_path = option
42 | break
43 | if self.json_path != None:
44 | file = archive.open(self.json_path)
45 | data = load(file)
46 | # load the options
47 | if not 'options' in data:
48 | raise PBTemplateException(path, "No options were found in the config. Make sure that it is in the correct format.")
49 | if not 'domain' in data or (data['domain'] != "AppDomain-com.apple.PosterBoard" and data['domain'] != "com.apple.PosterBoard"):
50 | # made an oopsie here, only AppDomain-com.apple.PosterBoard should be allowed
51 | # I will allow com.apple.PosterBoard to not break support
52 | raise PBTemplateException(path, "This config is not for the domain \"AppDomain-com.apple.PosterBoard\". Make sure that it is compatible with your version of Nugget.")
53 | self.format_version = int(data['format_version'])
54 | if self.format_version > CURRENT_FORMAT:
55 | raise PBTemplateException(path, "This config requires a newer version of Nugget.")
56 | self.name = f"{data['title']} - by {data['author']}"
57 | if 'description' in data:
58 | self.description = data['description']
59 | # load the previews
60 | prevs = []
61 | if 'previews' in data:
62 | prevs = data['previews']
63 | if 'preview_layout' in data:
64 | self.preview_layout = data['preview_layout']
65 | # load the banner
66 | if 'banner_text' in data:
67 | self.banner_text = data['banner_text']
68 | if 'banner_stylesheet' in data:
69 | self.banner_stylesheet = data['banner_stylesheet']
70 | # load the resources
71 | if 'resources' in data:
72 | self.resources = data['resources']
73 | # open the resources and put them in temp files
74 | rcs_path = self.json_path.removesuffix("config.json")
75 | for resource in self.resources:
76 | # handle wildcards
77 | rc_pattern = rcs_path + resource
78 | for rc_path in fnmatch.filter(archive.namelist(), rc_pattern):
79 | rc_data = archive.read(rc_path)
80 | if rc_data != None:
81 | # write it to a temp file
82 | if self.tmp_dir == None:
83 | self.tmp_dir = TemporaryDirectory()
84 | rc_full_path = os.path.join(self.tmp_dir.name, rc_path)
85 | os.makedirs(os.path.dirname(rc_full_path), exist_ok=True)
86 | with open(rc_full_path, "wb") as rc_fp:
87 | rc_fp.write(rc_data)
88 | # update the url in the banner stylesheet
89 | clean_path = rc_path.replace(rcs_path, "")
90 | if self.banner_stylesheet != None:
91 | self.banner_stylesheet = self.banner_stylesheet.replace(f"url({clean_path})", f"url({rc_full_path})")
92 | # set the preview images
93 | if clean_path in prevs:
94 | self.previews[clean_path] = rc_full_path
95 |
96 | # TODO: Add error handling
97 | for option in data['options']:
98 | opt_type = OptionType[option['type']]
99 | if opt_type == OptionType.replace:
100 | self.options.append(ReplaceOption(data=option))
101 | elif opt_type == OptionType.remove:
102 | self.options.append(RemoveOption(data=option))
103 | elif opt_type == OptionType.set:
104 | self.options.append(SetOption(data=option))
105 | elif opt_type == OptionType.picker:
106 | self.options.append(PickerOption(data=option))
107 | else:
108 | raise PBTemplateException(path, "Invalid option type in template")
109 | else:
110 | raise PBTemplateException(path, "No config.json found in file!")
111 |
112 | def clean_files(self):
113 | if self.tmp_dir != None:
114 | try:
115 | rmtree(self.tmp_dir.name)
116 | except Exception as e:
117 | print(f"Error when removing temp dir: {str(e)}")
118 |
119 | def extract(self, output_dir: str):
120 | zip_output = os.path.join(output_dir, str(uuid.uuid4()))
121 | os.makedirs(zip_output)
122 | with zipfile.ZipFile(self.path, 'r') as zip_ref:
123 | zip_ref.extractall(zip_output)
124 |
125 | # apply the options
126 | parent_path = os.path.join(zip_output, os.path.dirname(self.json_path))
127 | for option in self.options:
128 | option.apply(container_path=parent_path)
--------------------------------------------------------------------------------
/tweaks/posterboard/template_options/__init__.py:
--------------------------------------------------------------------------------
1 | from .template_options import OptionType, TemplateOption
2 | from .replace_option import ReplaceOption
3 | from .remove_option import RemoveOption
4 | from .set_option import SetterType, SetOption
5 | from .picker_option import PickerOption
--------------------------------------------------------------------------------
/tweaks/posterboard/template_options/picker_option.py:
--------------------------------------------------------------------------------
1 | from . import TemplateOption
2 |
3 | import os
4 | from typing import Optional
5 | from shutil import rmtree, move
6 | from PySide6.QtWidgets import QWidget, QLabel, QVBoxLayout, QHBoxLayout, QComboBox
7 |
8 | from controllers.xml_handler import delete_xml_value
9 | from gui.custom_qt_elements.multicombobox import MultiComboBox
10 |
11 | class PickerElement:
12 | label: str # label to show for the picker
13 | files: list[str] = [] # list of files to include with the picker
14 | associated_preview: Optional[str] = None
15 | preview_lbl: Optional[QLabel] = None
16 |
17 | def __init__(self, option_data: dict):
18 | self.label = option_data['label']
19 | if 'files' in option_data:
20 | self.files = option_data['files']
21 | if 'associated_preview' in option_data:
22 | self.associated_preview = option_data['associated_preview']
23 |
24 | class PickerOption(TemplateOption):
25 | options: list[PickerElement] # list of elements inside the picker
26 | allow_multiple_selection: bool = False # allow choosing multiple options
27 | rename: bool = False # whether or not to rename the chosen files (allow_multiple_selection must be False)
28 | names: list[str] = [] # list of names to rename to
29 |
30 | selection: int|list[str] = [] # chosen user selection
31 | default_val: int|list[int] = [] # the default values
32 |
33 | def __init__(self, data: dict):
34 | super().__init__(data=data)
35 | # get the list of options
36 | self.options = []
37 | opts = data['options']
38 | for opt in opts:
39 | self.options.append(PickerElement(opt))
40 |
41 | # add the other options
42 | if 'allow_multiple_selection' in data:
43 | self.allow_multiple_selection = data['allow_multiple_selection']
44 | if not self.allow_multiple_selection:
45 | self.selection = 0
46 | if 'rename' in data:
47 | self.rename = data['rename']
48 | if self.rename:
49 | self.names = data['names'] # only require if rename is true
50 | # default value(s)
51 | if 'default_value' in data:
52 | self.default_val = data['default_value']
53 | elif 'default_values' in data:
54 | self.default_val = data['default_values']
55 |
56 | def create_interface(self, options_widget: QWidget, options_layout: QVBoxLayout):
57 | # picker option
58 | picker_widget = QWidget(options_widget)
59 | picker_layout = QHBoxLayout(options_widget)
60 | # add label first
61 | picker_label = QLabel(picker_widget)
62 | picker_label.setText(self.label)
63 | picker_layout.addWidget(picker_label)
64 | if self.allow_multiple_selection:
65 | pickerDrp = MultiComboBox(picker_widget, updateAction=self.onPickerUpdate)
66 | for opt in self.options:
67 | pickerDrp.addItem(opt.label)
68 | pickerDrp.selectIndices(self.default_val)
69 | pickerDrp.updateText()
70 | pickerDrp.setStyleSheet("QWidget { background-color: #3b3b3b; border: 2px solid #3b3b3b; border-radius: 5px; }")
71 | else:
72 | pickerDrp = QComboBox(picker_widget)
73 | for opt in self.options:
74 | pickerDrp.addItem(opt.label)
75 | pickerDrp.activated.connect(self.onPickerUpdate)
76 | pickerDrp.setCurrentIndex(self.default_val)
77 | picker_layout.addWidget(pickerDrp)
78 | picker_widget.setLayout(picker_layout)
79 | options_layout.addWidget(picker_widget)
80 |
81 | def add_potential_preview_lbls(self, lbls: dict[str, QLabel]):
82 | for opt in self.options:
83 | if opt.associated_preview in lbls:
84 | opt.preview_lbl = lbls[opt.associated_preview]
85 |
86 | def update_preview(self):
87 | if self.allow_multiple_selection:
88 | for opt in self.options:
89 | # hide if not in selection or show if it is
90 | if opt.preview_lbl != None:
91 | opt.preview_lbl.setVisible(opt.label in self.selection)
92 | else:
93 | for i in range(len(self.options)):
94 | if self.options[i].preview_lbl != None:
95 | self.options[i].preview_lbl.setVisible(self.selection == i)
96 |
97 | def onPickerUpdate(self, selection: int|list[str]):
98 | self.selection = selection
99 | self.update_preview()
100 |
101 | def apply(self, container_path: str):
102 | # get the list of files
103 | sel_options: list[PickerElement] = []
104 | if self.allow_multiple_selection:
105 | for opt in self.options:
106 | if opt.label in self.selection:
107 | sel_options.append(opt)
108 | else:
109 | sel_options.append(self.options[self.selection])
110 | # delete the files that are not in use
111 | for opt in self.options:
112 | if not opt in sel_options:
113 | for file in opt.files:
114 | # delete files or directories
115 | path = os.path.join(container_path, *file.split('/'))
116 | if os.path.isdir(path):
117 | rmtree(path=path, ignore_errors=True)
118 | else:
119 | os.remove(path=path)
120 | # rename the files if needed
121 | if self.rename:
122 | for i in range(len(self.options[self.selection].files)):
123 | # rename files or directories
124 | old_path = os.path.join(container_path, self.options[self.selection].files[i])
125 | new_path = os.path.join(container_path, self.names[i])
126 | if os.path.isdir(path):
127 | # rename whole directory
128 | move(old_path, new_path)
129 | else:
130 | os.rename(old_path, new_path)
--------------------------------------------------------------------------------
/tweaks/posterboard/template_options/remove_option.py:
--------------------------------------------------------------------------------
1 | from . import TemplateOption
2 |
3 | import os
4 | import glob
5 | from dataclasses import dataclass
6 | from shutil import rmtree
7 | from typing import Optional
8 | from PySide6.QtWidgets import QWidget, QVBoxLayout, QCheckBox
9 |
10 | from controllers.xml_handler import delete_xml_value
11 |
12 | @dataclass
13 | class RemoveOption(TemplateOption):
14 | inverted: bool = False # if set to true, the files will only be deleted if the checkbox is unchecked
15 | value: bool = False # whether or not to delete the file
16 | identifier: Optional[str] = None # nuggetId for properties in caml
17 | use_ca_id: bool = False # whether or not to use the ca id over nuggetId
18 |
19 | def __init__(self, data: dict):
20 | super().__init__(data=data)
21 | if 'inverted' in data:
22 | self.inverted = data['inverted']
23 | if 'default_value' in data:
24 | self.value = data['default_value']
25 | if 'identifier' in data:
26 | self.identifier = data['identifier']
27 | if 'use_ca_id' in data:
28 | self.use_ca_id = data['use_ca_id']
29 |
30 | def create_interface(self, options_widget: QWidget, options_layout: QVBoxLayout):
31 | # remove object/setter toggle
32 | remove_chk = QCheckBox(options_widget)
33 | remove_chk.setText(self.label)
34 | remove_chk.setChecked(self.value)
35 | remove_chk.toggled.connect(self.set_option)
36 | options_layout.addWidget(remove_chk)
37 |
38 | def set_option(self, checked: bool):
39 | self.value = checked
40 | self.update_preview()
41 |
42 | def update_preview(self):
43 | if self.preview_lbl != None:
44 | to_hide = (self.inverted and not self.value) or (not self.inverted and self.value)
45 | self.preview_lbl.setVisible(not to_hide)
46 |
47 | def apply(self, container_path: str):
48 | if (self.inverted and not self.value) or (not self.inverted and self.value):
49 | for file in self.files:
50 | path = os.path.join(container_path, *file.split('/'))
51 | # wildcard support
52 | for full_path in glob.glob(path, recursive=True):
53 | if self.identifier != None:
54 | # delete properties in xml
55 | # TODO: make sure it isn't a directory
56 | delete_xml_value(file=full_path, id=self.identifier, use_ca_id=self.use_ca_id)
57 | else:
58 | # delete files or directories
59 | if os.path.isdir(full_path):
60 | rmtree(path=full_path, ignore_errors=True)
61 | else:
62 | os.remove(path=full_path)
--------------------------------------------------------------------------------
/tweaks/posterboard/template_options/replace_option.py:
--------------------------------------------------------------------------------
1 | from . import TemplateOption
2 |
3 | from exceptions.nugget_exception import NuggetException
4 |
5 | import os
6 | import glob
7 | from dataclasses import dataclass
8 | from typing import Optional
9 | from PySide6 import QtWidgets, QtGui, QtCore
10 |
11 | @dataclass
12 | class ReplaceOption(TemplateOption):
13 | allowed_files: str # Qt format - ex. "Image Files (*.png)"
14 | button_label: str = "Import"
15 | required: bool = False
16 | value: Optional[str] = None # path of file selected by user
17 | default_preview: Optional[QtGui.QPixmap] = None # the default preview image so that it isn't lost when replacing
18 |
19 | def __init__(self, data: dict):
20 | super().__init__(data=data)
21 | self.allowed_files = data['allowed_files']
22 | if 'button_label' in data:
23 | self.button_label = data['button_label']
24 | else:
25 | self.button_label = f"Import {self.allowed_files}"
26 | if 'required' in data:
27 | self.required = data['required']
28 | self.repl_btn = None
29 | self.file_path_lbl = None
30 | self.window = None
31 | self.import_icon = QtGui.QIcon(":/icon/import.svg")
32 | self.remove_icon = QtGui.QIcon(":/icon/trash.svg")
33 |
34 | def update_preview(self):
35 | if self.preview_lbl != None:
36 | if self.value == None:
37 | # set back to default
38 | self.preview_lbl.setPixmap(self.default_preview)
39 | else:
40 | # set to newly selected image
41 | pixmap = QtGui.QPixmap(self.value)
42 | self.preview_lbl.setPixmap(pixmap)
43 |
44 | def add_potential_preview_lbls(self, lbls):
45 | super().add_potential_preview_lbls(lbls)
46 | if self.preview_lbl != None:
47 | self.default_preview = self.preview_lbl.pixmap()
48 |
49 | def create_interface(self, options_widget: QtWidgets.QWidget, options_layout: QtWidgets.QVBoxLayout):
50 | # replacable object
51 | repl_widget = QtWidgets.QWidget(options_widget)
52 | repl_layout = QtWidgets.QHBoxLayout(options_widget)
53 | repl_layout.setContentsMargins(0, 2, 0, 2)
54 | repl_lbl = QtWidgets.QLabel(repl_widget)
55 | req_label = ""
56 | if self.required:
57 | req_label = "* "
58 | repl_lbl.setText(f"{req_label}{self.label}")
59 | repl_layout.addWidget(repl_lbl)
60 | # button for importing files
61 | imp_widget = QtWidgets.QWidget(options_widget)
62 | imp_layout = QtWidgets.QVBoxLayout(options_widget)
63 | imp_widget.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft)
64 | self.repl_btn = QtWidgets.QToolButton(imp_widget)
65 | self.repl_btn.setIcon(self.import_icon)
66 | self.repl_btn.setIconSize(QtCore.QSize(20, 20))
67 | self.repl_btn.setText(self.button_label)
68 | self.repl_btn.setToolButtonStyle(QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
69 | self.repl_btn.clicked.connect(self.on_importReplaceBtn_clicked)
70 | imp_layout.addWidget(self.repl_btn)
71 | # file path name
72 | self.file_path_lbl = QtWidgets.QLabel(imp_widget)
73 | self.file_path_lbl.hide()
74 | imp_layout.addWidget(self.file_path_lbl)
75 | imp_widget.setLayout(imp_layout)
76 | repl_layout.addWidget(imp_widget)
77 | repl_widget.setLayout(repl_layout)
78 | options_layout.addWidget(repl_widget)
79 |
80 | def on_importReplaceBtn_clicked(self):
81 | if self.value == None:
82 | # import image
83 | selected_file, _ = QtWidgets.QFileDialog.getOpenFileName(self.window, "Select File", "", self.allowed_files, options=QtWidgets.QFileDialog.ReadOnly)
84 | if selected_file != None and selected_file != "":
85 | self.value = selected_file
86 | self.file_path_lbl.setText(selected_file)
87 | self.file_path_lbl.show()
88 | self.repl_btn.setText("Remove Selected File")
89 | self.repl_btn.setIcon(self.remove_icon)
90 | else:
91 | # remove current image
92 | self.value = None
93 | self.file_path_lbl.hide()
94 | self.repl_btn.setText(self.button_label)
95 | self.repl_btn.setIcon(self.import_icon)
96 | self.update_preview()
97 |
98 | def apply(self, container_path: str):
99 | if self.value == None:
100 | if not self.required:
101 | return
102 | elif self.required and self.value == None:
103 | raise NuggetException(f"Error applying template:\n\nNo selected file for required option {self.label}")
104 | contents=None
105 | in_path = os.path.join(container_path, self.value)
106 | with open(in_path, "rb") as in_file:
107 | contents = in_file.read()
108 | for file in self.files:
109 | out_path = os.path.join(container_path, *file.split('/'))
110 | # wildcard support
111 | for full_path in glob.glob(out_path, recursive=True):
112 | with open(full_path, "wb") as out_file:
113 | out_file.write(contents)
114 | del contents
--------------------------------------------------------------------------------
/tweaks/posterboard/template_options/template_options.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from dataclasses import dataclass
3 | from shutil import rmtree
4 | from typing import Optional
5 | from PySide6.QtWidgets import QVBoxLayout, QWidget, QLabel
6 |
7 | class OptionType(Enum):
8 | replace = "replace"
9 | remove = "remove"
10 | set = "set"
11 | picker = "picker"
12 |
13 | @dataclass
14 | class TemplateOption:
15 | type: OptionType
16 | label: str
17 | files: list[str]
18 | associated_preview: Optional[str]
19 | preview_lbl: Optional[QLabel]
20 |
21 | def __init__(self, data: dict):
22 | self.type = OptionType[data['type']]
23 | self.label = data['label']
24 | if self.type == OptionType.picker:
25 | # picker should not have files
26 | self.files = []
27 | else:
28 | self.files = data['files']
29 | if 'associated_preview' in data:
30 | self.associated_preview = data['associated_preview']
31 | else:
32 | self.associated_preview = None
33 | self.preview_lbl = None
34 |
35 | def add_potential_preview_lbls(self, lbls: dict[str, QLabel]):
36 | if self.associated_preview != None and self.associated_preview in lbls:
37 | self.preview_lbl = lbls[self.associated_preview]
38 | def update_preview(self):
39 | pass
40 |
41 | def create_interface(self, options_widget: QWidget, options_layout: QVBoxLayout):
42 | raise NotImplementedError
43 |
44 | def apply(self, container_path: str):
45 | raise NotImplementedError
--------------------------------------------------------------------------------
/tweaks/posterboard/tendie_file.py:
--------------------------------------------------------------------------------
1 | import os
2 | import uuid
3 | import zipfile
4 |
5 | class TendieFile:
6 | path: str
7 | name: str
8 | descriptor_cnt: int
9 | is_container: bool
10 | unsafe_container: bool
11 | loaded: bool
12 |
13 | def __init__(self, path: str):
14 | self.path = path
15 | self.name = os.path.basename(path)
16 | self.descriptor_cnt = 0
17 | self.is_container = False
18 | self.unsafe_container = False
19 | self.loaded = False
20 |
21 | # read the contents
22 | with zipfile.ZipFile(path, mode="r") as archive:
23 | for option in archive.namelist():
24 | if "__macosx/" in option.lower():
25 | continue
26 | if "container" in option.lower():
27 | self.is_container = True
28 | # check for the unsafe file that requires prb reset
29 | if "PBFPosterExtensionDataStoreSQLiteDatabase.sqlite3" in option:
30 | self.unsafe_container = True
31 | if "descriptor/" in option.lower():
32 | item = option.lower().split("descriptor/")[1]
33 | if item.count('/') == 1 and item.endswith('/'):
34 | self.descriptor_cnt += 1
35 | elif "descriptors/" in option.lower():
36 | item = option.lower().split("descriptors/")[1]
37 | if item.count('/') == 1 and item.endswith('/'):
38 | self.descriptor_cnt += 1
39 |
40 | def get_icon(self):
41 | if self.is_container:
42 | # container
43 | return ":/icon/shippingbox.svg"
44 | elif self.descriptor_cnt == 1:
45 | # single descriptor
46 | return ":/icon/photo.svg"
47 | else:
48 | # multiple descriptors
49 | return ":/icon/photo-stack.svg"
50 |
51 | def extract(self, output_dir: str):
52 | zip_output = os.path.join(output_dir, str(uuid.uuid4()))
53 | os.makedirs(zip_output)
54 | with zipfile.ZipFile(self.path, 'r') as zip_ref:
55 | zip_ref.extractall(zip_output)
--------------------------------------------------------------------------------
/tweaks/tweak_classes.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from devicemanagement.constants import Version
3 | from .basic_plist_locations import FileLocation
4 |
5 | class Tweak:
6 | def __init__(
7 | self,
8 | key: str,
9 | value: any = 1,
10 | owner: int = 501, group: int = 501
11 | ):
12 | self.key = key
13 | self.value = value
14 | self.owner = owner
15 | self.group = group
16 | self.enabled = False
17 |
18 | def set_enabled(self, value: bool):
19 | self.enabled = value
20 | def toggle_enabled(self):
21 | self.enabled = not self.enabled
22 | def set_value(self, new_value: any, toggle_enabled: bool = True):
23 | self.value = new_value
24 | if toggle_enabled:
25 | self.enabled = True
26 |
27 | def apply_tweak(self):
28 | raise NotImplementedError
29 |
30 | class NullifyFileTweak(Tweak):
31 | def __init__(
32 | self,
33 | file_location: FileLocation,
34 | owner: int = 501, group: int = 501
35 | ):
36 | super().__init__(key=None, value=None, owner=owner, group=group)
37 | self.file_location = file_location
38 |
39 | def apply_tweak(self, other_tweaks: dict):
40 | if self.enabled:
41 | other_tweaks[self.file_location] = b""
42 |
43 |
44 | class BasicPlistTweak(Tweak):
45 | def __init__(
46 | self,
47 | file_location: FileLocation,
48 | key: str,
49 | value: any = True,
50 | owner: int = 501, group: int = 501,
51 | is_risky: bool = False
52 | ):
53 | super().__init__(key=key, value=value, owner=owner, group=group)
54 | self.file_location = file_location
55 | self.is_risky = is_risky
56 |
57 | def apply_tweak(self, other_tweaks: dict, risky_allowed: bool = False) -> dict:
58 | if not self.enabled or (self.is_risky and not risky_allowed):
59 | return other_tweaks
60 | if self.file_location in other_tweaks:
61 | other_tweaks[self.file_location][self.key] = self.value
62 | else:
63 | other_tweaks[self.file_location] = {self.key: self.value}
64 | return other_tweaks
65 |
66 | class AdvancedPlistTweak(BasicPlistTweak):
67 | def __init__(
68 | self,
69 | file_location: FileLocation,
70 | keyValues: dict,
71 | owner: int = 501, group: int = 501,
72 | is_risky: bool = False
73 | ):
74 | super().__init__(file_location=file_location, key=None, value=keyValues, owner=owner, group=group, is_risky=is_risky)
75 |
76 | def set_multiple_values(self, keys: list[str], value: any):
77 | for key in keys:
78 | self.value[key] = value
79 |
80 | def apply_tweak(self, other_tweaks: dict, risky_allowed: bool = False) -> dict:
81 | if not self.enabled or (self.is_risky and not risky_allowed):
82 | return other_tweaks
83 | plist = {}
84 | for key in self.value:
85 | plist[key] = self.value[key]
86 | other_tweaks[self.file_location] = plist
87 | return other_tweaks
88 |
89 |
90 | class RdarFixTweak(BasicPlistTweak):
91 | def __init__(self):
92 | super().__init__(file_location=FileLocation.resolution, key=None)
93 | self.mode = 0
94 | self.di_type = -1
95 |
96 | def get_rdar_mode(self, model: str) -> int:
97 | if (model == "iPhone11,2" or model == "iPhone11,4" or model == "iPhone11,6"
98 | or model == "iPhone11,8"
99 | or model == "iPhone12,1" or model == "iPhone12,3" or model == "iPhone12,5"):
100 | self.mode = 1
101 | elif (model == "iPhone13,2" or model == "iPhone13,3" or model == "iPhone13,4"
102 | or model == "iPhone14,5" or model == "iPhone14,2" or model == "iPhone14,3"
103 | or model == "iPhone14,7" or model == "iPhone14,8"):
104 | self.mode = 2
105 | elif (model == "iPhone12,8" or model == "iPhone14,6"):
106 | self.mode = 3
107 | return self.mode
108 |
109 | def get_rdar_title(self) -> str:
110 | if self.mode == 1 or self.mode == 3:
111 | if self.di_type == -1:
112 | return "Revert RDAR fix"
113 | return "Fix RDAR"
114 | elif self.mode == 2:
115 | if self.di_type == -1:
116 | return "Revert Status Bar Fix"
117 | return "Dynamic Island Status Bar Fix"
118 | return "hide"
119 |
120 | def set_di_type(self, type: int):
121 | self.di_type = type
122 |
123 | def apply_tweak(self, other_tweaks: dict, risky_allowed: bool = False) -> dict:
124 | if not self.enabled:
125 | return other_tweaks
126 | if self.di_type == -1:
127 | # revert the fix
128 | other_tweaks[self.file_location] = {}
129 | elif self.mode == 1:
130 | # iPhone XR, XS, and 11
131 | plist = {
132 | "canvas_height": 1791,
133 | "canvas_width": 828
134 | }
135 | other_tweaks[self.file_location] = plist
136 | elif self.mode == 3:
137 | # iPhone SEs
138 | plist = {
139 | "canvas_height": 1779,
140 | "canvas_width": 1000
141 | }
142 | other_tweaks[self.file_location] = plist
143 | elif self.mode == 2:
144 | # Status bar fix (iPhone 12+)
145 | width = 2868
146 | height = 1320
147 | if self.di_type == 2556:
148 | width = 1179
149 | height = 2556
150 | elif self.di_type == 2796 or self.di_type == 2976:
151 | width = 1290
152 | height = 2796
153 | elif self.di_type == 2622:
154 | width = 1206
155 | height = 2622
156 | elif self.di_type == 2868:
157 | width = 1320
158 | height = 2868
159 | plist = {
160 | "canvas_height": height,
161 | "canvas_width": width
162 | }
163 | other_tweaks[self.file_location] = plist
164 | return other_tweaks
165 |
166 |
167 | class MobileGestaltTweak(Tweak):
168 | def __init__(
169 | self,
170 | key: str, subkey: str = None,
171 | value: any = 1,
172 | owner: int = 501, group: int = 501
173 | ):
174 | super().__init__(key, value, owner, group)
175 | self.subkey = subkey
176 |
177 | def apply_tweak(self, plist: dict):
178 | if not self.enabled:
179 | return plist
180 | new_value = self.value
181 | if self.subkey == None:
182 | plist["CacheExtra"][self.key] = new_value
183 | else:
184 | plist["CacheExtra"][self.key][self.subkey] = new_value
185 | return plist
186 |
187 | class MobileGestaltPickerTweak(Tweak):
188 | def __init__(
189 | self,
190 | key: str, subkey: str = None,
191 | values: list = [1]
192 | ):
193 | super().__init__(key=key, value=values)
194 | self.subkey = subkey
195 | self.selected_option = 0 # index of the selected option
196 |
197 | def apply_tweak(self, plist: dict):
198 | if not self.enabled or self.value[self.selected_option] == "Placeholder":
199 | return plist
200 | new_value = self.value[self.selected_option]
201 | if self.subkey == None:
202 | plist["CacheExtra"][self.key] = new_value
203 | else:
204 | plist["CacheExtra"][self.key][self.subkey] = new_value
205 | return plist
206 |
207 | def set_selected_option(self, new_option: int, is_enabled: bool = True):
208 | self.selected_option = new_option
209 | self.enabled = is_enabled
210 |
211 | def get_selected_option(self) -> int:
212 | return self.selected_option
213 |
214 | class MobileGestaltMultiTweak(Tweak):
215 | def __init__(self, keyValues: dict):
216 | super().__init__(key=None)
217 | self.keyValues = keyValues
218 | # key values looks like ["key name" = value]
219 |
220 | def apply_tweak(self, plist: dict):
221 | if not self.enabled:
222 | return plist
223 | for key in self.keyValues:
224 | plist["CacheExtra"][key] = self.keyValues[key]
225 | return plist
226 |
227 | class FeatureFlagTweak(Tweak):
228 | def __init__(
229 | self,
230 | flag_category: str, flag_names: list,
231 | is_list: bool=True, inverted: bool=False
232 | ):
233 | super().__init__(key=None)
234 | self.flag_category = flag_category
235 | self.flag_names = flag_names
236 | self.is_list = is_list
237 | self.inverted = inverted
238 |
239 | def apply_tweak(self, plist: dict):
240 | to_enable = self.enabled
241 | if self.inverted:
242 | to_enable = not self.enabled
243 | # create the category list if it doesn't exist
244 | if not self.flag_category in plist:
245 | plist[self.flag_category] = {}
246 | for flag in self.flag_names:
247 | if self.is_list:
248 | plist[self.flag_category][flag] = {
249 | 'Enabled': to_enable
250 | }
251 | else:
252 | plist[self.flag_category][flag] = to_enable
253 | return plist
--------------------------------------------------------------------------------
/version.txt:
--------------------------------------------------------------------------------
1 | # UTF-8
2 | #
3 | # For more details about fixed file info 'ffi' see:
4 | # http://msdn.microsoft.com/en-us/library/ms646997.aspx
5 | VSVersionInfo(
6 | ffi=FixedFileInfo(
7 | # filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
8 | # Set not needed items to zero 0.
9 | filevers=(1, 0, 0, 0),
10 | prodvers=(1, 0, 0, 0),
11 | # Contains a bitmask that specifies the valid bits 'flags'r
12 | mask=0x3f,
13 | # Contains a bitmask that specifies the Boolean attributes of the file.
14 | flags=0x0,
15 | # The operating system for which this file was designed.
16 | # 0x4 - NT and there is no need to change it.
17 | OS=0x4,
18 | # The general type of file.
19 | # 0x1 - the file is an application.
20 | fileType=0x1,
21 | # The function of the file.
22 | # 0x0 - the function is not defined for this fileType
23 | subtype=0x0,
24 | # Creation date and time stamp.
25 | date=(0, 0)
26 | ),
27 | kids=[
28 | StringFileInfo(
29 | [
30 | StringTable(
31 | u'040904B0',
32 | [StringStruct(u'CompanyName', u'Cowabunga'),
33 | StringStruct(u'FileDescription', u'Nugget'),
34 | StringStruct(u'FileVersion', u'5.2'),
35 | StringStruct(u'InternalName', u'Nugget'),
36 | StringStruct(u'LegalCopyright', u'Copyright (c) 2025 LeminLimez'),
37 | StringStruct(u'OriginalFilename', u'Nugget.exe'),
38 | StringStruct(u'ProductName', u'Nugget'),
39 | StringStruct(u'ProductVersion', u'5.2.0')])
40 | ]),
41 | VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
42 | ]
43 | )
--------------------------------------------------------------------------------