diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 00000000..c3a90738
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,22 @@
+# Use the official Python image from the Docker Hub
+FROM python:3.9-slim
+
+# Set environment variables
+ENV PYTHONDONTWRITEBYTECODE 1
+ENV PYTHONUNBUFFERED 1
+
+# Create a directory for the app
+WORKDIR /app
+
+# Copy the requirements file into the container
+COPY .devcontainer/requirements.txt /app/
+
+# Install dependencies
+RUN pip install --upgrade pip \
+ && pip install -r requirements.txt
+
+# Copy the rest of the application code into the container
+COPY . /app/
+
+# Ensure the post-create script is executable
+RUN chmod +x .devcontainer/post-create.sh
\ No newline at end of file
diff --git a/.devcontainer/devcontainer 2.json b/.devcontainer/devcontainer 2.json
new file mode 100644
index 00000000..b34dc333
--- /dev/null
+++ b/.devcontainer/devcontainer 2.json
@@ -0,0 +1,10 @@
+{
+ "image": "swift:latest",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "sswg.swift-lang"
+ ]
+ }
+ }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 00000000..08ae5ab3
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,19 @@
+{
+ "name": "iOS Development Environment",
+ "build": {
+ "dockerfile": "Dockerfile",
+ "args": {
+ "VARIANT": "18-bullseye"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-python.python",
+ "ms-azuretools.vscode-docker"
+ ]
+ }
+ },
+ "postCreateCommand": "bash .devcontainer/post-create.sh",
+ "remoteUser": "vscode"
+}
\ No newline at end of file
diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh
new file mode 100644
index 00000000..8a893258
--- /dev/null
+++ b/.devcontainer/post-create.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# This script will run after the container is created.
+
+# Update and upgrade apt-get
+sudo apt-get update && sudo apt-get upgrade -y
+
+# Install necessary tools
+sudo apt-get install -y git curl
+
+# Ensure DaiSign-API is installed (this is just an example, adjust as needed)
+if ! command -v DaiSign-API &> /dev/null
+then
+ echo "DaiSign-API could not be found, installing..."
+ # Replace with the actual installation command for DaiSign-API
+ # Example: curl -sSL https://example.com/install-daisign-api.sh | bash
+fi
+
+# Any other setup tasks can go here
\ No newline at end of file
diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt
new file mode 100644
index 00000000..663bd1f6
--- /dev/null
+++ b/.devcontainer/requirements.txt
@@ -0,0 +1 @@
+requests
\ No newline at end of file
diff --git a/.github/workflows/Sign.yml b/.github/workflows/Sign.yml
new file mode 100644
index 00000000..d00f93a0
--- /dev/null
+++ b/.github/workflows/Sign.yml
@@ -0,0 +1,60 @@
+name: Sign IPA
+
+on:
+ push:
+ branches:
+ - main
+ workflow_dispatch:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ sign-ipa:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '14'
+
+ - name: Install dependencies
+ run: npm install axios form-data
+
+ - name: Download latest release asset
+ run: |
+ curl -L -o certificates/app.ipa https://github.com/BDGHubNoKey/Backdoor/releases/download/v0.0.8/feather_v0.0.8.ipa
+
+ - name: Run sign script
+ env:
+ P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
+ UDID: ${{ secrets.UDID }} # Add your UDID as a secret in the repository settings
+ run: |
+ node <<'EOF'
+ const axios = require('axios');
+ const FormData = require('form-data');
+ const fs = require('fs');
+
+ const form = new FormData();
+ form.append('ipa', fs.createReadStream('certificates/app.ipa'));
+ form.append('p12', fs.createReadStream('certificates/BDG.p12'));
+ form.append('mobileprovision', fs.createReadStream('certificates/BDG.mobileprovision'));
+ form.append('p12_password', process.env.P12_PASSWORD);
+ form.append('udid', process.env.UDID); // Include the UDID in the form data
+ form.append('save_cert', 'on'); // Set to 'on' to save certificates
+
+ axios.post('https://api.ipasign.pro/sign', form, {
+ headers: form.getHeaders()
+ })
+ .then(response => {
+ console.log('Install Link:', response.data.installLink);
+ })
+ .catch(error => {
+ console.error('Error:', error.response ? error.response.data : error.message);
+ });
+ EOF
\ No newline at end of file
diff --git a/.github/workflows/create.yml b/.github/workflows/create.yml
new file mode 100644
index 00000000..836c2791
--- /dev/null
+++ b/.github/workflows/create.yml
@@ -0,0 +1,53 @@
+name: Create New Beta
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build:
+ runs-on: macos-15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Clean Swift Environment
+ run: |
+ rm -rf ~/Library/Developer/Xcode/DerivedData
+ xcodebuild clean
+
+ - name: Install dependencies (packages)
+ run: |
+ curl -LO https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursus7/ldid_macosx_x86_64
+ sudo install -m755 ldid_macosx_x86_64 /usr/local/bin/ldid
+ brew install 7zip gnu-sed
+
+ - name: Compile f
+ run: |
+ mkdir upload
+ # Set optimization level to -Onone
+ make package SCHEME="'feather (Release)'" OPTIMIZATION_LEVEL=-Onone
+ mv packages/* upload/
+
+ - name: Get Version Number
+ id: get_version
+ run: |
+ VERSION=$( /usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" Payload/feather.app/Info.plist )
+ echo "VERSION=${VERSION}" >> $GITHUB_ENV
+
+ - name: Setup
+ run: |
+ mv upload/feather.ipa upload/feather_v${VERSION}.ipa
+ cp upload/feather_v${VERSION}.ipa upload/feather_v${VERSION}.tipa
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ name: Feather v${{ env.VERSION }}
+ tag_name: v${{ env.VERSION }}
+ files: |
+ upload/*ipa
+ generate_release_notes: true
+ fail_on_unmatched_files: false
+ draft: true
+ env:
+ GITHUB_TOKEN: ${{ env.WORKFLOW_SECRET }}
\ No newline at end of file
diff --git a/.github/workflows/idk b/.github/workflows/idk
new file mode 100644
index 00000000..77417fd9
--- /dev/null
+++ b/.github/workflows/idk
@@ -0,0 +1,42 @@
+name: Build iOS Dylib
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Install Xcode Command Line Tools
+ run: xcode-select --install
+
+ - name: Create Framework Project
+ run: |
+ mkdir iOSFramework
+ cd iOSFramework
+ xcodebuild -create-xcframework -framework cpux_lib.framework -output cpux_lib.xcframework
+
+ - name: Create Framework
+ run: |
+ mkdir cpux_lib.framework
+ mkdir cpux_lib.framework/Headers
+ cp cpux_lib.h cpux_lib.framework/Headers/
+ clang -target arm64-apple-ios14.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -fPIC -c cpux_lib.c -o cpux_lib.o
+ libtool -static cpux_lib.o -o cpux_lib.framework/cpux_lib
+ mkdir cpux_lib.framework/Modules
+ echo 'framework module cpux_lib { header "cpux_lib.h" export * }' > cpux_lib.framework/Modules/module.modulemap
+
+ - name: Upload Framework Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: cpux_lib_framework
+ path: iOSFramework/cpux_lib.xcframework
\ No newline at end of file
diff --git a/.github/workflows/idk.yml b/.github/workflows/idk.yml
new file mode 100644
index 00000000..b9e2fe9e
--- /dev/null
+++ b/.github/workflows/idk.yml
@@ -0,0 +1,105 @@
+name: Build iOS Dylib
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+ release:
+ types: [created]
+
+jobs:
+ build:
+ runs-on: macos-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ token: ${{ secrets.WORKFLOW_SECRET }}
+
+ - name: Install Dependencies
+ run: |
+ xcode-select --install || true
+
+ - name: Create iOS Framework
+ run: |
+ mkdir -p cpux_lib.framework/Headers
+ cp cpux_lib.h cpux_lib.framework/Headers/
+ clang -target arm64-apple-ios14.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -fPIC -c cpux_lib.c -o cpux_lib_ios.o
+ libtool -static cpux_lib_ios.o -o cpux_lib.framework/cpux_lib
+ mkdir -p cpux_lib.framework/Modules
+ echo 'framework module cpux_lib { header "cpux_lib.h" export * }' > cpux_lib.framework/Modules/module.modulemap
+
+ - name: Create iOS XCFramework
+ run: |
+ xcodebuild -create-xcframework -framework cpux_lib.framework -output cpux_lib.xcframework
+
+ - name: Zip iOS Framework
+ run: |
+ zip -r cpux_lib.framework.zip cpux_lib.framework
+
+ - name: Zip iOS XCFramework
+ run: |
+ zip -r cpux_lib.xcframework.zip cpux_lib.xcframework
+
+ - name: Create iOS Dylib
+ run: |
+ clang -dynamiclib -target arm64-apple-ios14.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -o libcpux.dylib cpux_lib_ios.o -framework Foundation
+
+ release:
+ runs-on: macos-latest
+ needs: build
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ with:
+ token: ${{ secrets.WORKFLOW_SECRET }}
+
+ - name: Install Dependencies
+ run: |
+ xcode-select --install || true
+
+ - name: Create iOS Framework
+ run: |
+ mkdir -p cpux_lib.framework/Headers
+ cp cpux_lib.h cpux_lib.framework/Headers/
+ clang -target arm64-apple-ios14.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -fPIC -c cpux_lib.c -o cpux_lib_ios.o
+ libtool -static cpux_lib_ios.o -o cpux_lib.framework/cpux_lib
+ mkdir -p cpux_lib.framework/Modules
+ echo 'framework module cpux_lib { header "cpux_lib.h" export * }' > cpux_lib.framework/Modules/module.modulemap
+
+ - name: Create iOS XCFramework
+ run: |
+ xcodebuild -create-xcframework -framework cpux_lib.framework -output cpux_lib.xcframework
+
+ - name: Zip iOS Framework
+ run: |
+ zip -r cpux_lib.framework.zip cpux_lib.framework
+
+ - name: Zip iOS XCFramework
+ run: |
+ zip -r cpux_lib.xcframework.zip cpux_lib.xcframework
+
+ - name: Create iOS Dylib
+ run: |
+ clang -dynamiclib -target arm64-apple-ios14.0 -isysroot $(xcrun --sdk iphoneos --show-sdk-path) -o libcpux.dylib cpux_lib_ios.o -framework Foundation
+
+ - name: Delete Existing Release and Tag
+ env:
+ GITHUB_TOKEN: ${{ secrets.WORKFLOW_SECRET }}
+ run: |
+ gh release delete Dylib -y
+ git push --delete origin Dylib || true
+
+ - name: Create Release and Set Tag
+ env:
+ GITHUB_TOKEN: ${{ secrets.WORKFLOW_SECRET }}
+ run: |
+ git tag Dylib
+ git push origin Dylib
+ gh release create Dylib cpux_lib.framework.zip cpux_lib.xcframework.zip libcpux.dylib --title "Dylib Release" --notes "Release notes for Dylib"
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1094c0dd..8c01ad98 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -1,6 +1,7 @@
name: Create New Release
on:
+
workflow_dispatch:
jobs:
@@ -44,4 +45,4 @@ jobs:
fail_on_unmatched_files: true
draft: true
env:
- GITHUB_TOKEN: ${{ secrets.WORKFLOW_SECRET }}
+ GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/repo.yml b/.github/workflows/repo.yml
index a81ce94a..c318c500 100644
--- a/.github/workflows/repo.yml
+++ b/.github/workflows/repo.yml
@@ -1,8 +1,8 @@
name: Update repo
on:
-
- workflow_dispatch:
+ release:
+ types: [published]
jobs:
update_repo:
@@ -14,7 +14,7 @@ jobs:
- name: Fetch and update release info
run: |
- release_info=$(curl -s https://api.github.com/repos/khcrysalis/Feather/releases/latest)
+ release_info=$(curl -s https://api.github.com/repos/BDGHubNoKey/Backdoor/releases/latest)
clean_release_info=$(echo "$release_info" | tr -d '\000-\037')
@@ -44,9 +44,9 @@ jobs:
size: $size,
downloadURL: $url
}] + .apps[0].versions
- ) | .apps[0].versions |= unique_by(.version)' app-repo.json > updated_app_data.json
+ ) | .apps[0].versions |= unique_by(.version)' App-repo.json > updated_app_data.json
- mv updated_app_data.json app-repo.json
+ mv updated_app_data.json App-repo.json
else
echo "No .ipa file found in the latest release or missing information."
echo "Updated at: $updated_at"
@@ -57,5 +57,5 @@ jobs:
uses: EndBug/add-and-commit@v9
with:
default_author: github_actions
- message: "chore: update repo"
- add: app-repo.json
+ message: "chore: update App-repo.json"
+ add: App-repo.json
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d5ec250a..72061e82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,4 @@ packages*
*xcuserstate*
Payload*
Payload
-/.vscode
+/.vscode
\ No newline at end of file
diff --git a/App-repo.json b/App-repo.json
new file mode 100644
index 00000000..85730583
--- /dev/null
+++ b/App-repo.json
@@ -0,0 +1,28 @@
+{
+ "name": "Backdoor Repository",
+ "identifier": "com.bdg.backdoor-repo",
+ "iconURL": "https://raw.githubusercontent.com/814bdg/App/refs/heads/main/Wing3x.png?raw=true",
+ "apps": [
+ {
+ "name": "Backdoor",
+ "bundleIdentifier": "com.bdg.backdoor",
+ "developerName": "BDG",
+ "iconURL": "https://raw.githubusercontent.com/814bdg/App/refs/heads/main/Wing3x.png?raw=true",
+ "localizedDescription": "Backdoor is a free on-device iOS application manager/installer.",
+ "subtitle": "On-device signing application",
+ "tintColor": "848ef9",
+ "versions": [
+ {
+ "version": "0.0.8",
+ "date": "2025-03-14T22:42:33Z",
+ "size": 12684769,
+ "downloadURL": "https://github.com/BDGHubNoKey/Backdoor/releases/download/v0.0.8/feather_v0.0.8.ipa"
+ }
+ ],
+ "size": 12684769,
+ "version": "0.0.8",
+ "versionDate": "2025-03-14T22:42:33Z",
+ "downloadURL": "https://github.com/BDGHubNoKey/Backdoor/releases/download/v0.0.8/feather_v0.0.8.ipa"
+ }
+ ]
+}
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
deleted file mode 100644
index 8ea5c782..00000000
--- a/CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# Code of Conduct
-
-Welcome to the Feather Code of Conduct. This document outlines essential guidelines and important information regarding the use of the tool.
-
-## DONT's
-- **Feather is not intended for piracy** and has never been considered as such; so please refrain from engaging in piracy while utilizing this tool.
-
-- **Any type of discrimination is not allowed**, you will be blocked from interacting with the repository if you use this as a way to make fun of other users.
-
-- **Trolling or spam.**
-
-## DO's
-
-- **Be respectful** when interacting in issues or us in general
-
-- **Give good criticsm!** Most issues and pull-requests will be noticed, if some are actually required we will do something about it. Such as...
- - Using of proper pronounciation
- - Changing any strings that may be harmful to a specific individual or group of people
- - Potential license violations
- - Code cleanup
- - Any security issues that don't involve CoreData or Documents directory, or jailbreaking
-
-- **Have fun sideloading!**
-
-#
-
-Multiple violations of these rules could end up you being restricted from the repository.
-
diff --git a/FAQ.md b/FAQ.md
deleted file mode 100644
index d9ca97c0..00000000
--- a/FAQ.md
+++ /dev/null
@@ -1,49 +0,0 @@
-## FAQ
-
-Q: How does feather work?
-
-A: Feather allows you to import a `.p12` and a `.mobileprovision` pair to sign the application with (you will need a correct password to the p12 before importing). [Zsign](https://github.com/zhlynn/zsign) is used for the signing aspect, feather feeds it the certificates you have selected in its certificates tab and will sign the app on your device - after its finished it will now be added to your signed applications tab. When selected, it will take awhile as its compressing and will prompt you to install it.
-
-#
-
-**Q: What does Feather use for its server?**
-
-A: It uses the [localhost.direct](https://github.com/Upinel/localhost.direct) certificate and [Vapor](https://github.com/vapor/vapor) to self host a HTTPS server on your device - all itms services really needs is a valid certificate and a valid HTTPS server. Which allows iOS to accept the request and install the application.
-
-#
-
-**Q: Does Feather bundle its own certificate for the server?**
-
-A: Yes, to be able to install applications on device the server needs to be HTTPS. Which, we use a localhost.direct certificate for when turning on the server while attempting to install.
-
-We have an option to download a new certificate to make this server be able to run in the far future but no guarantees. It entirely depends on the owners of localhost.direct to be able to provide a certificate for use. If it does expire and theres a new one available, hopefully we'll be there to update the files in the background so Feather is able to retrieve those.
-
-#
-
-**Q: Notifications aren't working**
-
-A: This is because of a default setting applied when using Feather, read below.
-
-#
-
-**Q: Why does Feather append a random string on the bundle ID?**
-
-A: New ADP (Apple Developer Program) memberships created after June 6, 2021, require development and ad-hoc signed apps for iOS, iPadOS, and tvOS to check with a PPQ (Provisioning Profile Query Check) service when the app is first launched. The device must be connected to the internet to verify.
-
-PPQCheck checks for a similar bundle identifier on the App Store, if said identifier matches the app you're launching and is happened to be signed with a non-appstore certificate, your Apple ID may be flagged and even banned from using the program for any longer.
-
-This is why we prepend the random string before each identifier, its done as a safety meassure - however you can disable it if you *really* want to in Feathers settings page.
-
-#
-
-**Q: What is remove dylib inside of options?**
-
-A: There's a very specific reason its there, for those wanting to remove pre-existing injected dylibs inside but it really serves no other practical use other than that. Don't use this if you have no idea what you're doing.
-
-#
-
-**Q: What about free developer accounts?**
-
-A: Sadly Feather is unlikely to ever support those as there are plenty of alternatives that exist! Here's a few: [Altstore](https://altstore.io), [Sideloadly](https://sideloadly.io/)
-
-#
\ No newline at end of file
diff --git a/Makefile b/Makefile
index c8ca6e66..6d2072f9 100644
--- a/Makefile
+++ b/Makefile
@@ -56,6 +56,4 @@ clean:
@rm -rf apple-include
@rm -rf $(APP_TMP)
-.PHONY: apple-include
-
-
+.PHONY: apple-include
\ No newline at end of file
diff --git a/README.md b/README.md
deleted file mode 100644
index ec25dafd..00000000
--- a/README.md
+++ /dev/null
@@ -1,88 +0,0 @@
-
-# Feather
-[](https://github.com/khcrysalis/feather/releases)
-[](https://github.com/khcrysalis/feather/releases)
-[](https://github.com/khcrysalis/feather/blob/main/LICENSE)
-
-Feather allows you to use an Apple Developer Account to sign and install applications on device without needing a computer on stock iOS versions, while allowing easy management with its applications.
-
-Due to limitations, it's hard to tell if the application is actually installed, so you will need to keep track of whats on your device. This is an entirely stock application and uses built-in features to be able to do this!
-
-## Features
-
-- Altstore repo support.
-- Import your own `.ipa`'s.
-- Inject tweaks when signing apps.
-- Install applications straight to your device seamlessly over the air.
-- Allows multiple certificate imports for easy switching.
-- Configurable signing options.
-- Meant to be used with Apple Accounts that are apart of `ADP` (Apple Developer Program).
-- No tracking, analytics, or any of the sort.
-
-## Preview
-
-|
|
|
|
|
-|:--:|:--:|:--:|:--:|
-| **Sources** | **Store** | **Library** | **Signing** |
-
-## Building
-
-#### Minimum requirements
-
-- Xcode 15
-- Swift 5.9
-- iOS 15
-
-Feather is not exactly as light as a feather as it needs to include an entire server framework so it can host it's server locally, totaling around 40mb~ when successfully compiled. While this is annoying to me, it doesn't really matter at the end as it does it's job.
-
-1. Clone repository
- ```sh
- git clone https://github.com/khcrysalis/Feather
- ```
-
-2. Compile
- ```sh
- cd Feather
- gmake package SCHEME="'feather (Release)'" # Build, Use `SCHEME="'feather (Debug)'"` for debug build
- ```
-
-3. Updating
- ```sh
- git pull
- ```
-
-Using the makefile will automatically create an unsigned ipa inside the packages directory, using this to debug or report issues is not recommend. When making a pull request or reporting issues, it's generally advised you've used Xcode to debug your changes properly.
-
-## Sponsors
-
-| Thanks to all my [sponsors](https://github.com/sponsors/khcrysalis)!! |
-|:-:|
-| |
-| _**"samara is cute" - Vendicated**_ |
-
-## Star History
-
-
-
-
-
-
-
-
-
-## Acknowledgements
-
-- ~~[localhost.direct](https://github.com/Upinel/localhost.direct) - localhost with public CA signed SSL certificate~~
-- [*.backloop.dev](https://backloop.dev/) - localhost with public CA signed SSL certificate
-- [Vapor](https://github.com/vapor/vapor) - A server-side Swift HTTP web framework.
-- [Zsign](https://github.com/zhlynn/zsign) - Allowing to sign on-device, reimplimented to work on other platforms such as iOS.
-- [Nuke](https://github.com/kean/Nuke) - Image caching.
-- [Asspp](https://github.com/Lakr233/Asspp) - Some code for setting up the http server.
-- [plistserver](https://github.com/nekohaxx/plistserver) - Hosted on https://api.palera.in
-
-## License
-
-This project is licensed under the GPL-3.0 license. You can see the full details of the license [here](https://github.com/khcrysalis/Feather/blob/main/LICENSE). It's under this specific license because I wanted to make a project that is transparent to the user thats related to Apple Developer Account sideloading, before this project there weren't any open source projects that filled in this gap.
-
-By contributing to this project, you agree to license your code under the GPL-3.0 license as well, ensuring that your work, like all other contributions, remains freely accessible and open.
-
diff --git a/Shared/CPU-X/CPUXLib.c b/Shared/CPU-X/CPUXLib.c
new file mode 100644
index 00000000..cf683b40
--- /dev/null
+++ b/Shared/CPU-X/CPUXLib.c
@@ -0,0 +1,119 @@
+// CPUXLib.c
+
+ #include "CPUXLib.h"
+ #include
+ #include
+ #include
+ #include
+ #include
+ #include
+
+ #ifdef __APPLE__
+ #include
+ #include
+ #include
+ #include
+ #endif
+
+ // Helper function to handle sysctl errors
+ static int sysctl_safe(const int* name, u_int namelen, void* oldp, size_t* oldlenp, const void* newp, size_t newlen) {
+ if (sysctl(name, namelen, oldp, oldlenp, newp, newlen) != 0) {
+ perror("sysctl failed");
+ return -1;
+ }
+ return 0;
+ }
+
+ // Helper function to handle sysctlbyname errors
+ static int sysctlbyname_safe(const char* name, void* oldp, size_t* oldlenp, const void* newp, size_t newlen) {
+ if (sysctlbyname(name, oldp, oldlenp, newp, newlen) != 0) {
+ perror("sysctlbyname failed");
+ return -1;
+ }
+ return 0;
+ }
+
+ CPUInfo* getCPUInfo() {
+ CPUInfo* info = (CPUInfo*)malloc(sizeof(CPUInfo));
+ if (!info) {
+ fprintf(stderr, "Error: Could not allocate memory for CPUInfo.\n");
+ return NULL;
+ }
+
+ memset(info, 0, sizeof(CPUInfo));
+
+ #ifdef __APPLE__
+ size_t size = sizeof(info->model);
+ if (sysctlbyname_safe("hw.machine", &info->model, &size, NULL, 0) != 0) {
+ strncpy(info->model, "Unknown", sizeof(info->model) - 1);
+ info->model[sizeof(info->model) - 1] = '\0';
+ }
+
+ int coreCount, threadCount;
+ size = sizeof(coreCount);
+ if (sysctlbyname_safe("hw.physicalcpu", &coreCount, &size, NULL, 0) == 0) {
+ info->coreCount = coreCount;
+ }
+ size = sizeof(threadCount);
+ if (sysctlbyname_safe("hw.logicalcpu", &threadCount, &size, NULL, 0) == 0) {
+ info->threadCount = threadCount;
+ }
+
+ char cpuBrand[256];
+ size = sizeof(cpuBrand);
+ if (sysctlbyname_safe("machdep.cpu.brand_string", &cpuBrand, &size, NULL, 0) == 0) {
+ strncpy(info->cpuBrand, cpuBrand, sizeof(info->cpuBrand) - 1);
+ info->cpuBrand[sizeof(info->cpuBrand) - 1] = '\0';
+ } else {
+ strncpy(info->cpuBrand, "Unknown", sizeof(info->cpuBrand) - 1);
+ info->cpuBrand[sizeof(info->cpuBrand) - 1] = '\0';
+ }
+
+ #else
+ strncpy(info->model, "Unknown", sizeof(info->model) - 1);
+ info->model[sizeof(info->model) - 1] = '\0';
+ strncpy(info->cpuBrand, "Unknown", sizeof(info->cpuBrand) - 1);
+ info->cpuBrand[sizeof(info->cpuBrand) - 1] = '\0';
+ #endif
+ return info;
+ }
+
+ void freeCPUInfo(CPUInfo* info) {
+ free(info);
+ }
+
+ MemoryInfo* getMemoryInfo() {
+ MemoryInfo* memInfo = (MemoryInfo*)malloc(sizeof(MemoryInfo));
+ if (!memInfo) {
+ fprintf(stderr, "Error: Could not allocate memory for MemoryInfo.\n");
+ return NULL;
+ }
+
+ #ifdef __APPLE__
+ int mib= {CTL_HW, HW_MEMSIZE};
+ size_t length = sizeof(memInfo->totalMemory);
+ if (sysctl_safe(mib, 2, &memInfo->totalMemory, &length, NULL, 0) != 0) {
+ fprintf(stderr, "Error: sysctl failed to get total memory.\n");
+ free(memInfo);
+ return NULL;
+ }
+
+ vm_statistics64_data_t vm_stats;
+ mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
+ if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&vm_stats, &count) == KERN_SUCCESS) {
+ memInfo->freeMemory = (int64_t)vm_stats.free_count * vm_page_size;
+ } else {
+ fprintf(stderr, "Error: host_statistics64 failed.\n");
+ }
+
+ #else
+ memInfo->totalMemory = 0;
+ memInfo->freeMemory = 0;
+ #endif
+
+ return memInfo;
+ }
+
+ void freeMemoryInfo(MemoryInfo* info) {
+ free(info);
+ }
\ No newline at end of file
diff --git a/Shared/CPU-X/CPUXLib.h b/Shared/CPU-X/CPUXLib.h
new file mode 100644
index 00000000..125a0679
--- /dev/null
+++ b/Shared/CPU-X/CPUXLib.h
@@ -0,0 +1,34 @@
+// CPUXLib.h
+
+ #ifndef CPUX_LIB_H
+ #define CPUX_LIB_H
+
+ #ifdef __cplusplus
+ extern "C" {
+ #endif
+
+ #include
+
+ typedef struct {
+ char model[256];
+ char cpuBrand[256];
+ uint32_t coreCount;
+ uint32_t threadCount;
+ } CPUInfo;
+
+ typedef struct {
+ uint64_t totalMemory;
+ uint64_t freeMemory;
+ } MemoryInfo;
+
+ CPUInfo* getCPUInfo();
+ MemoryInfo* getMemoryInfo();
+
+ void freeCPUInfo(CPUInfo* info);
+ void freeMemoryInfo(MemoryInfo* info);
+
+ #ifdef __cplusplus
+ }
+ #endif
+
+ #endif /* CPUX_LIB_H */
\ No newline at end of file
diff --git a/Shared/CPU-X/InfoProvider.h b/Shared/CPU-X/InfoProvider.h
new file mode 100644
index 00000000..d81b474a
--- /dev/null
+++ b/Shared/CPU-X/InfoProvider.h
@@ -0,0 +1,91 @@
+// InfoProvider.m
+
+ #import "InfoProvider.h"
+
+ @interface InfoProvider ()
+
+ @property (nonatomic, strong) UIButton *floatingButton;
+ @property (nonatomic, strong) UIWindow *infoWindow;
+
+ @end
+
+ @implementation InfoProvider
+
+ + (instancetype)sharedProvider {
+ static InfoProvider *sharedProvider = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ sharedProvider = [[InfoProvider alloc] init];
+ });
+ return sharedProvider;
+ }
+
+ - (instancetype)init {
+ self = [super init];
+ if (self) {
+ [self setupFloatingButton];
+ }
+ return self;
+ }
+
+ - (void)setupFloatingButton {
+ self.floatingButton = [UIButton buttonWithType:UIButtonTypeSystem];
+ self.floatingButton.frame = CGRectMake(20, 60, 60, 60);
+ [self.floatingButton setTitle:@"Info" forState:UIControlStateNormal];
+ self.floatingButton.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8];
+ self.floatingButton.layer.cornerRadius = 30;
+ self.floatingButton.clipsToBounds = YES;
+ [self.floatingButton addTarget:self action:@selector(toggleInfo) forControlEvents:UIControlEventTouchUpInside];
+ self.floatingButton.windowLevel = UIWindowLevelAlert + 1;
+
+ // Find the key window and add the button
+ UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
+ if (keyWindow) {
+ [keyWindow addSubview:self.floatingButton];
+ }
+ }
+
+ - (void)toggleInfo {
+ if (self.infoWindow) {
+ [self hideInfo];
+ } else {
+ [self showInfo];
+ }
+ }
+
+ - (void)showInfo {
+ CPUInfo *cpuInfo = getCPUInfo();
+ MemoryInfo *memInfo = getMemoryInfo();
+
+ self.infoWindow = [[UIWindow alloc] initWithFrame:CGRectMake(80, 80, 250, 200)];
+ self.infoWindow.windowLevel = UIWindowLevelAlert;
+ self.infoWindow.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8];
+ self.infoWindow.layer.cornerRadius = 10;
+ self.infoWindow.clipsToBounds = YES;
+
+ UILabel *cpuLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 230, 80)];
+ cpuLabel.numberOfLines = 0;
+ cpuLabel.textColor = [UIColor whiteColor];
+ cpuLabel.font = [UIFont systemFontOfSize:14];
+ cpuLabel.text = [NSString stringWithFormat:@"Model: %s\nBrand: %s\nCores: %u\nThreads: %u", cpuInfo->model, cpuInfo->cpuBrand, cpuInfo->coreCount, cpuInfo->threadCount];
+ [self.infoWindow addSubview:cpuLabel];
+
+ UILabel *memLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 230, 80)];
+ memLabel.numberOfLines = 0;
+ memLabel.textColor = [UIColor whiteColor];
+ memLabel.font = [UIFont systemFontOfSize:14];
+ memLabel.text = [NSString stringWithFormat:@"Total Memory: %llu bytes\nFree Memory: %llu bytes", memInfo->totalMemory, memInfo->freeMemory];
+ [self.infoWindow addSubview:memLabel];
+
+ [self.infoWindow makeKeyAndVisible];
+
+ freeCPUInfo(cpuInfo);
+ freeMemoryInfo(memInfo);
+ }
+
+ - (void)hideInfo {
+ [self.infoWindow removeFromSuperview];
+ self.infoWindow = nil;
+ }
+
+ @end
\ No newline at end of file
diff --git a/Shared/CPU-X/Makefile b/Shared/CPU-X/Makefile
new file mode 100644
index 00000000..776079ee
--- /dev/null
+++ b/Shared/CPU-X/Makefile
@@ -0,0 +1,17 @@
+# Makefile
+
+ TARGET = SystemInfo
+
+ include $(THEOS)/makefiles/common.mk
+
+ TUSD_FRAMEWORKS = UIKit
+
+ ARCHS = iphoneos-arm64
+
+ include $(THEOS_MAKE_PATH)/tweak.mk
+
+ $(TARGET)_FILES = InfoProvider.m CPUXLib.c
+
+ $(TARGET)_PRIVATE_FRAMEWORKS = CoreGraphics
+
+ INSTALL_TARGET_PROCESSES = SpringBoard
\ No newline at end of file
diff --git a/Shared/CPU-X/Tweak.xm b/Shared/CPU-X/Tweak.xm
new file mode 100644
index 00000000..34451449
--- /dev/null
+++ b/Shared/CPU-X/Tweak.xm
@@ -0,0 +1,80 @@
+// Tweak.xm
+
+ #import
+ #import "cpux_lib.h"
+
+ static UIWindow *infoWindow = nil;
+ static UIButton *floatingButton = nil;
+
+ %hook UIWindow
+
+ - (void)makeKeyAndVisible {
+ %orig;
+
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ [self setupFloatingButton];
+ });
+ }
+
+ - (void)setupFloatingButton {
+ floatingButton = [UIButton buttonWithType:UIButtonTypeSystem];
+ floatingButton.frame = CGRectMake(20, 60, 60, 60);
+ [floatingButton setTitle:@"Info" forState:UIControlStateNormal];
+ floatingButton.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8];
+ floatingButton.layer.cornerRadius = 30;
+ floatingButton.clipsToBounds = YES;
+ [floatingButton addTarget:self action:@selector(toggleInfo) forControlEvents:UIControlEventTouchUpInside];
+ floatingButton.windowLevel = UIWindowLevelAlert + 1;
+
+ // Find the key window and add the button
+ UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
+ if (keyWindow) {
+ [keyWindow addSubview:floatingButton];
+ }
+ }
+
+ - (void)toggleInfo {
+ if (infoWindow) {
+ [self hideInfo];
+ } else {
+ [self showInfo];
+ }
+ }
+
+ - (void)showInfo {
+ CPUInfo *cpuInfo = getCPUInfo();
+ MemoryInfo *memInfo = getMemoryInfo();
+
+ infoWindow = [[UIWindow alloc] initWithFrame:CGRectMake(80, 80, 250, 200)];
+ infoWindow.windowLevel = UIWindowLevelAlert;
+ infoWindow.backgroundColor = [UIColor colorWithWhite:0.2 alpha:0.8];
+ infoWindow.layer.cornerRadius = 10;
+ infoWindow.clipsToBounds = YES;
+
+ UILabel *cpuLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 230, 80)];
+ cpuLabel.numberOfLines = 0;
+ cpuLabel.textColor = [UIColor whiteColor];
+ cpuLabel.font = [UIFont systemFontOfSize:14];
+ cpuLabel.text = [NSString stringWithFormat:@"Model: %s\nBrand: %s\nCores: %u\nThreads: %u", cpuInfo->model, cpuInfo->cpuBrand, cpuInfo->coreCount, cpuInfo->threadCount];
+ [infoWindow addSubview:cpuLabel];
+
+ UILabel *memLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 230, 80)];
+ memLabel.numberOfLines = 0;
+ memLabel.textColor = [UIColor whiteColor];
+ memLabel.font = [UIFont systemFontOfSize:14];
+ memLabel.text = [NSString stringWithFormat:@"Total Memory: %llu bytes\nFree Memory: %llu bytes", memInfo->totalMemory, memInfo->freeMemory];
+ [infoWindow addSubview:memLabel];
+
+ [infoWindow makeKeyAndVisible];
+
+ freeCPUInfo(cpuInfo);
+ freeMemoryInfo(memInfo);
+ }
+
+ - (void)hideInfo {
+ [infoWindow removeFromSuperview];
+ infoWindow = nil;
+ }
+
+ %end
\ No newline at end of file
diff --git a/Shared/Localizations/en.lproj/Localizable.strings b/Shared/Localizations/en.lproj/Localizable.strings
index 3235a33d..d5dbf2ed 100644
--- a/Shared/Localizations/en.lproj/Localizable.strings
+++ b/Shared/Localizations/en.lproj/Localizable.strings
@@ -1,28 +1,45 @@
-/*
+/*
Localizable.strings
feather
Created by samara on 25.08.2024.
+
*/
// MARK: - Generic
+// Unknown, default string for applications
"UNKNOWN" = "Unknown";
+// Default string
"DEFAULT" = "Default";
+// Copy provided string in a menu
"COPY" = "Copy";
+// Delete item
"DELETE" = "Delete";
+// Cancel alert action
"CANCEL" = "Cancel";
+// Done action
"DONE" = "Done";
+// Dismiss action
"DISMISS" = "Dismiss";
+// Dismiss 2 action
"LAME" = "Lame";
+// Save action
"SAVE" = "Save";
+// Set action
"SET" = "Save";
+// OK action
"OK" = "OK";
+// Continue action
"CONTINUE" = "Continue";
+// Import Action
"IMPORT" = "Import";
+// Add Action
"ADD" = "Add";
+// Install Action
"INSTALL" = "Install";
// MARK: - Alerts
+// Alert titles
"ALERT_SUCCESS" = "Success";
"ALERT_TRACE" = "Trace";
"ALERT_ERROR" = "Error";
@@ -30,46 +47,70 @@
"ALERT_COPIED" = "Copied";
// MARK: - Error messages
+// Installer Error Title
"ERROR_INSTALLER" = "Installer";
+// Installer Error Description
"ERROR_INSTALLER_DESCRIPTION" = "Failed to load SSL certificates";
"ERROR_ZSIGN_FAILED" = "Signing failed.";
"ERROR_FAILED_TO_READ_MOBILEPROVISION" = "Failed to read mobileprovision file";
// MARK: - Success messages
+// Successfully signed an application
"SUCCESS_SIGNED" = "Successfully signed %@";
+// Successfully resigned an application
"SUCCESS_RESIGN" = "Successfully resigned!";
+// Success message requiring user to restart app
"SUCCESS_REQUIRES_RESTART" = "You must close the app for changes to take effect.";
// MARK: - Onboarding
+// Welcome to Feather!!!!!!!!
"ONBOARDING_WELCOMETITLE_1" = "Welcome to";
+// First feature inside of onboarding
"ONBOARDING_CELL_1_TITLE" = "Sideload On Device";
"ONBOARDING_CELL_1_DESCRIPTION" = "Sideload apps without a computer, all done from your device.";
+// Second feature inside of onboarding
"ONBOARDING_CELL_2_TITLE" = "Customize Apps";
"ONBOARDING_CELL_2_DESCRIPTION" = "Manage and customize your apps for your needs.";
-"ONBOARDING_CELL_3_TITLE" = "And Many More Thing’s.";
-"ONBOARDING_CELL_3_DESCRIPTION" = "Altstore, Esign, Scarlet any repo’s you got work. import IPA's easily, easy certificate management, and many more thing’s.";
-"ONBOARDING_FOOTER" = "Developed by BDG. Made for users who want to sideload & have freedom without the need of a computer. Packed with many features included inside.";
+// Third feature inside of onboarding
+"ONBOARDING_CELL_3_TITLE" = "And Much Much More!";
+"ONBOARDING_CELL_3_DESCRIPTION" = "AltStore, Esign, Scarlet, trollstore any repositories, import IPA's, easy certificate management, and much much more.";
+// "Developed by BDG. Made for users who are passionate for sideloading and freedom. Many features included inside. Learn more..."
+"ONBOARDING_FOOTER" = "Developed by BDG. Made for users who are passionate for sideloading and freedom. Many features included inside.";
"ONBOARDING_FOOTER_LINK" = "Learn more...";
+// Continue button to exit onboarding
"ONBOARDING_CONTINUE_BUTTON" = "Continue";
// MARK: - Tab area
+// Sources tab
"TAB_SOURCES" = "Sources";
+// Library tab
"TAB_LIBRARY" = "Library";
+// Settings tab
"TAB_SETTINGS" = "Settings";
// MARK: - TransferPreview
+// Packaging application to get ready for install
"TRANSFER_PREVIEW_PACKAGING" = "Packaging...";
+// Ready to install package
"TRANSFER_PREVIEW_READY" = "Ready To Install";
+// Opening manifest.plist for iOS
"TRANSFER_PREVIEW_SENDING_MANIFEST" = "Sending Manifest...";
+// iOS is retrieving IPA file
"TRANSFER_PREVIEW_SENDING_PAYLOAD" = "Sending Payload...";
+// Done transferring IPA file
"TRANSFER_PREVIEW_DONE" = "Done";
+// Completed packaging so you can share
"TRANSFER_PREVIEW_COMPLETED" = "Completed";
// MARK: - SourcesViewController
+// Repositories title
"SOURCES_VIEW_CONTROLLER_REPOSITORIES" = "Repositories";
+// Add repo button
"SOURCES_VIEW_CONTROLLER_ADD_SOURCES" = "Add Repo";
+// Number of sources, i.e. "100 Sources"
"SOURCES_VIEW_CONTROLLER_NUMBER_OF_SOURCES" = "%@ Source";
"SOURCES_VIEW_CONTROLLER_NUMBER_OF_SOURCES_PLURAL" = "%@ Sources";
+// Search sources in search bar
"SOURCES_VIEW_CONTROLLER_SEARCH_SOURCES" = "Search Sources";
// MARK: - SourcesViewController - Add Sources
@@ -78,39 +119,55 @@
"SOURCES_VIEW_ADD_SOURCES_ALERT_BUTTON_IMPORT_REPO" = "Import Repositories";
"SOURCES_VIEW_ADD_SOURCES_ALERT_BUTTON_EXPORT_REPO" = "Export Repositories";
"SOURCES_VIEW_ADD_SOURCES_ALERT_BUTTON_EXPORT_REPO_ACTION_SUCCESS" = "Copied Repositories to Clipboard";
+//Footer Validation
"SOURCES_VIEW_ADD_SOURCES_FOOTER_NOTSTARTED" = "Enter a URL to start validation.";
"SOURCES_VIEW_ADD_SOURCES_FOOTER_NOTVALIDJSON" = "Invalid JSON, please check your input.";
"SOURCES_VIEW_ADD_SOURCES_FOOTER_VALID" = "Valid JSON entered.";
// MARK: - SourcesViewController -> SourcesAppViewController
+// Number of apps, i.e. "444 Apps"
"SOURCES_APP_VIEW_CONTROLLER_NUMBER_OF_APPS" = "%@ App";
"SOURCES_APP_VIEW_CONTROLLER_NUMBER_OF_APPS_PLURAL" = "%@ Apps";
+// Search apps in search bar
"SOURCES_APP_VIEW_CONTROLLER_SEARCH_APPS" = "Search Apps";
// MARK: - SourcesViewController ... - Cells
+// Default subtitle string
"SOURCES_CELLS_DEFAULT_SUBTITLE" = "An awesome application.";
+// Default description string
"SOURCES_CELLS_DEFAULT_DESCRIPTION" = "A cool description.";
// MARK: - SourceAppViewController ... - Actions
+// Available Versions Alert Title
"SOURCES_CELLS_ACTIONS_HOLD_AVAILABLE_VERSIONS" = "Available Versions";
+//Filter Menu
"SOURCES_CELLS_ACTIONS_FILTER_TITLE" = "Filter by";
"SOURCES_CELLS_ACTIONS_FILTER_BY_DEFAULT" = "Default";
"SOURCES_CELLS_ACTIONS_FILTER_BY_NAME" = "Name";
"SOURCES_CELLS_ACTIONS_FILTER_BY_DATE" = "Date";
// MARK: - AppsInformationViewController
+// Application Section
"APPS_INFORMATION_SECTION_TITLE_NAME" = "Application";
+// Bundle Section
+"APPS_INFORMATION_SECTION_TITLE_NAME" = "Bundle";
+
+//
"APPS_INFORMATION_TITLE_DELETED_FILE" = "Deleted File";
"APPS_INFORMATION_TITLE_DELETED_FILE_TITLE" = "File has been deleted.";
"APPS_INFORMATION_TITLE_DELETED_FILE_DESCRIPTION" = "This is a useless entry, it does not have a file and Feather will not allow you to install it. It's recommended you delete by swiping on the cell in the Apps tab.";
+// Application Section
"APPS_INFORMATION_TITLE_NAME" = "Name";
"APPS_INFORMATION_TITLE_VERSION" = "Version";
"APPS_INFORMATION_TITLE_IDENTIFIER" = "Identifier";
"APPS_INFORMATION_TITLE_SIZE" = "Size";
+//
"APPS_INFORMATION_TITLE_DATE_ADDED" = "Date Added";
+// Bundle Section
"APPS_INFORMATION_TITLE_BUNDLE_NAME" = "Bundle Name";
"APPS_INFORMATION_TITLE_BUNDLE_PATH" = "Bundle Path";
"APPS_INFORMATION_TITLE_ICON_FILE" = "Icon File";
+//
"APPS_INFORMATION_TITLE_OPEN_IN_FILES" = "Open in Files";
// MARK: - AppSigningViewController
@@ -136,54 +193,76 @@
"APP_SIGNING_TWEAK_VIEW_CONTROLLER_TITLE" = "Tweaks";
// MARK: - SigningsOptionsViewController
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_PROTECTIONS" = "Enable Protection";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_PROTECTIONS_DESCRIPTION" = "Enabling protection will append a random string to each bundle identifier, this is to protect the Apple ID related to your certificate from being flagged by Apple. However, if you don't care about this you can ignore it.";
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_DYNAMIC_PROTECTION" = "Dynamic Protection";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_DYNAMIC_PROTECTION_DESCRIPTION" = "Dynamic protection will only apply PPQ protection if the bundle identifier exists on the App Store. This requires an internet connection during signing.";
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_IDENTIFIERS" = "Bundle Identifiers";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_IDENTIFIERS_NEW" = "New Identifier";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_IDENTIFIERS_ID" = "Identifier";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_IDENTIFIERS_ID_REPLACEMENT" = "Replacement";
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_DISPLAYNAMES" = "Display Names";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_DISPLAYNAMES_ID" = "Original Display Name";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_DISPLAYNAMES_ID_REPLACEMENT" = "New Display Name";
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_IMMEDIATELY_INSTALL_FROM_SOURCE" = "Immediately Install from Source";
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_IMMEDIATELY_INSTALL_FROM_SOURCE_DESCRIPTION" = "When enabled, apps downloaded from sources will prompt to sign and install after downloading.";
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_INSTALLAFTERSIGNED" = "Install after Signing";
+
"APP_SIGNING_VIEW_CONTROLLER_CELL_SIGNING_OPTIONS_TITLE" = "Signing Options";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_PLUGINS" = "Remove all PlugIns";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_PLUGINS_DESCRIPTION" = "Removes the PlugIns directory inside of the app, which would usually have some components for the app to function properly.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_UISUPPORTEDDEVICES" = "Remove UISupportedDevices";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_UISUPPORTEDDEVICES_DESCRIPTION" = "Removes device restrictions for the application.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_URLSCHEME" = "Remove URLScheme";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_URLSCHEME_DESCRIPTION" = "Removes any possible URL schemes (i.e. 'feather://')";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_ALLOW_BROWSING_DOCUMENTS" = "Allow Browsing Documents";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_ALLOW_BROWSING_DOCUMENTS_DESCRIPTION" = "Allows other apps to open and edit the files stored in the Documents folder. This option also lets users set the app’s default save location in Settings.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_ALLOW_ITUNES_SHARING" = "Allow iTunes Sharing";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_ALLOW_ITUNES_SHARING_DESCRIPTION" = "Forces the app to share their documents directory, allowing sharing between iTunes and Finder.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_FORCE_PRO_MOTION" = "Force ProMotion";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_FORCE_PRO_MOTION_DESCRIPTION" = "Enables ProMotion capabilities within the app, however on lower versions of 15.x this may not be enough.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_FORCE_GAME_MODE" = "Force Game Mode";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_FORCE_GAME_MODE_DESCRIPTION" = "Enables Game Mode within the app, minimizing background activity and prioritized performance for the app.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_FORCE_FULLSCREEN" = "Force Fullscreen";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_FORCE_FULLSCREEN_DESCRIPTION" = "Forces only fullscreen capabilities within iPad apps, disallowing sharing the screen with other apps. On an external screen, the window for an app with this setting maintains its canvas size.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_DELETE_PLACEHOLDER_WATCH_APP" = "Delete Placeholder Watch App";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_DELETE_PLACEHOLDER_WATCH_APP_DESCRIPTION" = "Removes unwanted watch placeholder which isn't supposed to be there, present in apps such as YouTube music, etc.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_FORCELOCALIZATIONS" = "Force Try To Localize";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_FORCELOCALIZATIONS_DESCRIPTION" = "Forces localization by modifying every localizable bundle within the app when trying to change a name of the app.";
+
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_PROVISIONING" = "Remove Provisioning File";
"APP_SIGNING_INPUT_VIEW_CONTROLLER_REMOVE_PROVISIONING_DESCRIPTION" = "Removes .mobileprovison from appearing in your app after signing.";
// MARK: - LibraryViewController
+//Search Library placeholder
"SETTINGS_VIEW_CONTROLLER_SEARCH_PLACEHOLDER" = "Search Library";
+//Headers
"LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_SIGNED_APPS" = "Signed Apps";
"LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_SIGNED_APPS_TOTAL" = "%@ Signed";
"LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_SIGNED_APPS_TOTAL_PLURAL" = "%@ Signed";
"LIBRARY_VIEW_CONTROLLER_SECTION_BUTTON_IMPORT" = "Import";
"LIBRARY_VIEW_CONTROLLER_SECTION_DOWNLOADED_APPS" = "Downloaded Apps";
"LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_DOWNLOADED_APPS_TOTAL" = "%@ Downloaded";
+//Import Action Sheet
"LIBRARY_VIEW_CONTROLLER_IMPORT_ACTION_SHEET_FILE" = "Import from Files";
"LIBRARY_VIEW_CONTROLLER_IMPORT_ACTION_SHEET_URL" = "Import from URL";
+//Sign Action Sheet
"LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_SIGN" = "Sign %@";
"LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_SIGN_INSTALL" = "Sign & Install %@";
"LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_RESIGN" = "ReSign %@";
@@ -198,15 +277,22 @@
"LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_INSTALL_CONFIRM_DESCRIPTION" = "Trying to install via the downloaded apps tab may not work as they are most likely not signed! It's recommended you sign that application first before installing.";
// MARK: - SettingsViewController
+// Headers
"SETTINGS_VIEW_CONTROLLER_SECTION_TITLE_GENERAL" = "General";
"SETTINGS_VIEW_CONTROLLER_SECTION_TITLE_SIGNING" = "Signing";
"SETTINGS_VIEW_CONTROLLER_SECTION_TITLE_SIGNING_SERVER" = "Signing Server";
-"SETTINGS_VIEW_CONTROLLER_SECTION_FOOTER_ISSUES" = "If any issues occur within Feather please report it via the GitHub repository. When submitting an issue, be sure to submit any logs.";
+
+// Footers
+"SETTINGS_VIEW_CONTROLLER_SECTION_FOOTER_ISSUES" = "If any issues occur within Backdoor please report it via my website. When submitting an issue, be sure to submit any logs.";
"SETTINGS_VIEW_CONTROLLER_SECTION_FOOTER_SERVER_LIMITATIONS" = "Sadly due to limitations server certificates will need to be re-renewed every year to keep Feathers local features working properly, tap this button to retrieve the most up-to-date files from our repositories.";
"SETTINGS_VIEW_CONTROLLER_SECTION_FOOTER_DEFAULT_SERVER" = "Default server goes to %@";
+
"SETTINGS_VIEW_CONTROLLER_TITLE" = "Server Options";
"SETTINGS_VIEW_CONTROLLER_TITLE_ONLINE" = "Online";
"SETTINGS_VIEW_CONTROLLER_TITLE_LOCAL" = "Local";
+
+// Cell Titles
+// About Feather
"SETTINGS_VIEW_CONTROLLER_CELL_ABOUT" = "About %@";
"SETTINGS_VIEW_CONTROLLER_CELL_SUBMIT_FEEDBACK" = "Submit Feedback";
"SETTINGS_VIEW_CONTROLLER_CELL_GITHUB" = "GitHub Repository";
@@ -215,30 +301,45 @@
"SETTINGS_VIEW_CONTROLLER_CELL_LANGUAGE" = "Language";
"SETTINGS_VIEW_CONTROLLER_CELL_CURRENT_CERTIFICATE_NOSELECTED" = "No certificates selected";
"SETTINGS_VIEW_CONTROLLER_CELL_ADD_CERTIFICATES" = "Add Certificate";
+
+
+
"SETTINGS_VIEW_CONTROLLER_CELL_SIGN_OPTIONS" = "Signing Options";
"SETTINGS_VIEW_CONTROLLER_CELL_SERVER_OPTIONS" = "Server Options";
"SETTINGS_VIEW_CONTROLLER_CELL_VIEW_LOGS" = "View Logs";
"SETTINGS_VIEW_CONTROLLER_CELL_APPS_FOLDER" = "Open Apps Folder";
"SETTINGS_VIEW_CONTROLLER_CELL_CERTS_FOLDER" = "Open Certificates Folder";
+
+
"SETTINGS_VIEW_CONTROLLER_CELL_UPDATE_LOCAL_CERTIFICATE" = "Update Local Certificate";
"SETTINGS_VIEW_CONTROLLER_CELL_UPDATE_LOCAL_CERTIFICATE_UPDATING" = "Update Local Certificate";
"SETTINGS_VIEW_CONTROLLER_CELL_RESET" = "Reset";
"SETTINGS_VIEW_CONTROLLER_CELL_RESET_ALL" = "Reset All";
+
"SETTINGS_VIEW_CONTROLLER_CELL_RESET_CONFIGURATION" = "Reset Configuration";
"SETTINGS_VIEW_CONTROLLER_CELL_USE_CUSTOM_SERVER" = "Use Custom Server";
+
"SETTINGS_VIEW_CONTROLLER_CELL_ONLINE_INSTALL_METHOD" = "Online Install Method";
+
+
"SETTINGS_VIEW_CONTROLLER_CELL_EXPORT_ID" = "Export Random Identifier";
"SETTINGS_VIEW_CONTROLLER_CELL_CHANGE_ID" = "Change Random Identifier";
+
+//
"SETTINGS_VIEW_CONTROLLER_PPQ_ALERT_TITLE" = "PPQCheck Protections";
"SETTINGS_VIEW_CONTROLLER_PPQ_ALERT_DESCRIPTION" = "This setting enables the PPQCheck protections, which is designed to prepend each bundle identifier for the apps you sideload with a random string.\n\nThis is meant to avoid apple flagging your account by (trying) to make it so they're unable to associate the app you're sideloading with one from the App Store.";
+
"SETTINGS_VIEW_CONTROLLER_URL_ALERT_TITLE" = "Change Download URL";
"SETTINGS_VIEW_CONTROLLER_CELL_CHANGE_IDENTIFIER" = "Change Random Identifier";
// MARK: - SettingsViewController -> AboutViewController
"ABOUT_VIEW_CONTROLLER_SECTION_TITLE_CREDITS" = "Credits";
"ABOUT_VIEW_CONTROLLER_SECTION_TITLE_SPONSORS" = "Sponsors";
+
"ABOUT_VIEW_CONTROLLER_SECTION_TITLE_DEVICE" = "Device";
"ABOUT_VIEW_CONTROLLER_SECTION_TITLE_ACKNOWLEDGEMENTS" = "Acknowledgements";
+
+
"ABOUT_VIEW_CONTROLLER_CELL_DEVICE_VERSION" = "Device Version";
"ABOUT_VIEW_CONTROLLER_CELL_DEVICE_ARCH" = "Architecture";
"ABOUT_VIEW_CONTROLLER_CELL_APP_VERSION" = "App Version";
@@ -247,12 +348,16 @@
"DISPLAY_VIEW_CONTROLLER_SECTION_TITLE_TINT_COLOR" = "Tint Color";
"DISPLAY_VIEW_CONTROLLER_SECTION_TITLE_APP_APPEARENCE" = "App Appearence";
"DISPLAY_VIEW_CONTROLLER_SECTION_TITLE_STORE" = "Store";
+
"DISPLAY_VIEW_CONTROLLER_CELL_DEFAULT_SUBTITLE" = "Default Subtitle";
-"DISPLAY_VIEW_CONTROLLER_CELL_DEFAULT_SUBTITLE_DESCRIPTION" = "Default style for feather, hides localized description and only includes subtitle.";
+"DISPLAY_VIEW_CONTROLLER_CELL_DEFAULT_SUBTITLE_DESCRIPTION" = "Default style for backdoor, hides localized description and only includes subtitle.";
+
"DISPLAY_VIEW_CONTROLLER_CELL_LOCALIZED_SUBTITLE" = "Localized Subtitle";
"DISPLAY_VIEW_CONTROLLER_CELL_LOCALIZED_SUBTITLE_DESCRIPTION" = "Replaces subtitle with app description.";
+
"DISPLAY_VIEW_CONTROLLER_CELL_BIG_DESCRIPTION" = "Big Description";
"DISPLAY_VIEW_CONTROLLER_CELL_BIG_DESCRIPTION_DESCRIPTION" = "Replaces screenshots with the app description.";
+
"DISPLAY_VIEW_CONTROLLER_CELL_TEAM_NAME" = "Use Team Name";
"DISPLAY_VIEW_CONTROLLER_CELL_TEAM_NAME_DESCRIPTION" = "Replaces the certificate name with your team name, could be for better to distinguish between certificates if providers happen to always use the same App ID name.";
@@ -262,26 +367,37 @@
// MARK: - SettingsViewController -> CertificatesViewController
"CERTIFICATES_VIEW_CONTROLLER_TITLE" = "Certificates";
+
"CERTIFICATES_VIEW_CONTROLLER_CELL_ADD_FOOTER" = "Supported file formats:\n\n- P12 (.p12)\n- Mobile Provision (.mobileprovision)\n\nMake sure your certificates are valid and are able to sideload to your device!";
+
"CERTIFICATES_VIEW_CONTROLLER_CELL_ADD" = "Add Certificates";
"CERTIFICATES_VIEW_CONTROLLER_CELL_ADD_DESCRIPTION" = "Tap to add a certificate";
+
+// MARK: - SettingsViewController -> CertificatesViewController -> Delete Alert
"CERTIFICATES_VIEW_CONTROLLER_DELETE_ALERT_TITLE" = "You don't want to do this!";
"CERTIFICATES_VIEW_CONTROLLER_DELETE_ALERT_DESCRIPTION" = "You're trying to delete a selected certificate, try again later when you have another certificate on hand.";
+
+// MARK: - SettingsViewController -> CertificatesViewController -> Cell
"CERTIFICATES_VIEW_CONTROLLER_CELL_EXPIRED" = "Expired";
"CERTIFICATES_VIEW_CONTROLLER_CELL_DAYS_LEFT" = "%@ days left";
// MARK: - SettingsViewController -> CertificatesViewController -> CertImportingViewController
"CERT_IMPORTING_VIEWCONTROLLER_TITLE" = "Import";
"CERT_IMPORTING_VIEWCONTROLLER_SECTION_PROVISIONING" = "";
+
+// MARK: - SettingsViewController -> CertificatesViewController -> CertImportingViewController -> Password Alert
+
"CERT_IMPORTING_VIEWCONTROLLER_PW_ALERT_TITLE" = "Bad Password";
"CERT_IMPORTING_VIEWCONTROLLER_PW_ALERT_DESCRIPTION" = "Please check the password and try again.";
+
"CERT_IMPORTING_VIEWCONTROLLER_CELL_IMPORT_PROV" = "Import Provisioning File";
"CERT_IMPORTING_VIEWCONTROLLER_CELL_IMPORT_CERT" = "Import Certificate File";
"CERT_IMPORTING_VIEWCONTROLLER_CELL_IMPORT_ENTER_PW" = "Enter Password";
"CERT_IMPORTING_VIEWCONTROLLER_CELL_IMPORT_PW" = "Password";
+
"CERT_IMPORTING_VIEWCONTROLLER_FOOTER_PROV" = "Import a provisioning file to be able to sideload to your device.";
"CERT_IMPORTING_VIEWCONTROLLER_FOOTER_CERT" = "Import a file containing a valid certificate.";
-"CERT_IMPORTING_VIEWCONTROLLER_FOOTER_PASS" = "Enter the password associated with the private key, leave it blank if there's no password required.";
+"CERT_IMPORTING_VIEWCONTROLLER_FOOTER_PASS" = "Enter the password associated with the private key, leave it blank if theres no password required.";
// MARK: - SettingsViewController -> ResetViewController
"RESET_VIEW_CONTROLLER_CLEAR_CACHE" = "Clear Network Cache";
@@ -290,8 +406,10 @@
// MARK: - Donation
"DONATION_TITLE" = "Donate";
"DONATION_DONATIONS" = "Donations";
+
"DONATION_CELL_1_TITLE" = "Secret Repo";
"DONATION_CELL_1_DESCRIPTION" = "Get access to our secret repo by donating, will provide you with beta access to Feather.";
+
"DONATION_CELL_2_TITLE" = "Show Your Support";
"DONATION_CELL_2_DESCRIPTION" = "Show your support by donating! If you're unable to donate, spreading the word about Feather works too!";
@@ -300,4 +418,5 @@
"LOGS_VIEW_SECTION_TITLE_SHARE" = "Share Logs";
"LOGS_VIEW_SECTION_TITLE_COPY" = "Copy Logs";
"LOGS_VIEW_SUCCESS_DESCRIPTION" = "Log contents have been copied to clipboard.";
-"LOGS_VIEW_FAIL_DESCRIPTION" = "Failed to copy log contents.";
\ No newline at end of file
+"LOGS_VIEW_FAIL_DESCRIPTION" = "Failed to copy log contents.";
+"LOGS_VIEW_TITLE" = "Backdoor Logs";
diff --git a/Shared/Magic/AppSigner.swift b/Shared/Magic/AppSigner.swift
index 3b151411..c5895c79 100644
--- a/Shared/Magic/AppSigner.swift
+++ b/Shared/Magic/AppSigner.swift
@@ -322,4 +322,4 @@ func updateLocalizedInfoPlist(in appDirectory: URL, newDisplayName: String) {
} catch {
Debug.shared.log(message: "Unable to localize, skipping!", type: .debug)
}
-}
+}
\ No newline at end of file
diff --git a/Shared/Magic/TweakHandler.swift b/Shared/Magic/TweakHandler.swift
index a18b1ea7..48b6ab66 100644
--- a/Shared/Magic/TweakHandler.swift
+++ b/Shared/Magic/TweakHandler.swift
@@ -1,324 +1,294 @@
-//
-// DylibHandler.swift
-// feather
-//
-// Created by samara on 8/17/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import Foundation
import SWCompression
enum FileProcessingError: Error {
- case unsupportedFileExtension(String)
- case decompressionFailed(String)
- case missingFile(String)
+ case unsupportedFileExtension(String)
+ case decompressionFailed(String)
+ case missingFile(String)
}
class TweakHandler {
-
- let fileManager = FileManager.default
-
- private var urls: [String]
- private let app: URL
- private var urlsToInject: [URL] = []
- private var directoriesToCheck: [URL] = []
-
- init(urls: [String], app: URL) {
- self.urls = urls
- self.app = app
- }
-
- public func getInputFiles() throws {
- guard !urls.isEmpty else {
- Debug.shared.log(message: "No dylibs to inject, skipping!")
- return
- }
-
- let frameworksPath = app.appendingPathComponent("Frameworks").appendingPathComponent("CydiaSubstrate.framework")
- if !fileManager.fileExists(atPath: frameworksPath.path) {
- if let ellekitURL = Bundle.main.url(forResource: "ellekit", withExtension: "deb") {
- self.urls.insert(ellekitURL.absoluteString, at: 0)
- } else {
- Debug.shared.log(message: "Error: ellekit.deb not found in the app bundle ⁉️", type: .error)
- return
- }
- }
-
- let baseTmpDir = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)
-
- do {
- try TweakHandler.createDirectoryIfNeeded(at: app.appendingPathComponent("Frameworks"))
- try TweakHandler.createDirectoryIfNeeded(at: baseTmpDir)
-
- // check for appropriate files, if theres debs
- // it will extract then add a url, if theres no url, i.e.
- // you haven't added a deb, it will skip
- for url in urls {
- let urlf = URL(string: url)
- switch urlf!.pathExtension.lowercased() {
- case "dylib":
- try handleDylib(at: urlf!)
- case "deb":
- try handleDeb(at: urlf!, baseTmpDir: baseTmpDir)
- default:
- Debug.shared.log(message: "Unsupported file type: \(urlf!.lastPathComponent), skipping.")
- }
- }
-
- // check contents of data.tar's extracted from debs
- if !directoriesToCheck.isEmpty {
- try handleDirectories(at: directoriesToCheck)
- if !urlsToInject.isEmpty {
- try handleExtractedDirectoryContents(at: urlsToInject)
- }
- }
-
- } catch {
- throw error
- }
- }
-
- // finally, handle extracted contents
- private func handleExtractedDirectoryContents(at urls: [URL]) throws {
- for url in urls {
- switch url.pathExtension.lowercased() {
- case "dylib":
- try handleDylib(at: url)
- case "framework":
- let destinationURL = app.appendingPathComponent("Frameworks").appendingPathComponent(url.lastPathComponent)
- try TweakHandler.moveFile(from: url, to: destinationURL)
- try handleDylib(framework: destinationURL)
- case "bundle":
- let destinationURL = app.appendingPathComponent(url.lastPathComponent)
- try TweakHandler.moveFile(from: url, to: destinationURL)
- default:
- Debug.shared.log(message: "Unsupported file type: \(url.lastPathComponent), skipping.")
- }
- }
- }
-
- // Inject imported dylib file
- private func handleDylib(at url: URL) throws {
- do {
- let destinationURL = app.appendingPathComponent("Frameworks").appendingPathComponent(url.lastPathComponent)
- try TweakHandler.moveFile(from: url, to: destinationURL)
-
- // change paths because some tweaks hardlink, which is not ideal.
- // this is not a good solution, at most this would work for basic tweaks
- // we recommend you use newer theos to compile, and make sure it works
- // using the ellekit framework
- _ = changeDylib(
- filePath: destinationURL.path,
- oldPath: "/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate",
- newPath: "@rpath/CydiaSubstrate.framework/CydiaSubstrate"
- )
-
- // inject if there's a valid app main executable
- if let exe = try TweakHandler.findExecutable(at: app) {
- _ = injectDylib(
- filePath: exe.path,
- dylibPath: "@executable_path/Frameworks/\(destinationURL.lastPathComponent)",
- weakInject: true
- )
- }
- } catch {
- throw error
- }
- }
-
- // Inject imported framework dir
- private func handleDylib(framework: URL) throws {
- do {
- if let fexe = try TweakHandler.findExecutable(at: framework) {
-
- // change paths because some tweaks hardlink, which is not ideal.
- // this is not a good solution, at most this would work for basic tweaks
- // we recommend you use newer theos to compile, and make sure it works
- // using the ellekit framework
- _ = changeDylib(
- filePath: fexe.path,
- oldPath: "/Library/Frameworks/CydiaSubstrate.framework/CydiaSubstrate",
- newPath: "@rpath/CydiaSubstrate.framework/CydiaSubstrate"
- )
-
- // inject if there's a valid app main executable
- if let appexe = try TweakHandler.findExecutable(at: app) {
- _ = injectDylib(
- filePath: appexe.path,
- dylibPath: "@executable_path/Frameworks/\(framework.lastPathComponent)/\(fexe.lastPathComponent)",
- weakInject: true
- )
- }
- }
-
-
- } catch {
- throw error
- }
- }
-
- // Extracy imported deb file
- private func handleDeb(at url: URL, baseTmpDir: URL) throws {
- let uniqueSubDir = baseTmpDir.appendingPathComponent(UUID().uuidString)
- try TweakHandler.createDirectoryIfNeeded(at: uniqueSubDir)
-
- // I don't particularly like this code
- // but it somehow works well enough,
- // do note large lzma's are slow as hell
- do {
- let arFiles = try extractAR(try Data(contentsOf: url))
-
- for arFile in arFiles {
- let outputPath = uniqueSubDir.appendingPathComponent(arFile.name)
- try arFile.content.write(to: outputPath)
-
- if ["data.tar.lzma", "data.tar.gz", "data.tar.xz", "data.tar.bz2"].contains(arFile.name) {
- var fileToProcess = outputPath
- try processFile(at: &fileToProcess)
- try processFile(at: &fileToProcess)
- directoriesToCheck.append(fileToProcess)
- }
- }
- } catch {
- Debug.shared.log(message: "Error handling file \(url): \(error)")
- throw error
- }
- }
-
- // Read extracted deb file, locate all neccessary contents to copy over to the .app
- private func handleDirectories(at urls: [URL]) throws {
- let directoriesToCheck = [
- "Library/Frameworks/", "var/jb/Library/Frameworks/",
- "Library/MobileSubstrate/DynamicLibraries/", "var/jb/Library/MobileSubstrate/DynamicLibraries/",
- "Library/Application Support/", "var/jb/Library/Application Support/"
- ]
-
- let fileManager = FileManager.default
-
- for baseURL in urls {
- for directory in directoriesToCheck {
- let directoryURL = baseURL.appendingPathComponent(directory)
-
- guard fileManager.fileExists(atPath: directoryURL.path) else {
- Debug.shared.log(message: "Directory does not exist: \(directoryURL.path). Skipping.")
- continue
- }
-
- switch directory {
- case "Library/MobileSubstrate/DynamicLibraries/", "var/jb/Library/MobileSubstrate/DynamicLibraries/":
- let dylibFiles = try locateDylibFiles(in: directoryURL)
- for fileURL in dylibFiles {
- urlsToInject.append(fileURL)
- }
-
- case "Library/Frameworks/", "var/jb/Library/Frameworks/":
- let frameworkDirectories = try locateFrameworkDirectories(in: directoryURL)
- for frameworkURL in frameworkDirectories {
- urlsToInject.append(frameworkURL)
- }
-
- case "Library/Application Support/", "var/jb/Library/Application Support/":
- try searchForBundles(in: directoryURL)
-
- default:
- Debug.shared.log(message: "Unexpected directory path: \(directoryURL.path)")
- }
- }
- }
- }
+
+ let fileManager = FileManager.default
+
+ private var urls: [String]
+ private let app: URL
+ private var urlsToInject: [URL] = []
+ private var directoriesToCheck: [URL] = []
+
+ init(urls: [String], app: URL) {
+ self.urls = urls
+ self.app = app
+ }
+
+ public func getInputFiles() throws {
+ guard !urls.isEmpty else {
+ Debug.shared.log(message: "No dylibs to inject, skipping!")
+ return
+ }
+
+ let frameworksPath = app.appendingPathComponent("Frameworks").appendingPathComponent("CydiaSubstrate.framework")
+ if (!fileManager.fileExists(atPath: frameworksPath.path)) {
+ if let ellekitURL = Bundle.main.url(forResource: "ellekit", withExtension: "deb") {
+ self.urls.insert(ellekitURL.absoluteString, at: 0)
+ } else {
+ Debug.shared.log(message: "Error: ellekit.deb not found in the app bundle \u{2049}\u{fe0f}", type: .error)
+ return
+ }
+ }
+
+ let baseTmpDir = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString)
+
+ do {
+ try TweakHandler.createDirectoryIfNeeded(at: app.appendingPathComponent("Frameworks"))
+ try TweakHandler.createDirectoryIfNeeded(at: baseTmpDir)
+
+ // check for appropriate files, if theres debs
+ // it will extract then add a url, if theres no url, i.e.
+ // you haven't added a deb, it will skip
+ for url in urls {
+ let urlf = URL(string: url)
+ switch urlf!.pathExtension.lowercased() {
+ case "dylib":
+ try handleDylib(at: urlf!)
+ case "deb":
+ try handleDeb(at: urlf!, baseTmpDir: baseTmpDir)
+ default:
+ Debug.shared.log(message: "Unsupported file type: \(urlf!.lastPathComponent), skipping.")
+ }
+ }
+
+ // check contents of data.tar's extracted from debs
+ if !directoriesToCheck.isEmpty {
+ try handleDirectories(at: directoriesToCheck)
+ if !urlsToInject.isEmpty {
+ try handleExtractedDirectoryContents(at: urlsToInject)
+ }
+ }
+
+ } catch {
+ throw error
+ }
+ }
+
+ // finally, handle extracted contents
+ private func handleExtractedDirectoryContents(at urls: [URL]) throws {
+ for url in urls {
+ switch url.pathExtension.lowercased() {
+ case "dylib":
+ try handleDylib(at: url)
+ case "framework":
+ let destinationURL = app.appendingPathComponent("Frameworks").appendingPathComponent(url.lastPathComponent)
+ try TweakHandler.moveFile(from: url, to: destinationURL)
+ try handleDylib(framework: destinationURL)
+ case "bundle":
+ let destinationURL = app.appendingPathComponent(url.lastPathComponent)
+ try TweakHandler.moveFile(from: url, to: destinationURL)
+ default:
+ Debug.shared.log(message: "Unsupported file type: \(url.lastPathComponent), skipping.")
+ }
+ }
+ }
+
+ // Inject imported dylib file
+ private func handleDylib(at url: URL) throws {
+ do {
+ let destinationURL = app.appendingPathComponent("Frameworks").appendingPathComponent(url.lastPathComponent)
+ try TweakHandler.moveFile(from: url, to: destinationURL)
+
+ // Perform path adjustments if needed
+ // This is a placeholder for the removed Process command
+
+ // inject if there's a valid app main executable
+ if let exe = try TweakHandler.findExecutable(at: app) {
+ _ = injectDylib(
+ filePath: exe.path,
+ dylibPath: "@executable_path/Frameworks/\(url.lastPathComponent)",
+ weakInject: true
+ )
+ }
+ } catch {
+ throw error
+ }
+ }
+
+ // Inject imported framework dir
+ private func handleDylib(framework: URL) throws {
+ do {
+ if let fexe = try TweakHandler.findExecutable(at: framework) {
+
+ // Perform path adjustments if needed
+ // This is a placeholder for the removed Process command
+
+ // inject if there's a valid app main executable
+ if let appexe = try TweakHandler.findExecutable(at: app) {
+ _ = injectDylib(
+ filePath: appexe.path,
+ dylibPath: "@executable_path/Frameworks/\(framework.lastPathComponent)/\(fexe.lastPathComponent)",
+ weakInject: true
+ )
+ }
+ }
+ } catch {
+ throw error
+ }
+ }
+
+ // Extract imported deb file
+ private func handleDeb(at url: URL, baseTmpDir: URL) throws {
+ let uniqueSubDir = baseTmpDir.appendingPathComponent(UUID().uuidString)
+ try TweakHandler.createDirectoryIfNeeded(at: uniqueSubDir)
+
+ // I don't particularly like this code
+ // but it somehow works well enough,
+ // do note large lzma's are slow as hell
+ do {
+ let arFiles = try extractAR(try Data(contentsOf: url))
+
+ for arFile in arFiles {
+ let outputPath = uniqueSubDir.appendingPathComponent(arFile.name)
+ try arFile.content.write(to: outputPath)
+
+ if ["data.tar.lzma", "data.tar.gz", "data.tar.xz", "data.tar.bz2"].contains(arFile.name) {
+ var fileToProcess = outputPath
+ try processFile(at: &fileToProcess)
+ try processFile(at: &fileToProcess)
+ directoriesToCheck.append(fileToProcess)
+ }
+ }
+ } catch {
+ Debug.shared.log(message: "Error handling file \(url): \(error)")
+ throw error
+ }
+ }
+
+ // Read extracted deb file, locate all necessary contents to copy over to the .app
+ private func handleDirectories(at urls: [URL]) throws {
+ let directoriesToCheck = [
+ "Library/Frameworks/", "var/jb/Library/Frameworks/",
+ "Library/MobileSubstrate/DynamicLibraries/", "var/jb/Library/MobileSubstrate/DynamicLibraries/",
+ "Library/Application Support/", "var/jb/Library/Application Support/"
+ ]
+
+ let fileManager = FileManager.default
+
+ for baseURL in urls {
+ for directory in directoriesToCheck {
+ let directoryURL = baseURL.appendingPathComponent(directory)
+
+ guard fileManager.fileExists(atPath: directoryURL.path) else {
+ Debug.shared.log(message: "Directory does not exist: \(directoryURL.path). Skipping.")
+ continue
+ }
+
+ switch directory {
+ case "Library/MobileSubstrate/DynamicLibraries/", "var/jb/Library/MobileSubstrate/DynamicLibraries/":
+ let dylibFiles = try locateDylibFiles(in: directoryURL)
+ for fileURL in dylibFiles {
+ urlsToInject.append(fileURL)
+ }
+
+ case "Library/Frameworks/", "var/jb/Library/Frameworks/":
+ let frameworkDirectories = try locateFrameworkDirectories(in: directoryURL)
+ for frameworkURL in frameworkDirectories {
+ urlsToInject.append(frameworkURL)
+ }
+
+ case "Library/Application Support/", "var/jb/Library/Application Support/":
+ try searchForBundles(in: directoryURL)
+
+ default:
+ Debug.shared.log(message: "Unexpected directory path: \(directoryURL.path)")
+ }
+ }
+ }
+ }
}
-
-
-
// MARK: - Find correct files in debs
extension TweakHandler {
- private func searchForBundles(in directory: URL) throws {
- let fileManager = FileManager.default
- let allFiles = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
+ private func searchForBundles(in directory: URL) throws {
+ let fileManager = FileManager.default
+ let allFiles = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
- let bundleDirectories = allFiles.filter { url in
- let attributes = try? fileManager.attributesOfItem(atPath: url.path)
- let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
- return url.pathExtension.lowercased() == "bundle" && url.hasDirectoryPath && !isSymlink
- }
-
- for bundleURL in bundleDirectories {
- urlsToInject.append(bundleURL)
- }
-
- let directoriesToSearch = allFiles.filter { url in
- let attributes = try? fileManager.attributesOfItem(atPath: url.path)
- let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
- return url.hasDirectoryPath && !bundleDirectories.contains(url) && !isSymlink
- }
-
- for dirURL in directoriesToSearch {
- try searchForBundles(in: dirURL)
- }
- }
+ let bundleDirectories = allFiles.filter { url in
+ let attributes = try? fileManager.attributesOfItem(atPath: url.path)
+ let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
+ return url.pathExtension.lowercased() == "bundle" && url.hasDirectoryPath && !isSymlink
+ }
+
+ for bundleURL in bundleDirectories {
+ urlsToInject.append(bundleURL)
+ }
+
+ let directoriesToSearch = allFiles.filter { url in
+ let attributes = try? fileManager.attributesOfItem(atPath: url.path)
+ let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
+ return url.hasDirectoryPath && !bundleDirectories.contains(url) && !isSymlink
+ }
+
+ for dirURL in directoriesToSearch {
+ try searchForBundles(in: dirURL)
+ }
+ }
- private func locateDylibFiles(in directory: URL) throws -> [URL] {
- let fileManager = FileManager.default
- let files = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])
+ private func locateDylibFiles(in directory: URL) throws -> [URL] {
+ let fileManager = FileManager.default
+ let files = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [])
- let dylibFiles = files.filter { url in
- let attributes = try? fileManager.attributesOfItem(atPath: url.path)
- let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
- return url.pathExtension.lowercased() == "dylib" && !isSymlink
- }
-
- return dylibFiles
- }
+ let dylibFiles = files.filter { url in
+ let attributes = try? fileManager.attributesOfItem(atPath: url.path)
+ let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
+ return url.pathExtension.lowercased() == "dylib" && !isSymlink
+ }
+
+ return dylibFiles
+ }
- private func locateFrameworkDirectories(in directory: URL) throws -> [URL] {
- let fileManager = FileManager.default
- let files = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
+ private func locateFrameworkDirectories(in directory: URL) throws -> [URL] {
+ let fileManager = FileManager.default
+ let files = try fileManager.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
- let frameworkDirectories = files.filter { url in
- let attributes = try? fileManager.attributesOfItem(atPath: url.path)
- let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
- return url.pathExtension.lowercased() == "framework" && url.hasDirectoryPath && !isSymlink
- }
-
- return frameworkDirectories
- }
+ let frameworkDirectories = files.filter { url in
+ let attributes = try? fileManager.attributesOfItem(atPath: url.path)
+ let isSymlink = attributes?[.type] as? FileAttributeType == .typeSymbolicLink
+ return url.pathExtension.lowercased() == "framework" && url.hasDirectoryPath && !isSymlink
+ }
+
+ return frameworkDirectories
+ }
}
-
-
// MARK: - File management
extension TweakHandler {
- private static func createDirectoryIfNeeded(at url: URL) throws {
- let fileManager = FileManager.default
- if !fileManager.fileExists(atPath: url.path) {
- try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
- }
- }
-
- public static func findExecutable(at frameworkURL: URL) throws -> URL? {
-
- let infoPlistURL = frameworkURL.appendingPathComponent("Info.plist")
-
- let plistData = try Data(contentsOf: infoPlistURL)
- if let plist = try PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any],
- let executableName = plist["CFBundleExecutable"] as? String {
- let executableURL = frameworkURL.appendingPathComponent(executableName)
- return executableURL
- } else {
- Debug.shared.log(message: "CFBundleExecutable not found in Info.plist")
- return nil
- }
- }
-
- private static func moveFile(from sourceURL: URL, to destinationURL: URL) throws {
- let fileManager = FileManager.default
- if fileManager.fileExists(atPath: destinationURL.path) {
- Debug.shared.log(message: "File already exists at destination: \(destinationURL)")
- } else {
- try fileManager.moveItem(at: sourceURL, to: destinationURL)
- }
- }
-}
+ private static func createDirectoryIfNeeded(at url: URL) throws {
+ let fileManager = FileManager.default
+ if !fileManager.fileExists(atPath: url.path) {
+ try fileManager.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
+ }
+ }
+
+ public static func findExecutable(at frameworkURL: URL) throws -> URL? {
+
+ let infoPlistURL = frameworkURL.appendingPathComponent("Info.plist")
+
+ let plistData = try Data(contentsOf: infoPlistURL)
+ if let plist = try PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any],
+ let executableName = plist["CFBundleExecutable"] as? String {
+ let executableURL = frameworkURL.appendingPathComponent(executableName)
+ return executableURL
+ } else {
+ Debug.shared.log(message: "CFBundleExecutable not found in Info.plist")
+ return nil
+ }
+ }
+ private static func moveFile(from sourceURL: URL, to destinationURL: URL) throws {
+ let fileManager = FileManager.default
+ if fileManager.fileExists(atPath: destinationURL.path) {
+ Debug.shared.log(message: "File already exists at destination: \(destinationURL)")
+ } else {
+ try fileManager.moveItem(at: sourceURL, to: destinationURL)
+ }
+ }
+}
\ No newline at end of file
diff --git a/Shared/Magic/decompression/AR.swift b/Shared/Magic/decompression/AR.swift
index 631387b4..8cb3924b 100644
--- a/Shared/Magic/decompression/AR.swift
+++ b/Shared/Magic/decompression/AR.swift
@@ -70,4 +70,4 @@ public func extractAR(_ rawData: Data) throws -> [ARFile] {
offset += offset % 2
}
return files
-}
+}
\ No newline at end of file
diff --git a/Shared/Magic/decompression/Decompression.swift b/Shared/Magic/decompression/Decompression.swift
index 2786b1fc..d7c7f8a8 100644
--- a/Shared/Magic/decompression/Decompression.swift
+++ b/Shared/Magic/decompression/Decompression.swift
@@ -70,4 +70,4 @@ func processFile(at packagesFile: inout URL) throws {
default:
throw FileProcessingError.unsupportedFileExtension(succeededExtension)
}
-}
+}
\ No newline at end of file
diff --git a/Shared/Magic/feather-Bridging-Header.h b/Shared/Magic/feather-Bridging-Header.h
index 09f8d680..cd7fcaa7 100644
--- a/Shared/Magic/feather-Bridging-Header.h
+++ b/Shared/Magic/feather-Bridging-Header.h
@@ -1,9 +1,11 @@
//
-// Use this file to import your target's public headers that you would like to expose to Swift.
-//
+ // Use this file to import your target's public headers that you would like to expose to Swift.
+ //
+
+ #include "UISheetPresentationControllerDetent+Private.h"
+ #include "LSApplicationWorkspace.h"
-#include "UISheetPresentationControllerDetent+Private.h"
-#include "LSApplicationWorkspace.h"
+ #include "zsign.hpp"
+ #include "openssl_tools.hpp"
-#include "zsign.hpp"
-#include "openssl_tools.hpp"
+ #import "CPUXLib.h" // Added this line
\ No newline at end of file
diff --git a/Shared/Server/DownloadCertificate.swift b/Shared/Server/DownloadCertificate.swift
index df70376a..f1da3d4a 100644
--- a/Shared/Server/DownloadCertificate.swift
+++ b/Shared/Server/DownloadCertificate.swift
@@ -35,10 +35,4 @@ func getCertificates() {
Debug.shared.log(message: "Error fetching data from \(uri): \(error.localizedDescription)", type: .error)
}
}
-}
-
-func getDocumentsDirectory() -> URL {
- let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
- let documentsDirectory = paths[0]
- return documentsDirectory
}
\ No newline at end of file
diff --git a/app-repo.json b/app-repo.json
deleted file mode 100644
index 0c07b616..00000000
--- a/app-repo.json
+++ /dev/null
@@ -1,141 +0,0 @@
-{
- "name": "Feather Repository",
- "identifier": "kh.crysalis.feather-repo",
- "iconURL": "https://github.com/khcrysalis/Feather/blob/main/iOS/Icons/Main/Mac%403x.png?raw=true",
- "apps": [
- {
- "name": "Feather",
- "bundleIdentifier": "kh.crysalis.feather",
- "developerName": "Samara",
- "iconURL": "https://github.com/khcrysalis/Feather/blob/main/iOS/Icons/Main/Mac%403x.png?raw=true",
- "localizedDescription": "Feather is a free on-device iOS application manager/installer built with UIKit for quality.",
- "subtitle": "On-device signing application",
- "tintColor": "848ef9",
- "versions": [
- {
- "version": "1.4.0",
- "date": "2025-03-08T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.4.0/feather_v1.4.0.ipa"
- },
- {
- "version": "1.3.1",
- "date": "2025-02-10T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.3.1/feather_v1.3.1.ipa"
- },
- {
- "version": "1.3.0",
- "date": "2025-02-4T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.3.0/feather_v1.3.0.ipa"
- },
- {
- "version": "1.2.1",
- "date": "2025-01-30T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.2.1/feather_v1.2.1.ipa"
- },
- {
- "version": "1.2.0",
- "date": "2025-01-23T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.2.0/feather_v1.2.0.ipa"
- },
- {
- "version": "1.1.3",
- "date": "2024-11-1T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.1.3/feather_v1.1.3.ipa"
- },
- {
- "version": "1.1.2",
- "date": "2024-10-31T18:35:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.1.2/feather_v1.1.2.ipa"
- },
- {
- "version": "1.1.1",
- "date": "2024-10-31T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.1.1/feather_v1.1.1.ipa"
- },
- {
- "version": "1.1.0",
- "date": "2024-10-31T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.1.0/feather_v1.1.0.ipa"
- },
- {
- "version": "1.0.5",
- "date": "2024-10-03T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.0.5/feather_v1.0.5.ipa"
- },
- {
- "version": "1.0.4",
- "date": "2024-10-03T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.0.4/feather_v1.0.4.ipa"
- },
- {
- "version": "1.0.3",
- "date": "2024-10-03T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.0.3/feather_v1.0.3.ipa"
- },
- {
- "version": "1.0.2",
- "date": "2024-09-18T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.0.2/feather_v1.0.2.ipa"
- },
- {
- "version": "1.0.1",
- "date": "2024-08-30T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.0.1/feather_v1.0.1.ipa"
- },
- {
- "version": "1.0",
- "date": "2024-08-24T18:34:10Z",
- "size": 12375230,
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.0/feather_v1.0.ipa"
- }
- ],
- "size": 12375230,
- "version": "1.4.0",
- "versionDate": "2025-02-10T18:35:10Z",
- "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v1.4.0/feather_v1.4.0.ipa",
- "appPermissions": {},
- "screenshotURLs": [
- "https://github.com/khcrysalis/Feather/blob/main/Images/Library.png?raw=true",
- "https://github.com/khcrysalis/Feather/blob/main/Images/Repos.png?raw=true",
- "https://github.com/khcrysalis/Feather/blob/main/Images/Sign.png?raw=true",
- "https://github.com/khcrysalis/Feather/blob/main/Images/Store.png?raw=true"
- ]
- }
- ],
- "news": [
- {
- "title": "Donate",
- "identifier": "feather-donate",
- "caption": "Feather is a free project and is not made possible without my sponsors! Feel free to donate, it helps keep me motivated in making new updates for this app, and thank you everyone.",
- "tintColor": "848ef9",
- "imageURL": "https://github.com/khcrysalis/Feather/blob/main/Images/donate.png?raw=true",
- "date": "2025-02-04",
- "url": "https://github.com/sponsors/khcrysalis",
- "notify": false
- },
- {
- "title": "About Feather",
- "identifier": "feather-about",
- "caption": "Feather allows you to use an Apple Developer Account to sign and install applications on device without needing a computer on stock iOS versions, while allowing easy management with its applications.\n\nDue to limitations, it's hard to tell if the application is actually installed, so you will need to keep track of whats on your device. This is an entirely stock application and uses built-in features to be able to do this!",
- "tintColor": "8A28F7",
- "imageURL": "https://media.idownloadblog.com/wp-content/uploads/2024/08/Feather-on-device-signing-UI-app.jpg",
- "url": "https://github.com/khcrysalis/feather",
- "date": "2025-02-03",
- "notify": true
- }
- ]
-}
diff --git a/certificates/BDG.mobileprovision b/certificates/BDG.mobileprovision
new file mode 100644
index 00000000..f7aad30e
Binary files /dev/null and b/certificates/BDG.mobileprovision differ
diff --git a/certificates/BDG.p12 b/certificates/BDG.p12
new file mode 100644
index 00000000..0f061cf8
Binary files /dev/null and b/certificates/BDG.p12 differ
diff --git a/feather.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/feather.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index e6cb2c16..c52c4ca0 100644
--- a/feather.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/feather.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -255,4 +255,4 @@
}
],
"version" : 3
-}
+}
\ No newline at end of file
diff --git a/feather.xcodeproj/xcshareddata/xcschemes/feather (Release).xcscheme b/feather.xcodeproj/xcshareddata/xcschemes/feather (Release).xcscheme
index 8ff8c7ba..bd6f9f42 100644
--- a/feather.xcodeproj/xcshareddata/xcschemes/feather (Release).xcscheme
+++ b/feather.xcodeproj/xcshareddata/xcschemes/feather (Release).xcscheme
@@ -17,7 +17,7 @@
BlueprintIdentifier = "33BA378A2BF8159F00FF530A"
BuildableName = "backdoor.app"
BlueprintName = "backdoor"
- ReferencedContainer = "container:backdoor.xcodeproj">
+ ReferencedContainer = "container:feather.xcodeproj">
@@ -46,7 +46,7 @@
BlueprintIdentifier = "33BA378A2BF8159F00FF530A"
BuildableName = "backdoor.app"
BlueprintName = "backdoor"
- ReferencedContainer = "container:backdoor.xcodeproj">
+ ReferencedContainer = "container:feather.xcodeproj">
@@ -63,7 +63,7 @@
BlueprintIdentifier = "33BA378A2BF8159F00FF530A"
BuildableName = "backdoor.app"
BlueprintName = "backdoor"
- ReferencedContainer = "container:backdoor.xcodeproj">
+ ReferencedContainer = "container:feather.xcodeproj">
diff --git a/feather.xcworkspace/xcshareddata/swiftpm/Package.resolved b/feather.xcworkspace/xcshareddata/swiftpm/Package.resolved
index ec37cda8..cc4548da 100644
--- a/feather.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/feather.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -291,4 +291,4 @@
}
],
"version" : 3
-}
+}
\ No newline at end of file
diff --git a/iOS/Delegates/AppDelegate.swift b/iOS/Delegates/AppDelegate.swift
index 9f5372d3..4c3e383d 100644
--- a/iOS/Delegates/AppDelegate.swift
+++ b/iOS/Delegates/AppDelegate.swift
@@ -1,11 +1,3 @@
-//
-// AppDelegate.swift
-// feather
-//
-// Created by samara on 5/17/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import BackgroundTasks
import CoreData
import Foundation
@@ -15,12 +7,19 @@ import UIKit
import UIOnboarding
var downloadTaskManager = DownloadTaskManager.shared
+
+// Adding the function to the global scope
+/// Returns the URL for the app's Documents directory.
+public func getDocumentsDirectory() -> URL {
+ let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
+ return paths[0]
+}
+
class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControllerDelegate {
static let isSideloaded = Bundle.main.bundleIdentifier != "com.bdg.backdoor"
var window: UIWindow?
- var loaderAlert = presentLoader()
- func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let userDefaults = UserDefaults.standard
userDefaults.set(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, forKey: "currentVersion")
@@ -29,9 +28,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
userDefaults.signingOptions = UserDefaults.defaultSigningData
}
- createSourcesDirectory()
+ createSourcesDirectory()
addDefaultRepos()
- giveUserDefaultSSLCerts()
+ giveUserDefaultSSLCerts()
imagePipline()
setupLogFile()
cleanTmp()
@@ -64,23 +63,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
Debug.shared.log(message: "Model: \(UIDevice.current.model)")
Debug.shared.log(message: "Backdoor Version: \(logAppVersionInfo())\n")
- if Preferences.appUpdates {
- // Register background task
- BGTaskScheduler.shared.register(forTaskWithIdentifier: "kh.crysalis.feather.sourcerefresh", using: nil) { task in
- self.handleAppRefresh(task: task as! BGAppRefreshTask)
- }
- scheduleAppRefresh()
-
- let backgroundQueue = OperationQueue()
- backgroundQueue.qualityOfService = .background
- let operation = SourceRefreshOperation()
- backgroundQueue.addOperation(operation)
- }
+ if Preferences.appUpdates {
+ // Register background task
+ BGTaskScheduler.shared.register(forTaskWithIdentifier: "kh.crysalis.feather.sourcerefresh", using: nil) { task in
+ self.handleAppRefresh(task: task as! BGAppRefreshTask)
+ }
+ scheduleAppRefresh()
+
+ let backgroundQueue = OperationQueue()
+ backgroundQueue.qualityOfService = .background
+ let operation = SourceRefreshOperation()
+ backgroundQueue.addOperation(operation)
+ }
return true
}
- func applicationWillEnterForeground(_: UIApplication) {
+ func applicationWillEnterForeground(_ application: UIApplication) {
let backgroundQueue = OperationQueue()
backgroundQueue.qualityOfService = .background
let operation = SourceRefreshOperation()
@@ -117,7 +116,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
backgroundQueue.addOperation(operation)
}
- func application(_: UIApplication, open url: URL, options _: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+ func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if url.scheme == "feather" {
// I know this is super hacky, honestly
// I don't *exactly* care as it just works :shrug:
@@ -146,7 +145,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
}
DispatchQueue.main.async {
- rootViewController.present(self.loaderAlert, animated: true)
+ rootViewController.present(self.presentLoader(), animated: true)
}
DispatchQueue.global(qos: .background).async {
@@ -163,7 +162,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
try handleIPAFile(destinationURL: destinationURL, uuid: uuid, dl: dl)
DispatchQueue.main.async {
- self.loaderAlert.dismiss(animated: true) {
+ self.presentLoader().dismiss(animated: true) {
let downloadedApps = CoreDataManager.shared.getDatedDownloadedApps()
if let downloadedApp = downloadedApps.first(where: { $0.uuid == uuid }) {
let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
@@ -190,7 +189,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
let navigationController = UINavigationController(rootViewController: ap)
- navigationController.shouldPresentFullScreen()
+ navigationController.shouldPresentFullScreen()
rootViewController.present(navigationController, animated: true)
}
@@ -199,7 +198,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
}
} catch {
DispatchQueue.main.async {
- self.loaderAlert.dismiss(animated: true)
+ self.presentLoader().dismiss(animated: true)
Debug.shared.log(message: "Failed to handle IPA file: \(error)", type: .error)
}
}
@@ -219,7 +218,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
}
DispatchQueue.main.async {
- rootViewController.present(self.loaderAlert, animated: true)
+ rootViewController.present(self.presentLoader(), animated: true)
}
DispatchQueue.global(qos: .background).async {
@@ -234,12 +233,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
try handleIPAFile(destinationURL: destinationURL, uuid: uuid, dl: dl)
DispatchQueue.main.async {
- self.loaderAlert.dismiss(animated: true)
+ self.presentLoader().dismiss(animated: true)
Debug.shared.log(message: "Moved IPA file to: \(destinationURL)")
}
} catch {
DispatchQueue.main.async {
- self.loaderAlert.dismiss(animated: true)
+ self.presentLoader().dismiss(animated: true)
Debug.shared.log(message: "Failed to move IPA file: \(error)")
}
}
@@ -266,25 +265,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
}
fileprivate func addDefaultRepos() {
- if !Preferences.defaultRepos {
+ if (!Preferences.defaultRepos) {
CoreDataManager.shared.saveSource(
name: "Backdoor Repository",
id: "com.bdg.backdoor-repo",
iconURL: URL(string: "https://raw.githubusercontent.com/814bdg/App/refs/heads/main/Wing3x.png?raw=true"),
- url: "https://raw.githubusercontent.com/814bdg/App/c56e7beebe634db3065b8cf763c6e4a049ca73c1/App-repo.json"
+ url: "https://raw.githubusercontent.com/BDGHubNoKey/Backdoor/refs/heads/main/App-repo.json"
) { _ in
Debug.shared.log(message: "Added default repos!")
- Preferences.defaultRepos = true
+ Preferences.defaultRepos = false
}
}
}
-
- fileprivate func giveUserDefaultSSLCerts() {
- if !Preferences.gotSSLCerts {
- getCertificates()
- Preferences.gotSSLCerts = true
- }
- }
+
+ fileprivate func giveUserDefaultSSLCerts() {
+ if (!Preferences.gotSSLCerts) {
+ getCertificates()
+ Preferences.gotSSLCerts = true
+ }
+ }
fileprivate static func generateRandomString(length: Int = 8) -> String {
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@@ -300,7 +299,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
if !fileManager.fileExists(atPath: sourcesURL.path) {
do { try! fileManager.createDirectory(at: sourcesURL, withIntermediateDirectories: true, attributes: nil) }
}
- if !fileManager.fileExists(atPath: certsURL.path) {
+ if (!fileManager.fileExists(atPath: certsURL.path)) {
do { try! fileManager.createDirectory(at: certsURL, withIntermediateDirectories: true, attributes: nil) }
}
}
@@ -363,12 +362,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UIOnboardingViewControlle
}
return ""
}
+
+ func presentLoader() -> UIAlertController {
+ let alert = UIAlertController(title: "Loading...", message: nil, preferredStyle: .alert)
+
+ let loadingIndicator = UIActivityIndicatorView(frame: CGRect(x: 10, y: 5, width: 50, height: 50))
+ loadingIndicator.style = .large
+ loadingIndicator.startAnimating()
+
+ alert.view.addSubview(loadingIndicator)
+ return alert
+ }
+}
+
+// Example usage in a ViewController
+class ExampleViewController: UIViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Use ProcessUtility to execute a shell command
+ ProcessUtility.shared.executeShellCommand("echo Hello, World!") { output in
+ print(output ?? "No output")
+ }
+ }
}
extension UIOnboardingViewConfiguration {
static func setUp() -> Self {
let welcomeToLine = NSMutableAttributedString(string: String.localized("ONBOARDING_WELCOMETITLE_1"))
- let featherLine = NSMutableAttributedString(string: "Feather", attributes: [
+ let featherLine = NSMutableAttributedString(string: "Backdoor", attributes: [
.foregroundColor: UIColor.tintColor,
])
@@ -418,4 +441,4 @@ extension UIOnboardingViewConfiguration {
buttonConfiguration: .init(title: String.localized("ONBOARDING_CONTINUE_BUTTON"), backgroundColor: .tintColor)
)
}
-}
+}
\ No newline at end of file
diff --git a/iOS/Info.plist b/iOS/Info.plist
index cd078865..642c13bd 100644
--- a/iOS/Info.plist
+++ b/iOS/Info.plist
@@ -121,5 +121,7 @@
+ CFBundleShortVersionString
+ 0.1.0
-
+
\ No newline at end of file
diff --git a/iOS/Views/Apps/AppsTableViewCell.swift b/iOS/Views/Apps/AppsTableViewCell.swift
index af590368..c4112e9f 100644
--- a/iOS/Views/Apps/AppsTableViewCell.swift
+++ b/iOS/Views/Apps/AppsTableViewCell.swift
@@ -1,200 +1,189 @@
-//
-// AppsTableViewCell.swift
-// feather
-//
-// Created by samara on 7/1/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import Foundation
import UIKit
import CoreData
class AppsTableViewCell: UITableViewCell {
- let nameLabel: UILabel = {
- let label = UILabel()
- label.font = UIFont.boldSystemFont(ofSize: 17)
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- let versionLabel: UILabel = {
- let label = UILabel()
- label.font = UIFont.systemFont(ofSize: 13)
- label.textColor = .secondaryLabel
- label.numberOfLines = 1
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- let detailLabel: UILabel = {
- let label = UILabel()
- label.font = UIFont.systemFont(ofSize: 11)
- label.textColor = .secondaryLabel
- label.numberOfLines = 1
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- private let pillsStackView: UIStackView = {
- let stackView = UIStackView()
- stackView.axis = .horizontal
- stackView.spacing = 10
- stackView.distribution = .fillEqually
- stackView.alignment = .leading
- stackView.translatesAutoresizingMaskIntoConstraints = false
- return stackView
- }()
-
- override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
- super.init(style: style, reuseIdentifier: reuseIdentifier)
- setupViews()
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- private func setupViews() {
- contentView.addSubview(nameLabel)
- contentView.addSubview(versionLabel)
- contentView.addSubview(pillsStackView)
- imageView?.translatesAutoresizingMaskIntoConstraints = true
+ let nameLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFont.boldSystemFont(ofSize: 17)
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ let versionLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFont.systemFont(ofSize: 13)
+ label.textColor = .secondaryLabel
+ label.numberOfLines = 1
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ let detailLabel: UILabel = {
+ let label = UILabel()
+ label.font = UIFont.systemFont(ofSize: 11)
+ label.textColor = .secondaryLabel
+ label.numberOfLines = 1
+ label.translatesAutoresizingMaskIntoConstraints = false
+ return label
+ }()
+
+ private let pillsStackView: UIStackView = {
+ let stackView = UIStackView()
+ stackView.axis = .horizontal
+ stackView.spacing = 10
+ stackView.distribution = .fillEqually
+ stackView.alignment = .leading
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+ return stackView
+ }()
- NSLayoutConstraint.activate([
-
-
- nameLabel.leadingAnchor.constraint(equalTo: imageView!.trailingAnchor, constant: 15),
- nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
-
- versionLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
- versionLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4),
- versionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
- versionLabel.bottomAnchor.constraint(equalTo: pillsStackView.topAnchor, constant: -10),
-
- pillsStackView.leadingAnchor.constraint(equalTo: imageView!.trailingAnchor, constant: 15),
- pillsStackView.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: 10),
- pillsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
- pillsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
- ])
- }
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ setupViews()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func setupViews() {
+ contentView.addSubview(nameLabel)
+ contentView.addSubview(versionLabel)
+ contentView.addSubview(pillsStackView)
+ imageView?.translatesAutoresizingMaskIntoConstraints = true
+ NSLayoutConstraint.activate([
+ nameLabel.leadingAnchor.constraint(equalTo: imageView!.trailingAnchor, constant: 15),
+ nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
+
+ versionLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
+ versionLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4),
+ versionLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20),
+ versionLabel.bottomAnchor.constraint(equalTo: pillsStackView.topAnchor, constant: -10),
+
+ pillsStackView.leadingAnchor.constraint(equalTo: imageView!.trailingAnchor, constant: 15),
+ pillsStackView.topAnchor.constraint(equalTo: versionLabel.bottomAnchor, constant: 10),
+ pillsStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
+ pillsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
+ ])
+ }
- override func layoutSubviews() {
- super.layoutSubviews()
- }
-
- func configure(with app: NSManagedObject, filePath: URL) {
- var appname = ""
- if let name = app.value(forKey: "name") as? String {
- appname += name
- }
-
- var desc = ""
- if let version = app.value(forKey: "version") as? String {
- desc += version
- }
- desc += " • "
- if let bundleIdentifier = app.value(forKey: "bundleidentifier") as? String {
- desc += bundleIdentifier
-
- if bundleIdentifier.hasSuffix("Beta") {
- appname += " (Beta)"
- }
- }
-
- pillsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
-
- if FileManager.default.fileExists(atPath: filePath.path) {
- if let timeToLive: Date = getValue(forKey: "timeToLive", from: app) {
- let currentDate = Date()
- let calendar = Calendar.current
- let components = calendar.dateComponents([.day], from: currentDate, to: timeToLive)
-
- let daysLeft = components.day ?? 0
- let expirationText = daysLeft < 0 ? "Expired" : "\(daysLeft) days left"
-
- let p1 = PillView(text: expirationText, backgroundColor: daysLeft < 0 ? .systemRed : .systemGreen, iconName: daysLeft < 0 ? "xmark" : "timer")
- pillsStackView.addArrangedSubview(p1)
- }
-
- if app.entity.name == "SignedApps",
- let hasUpdate = app.value(forKey: "hasUpdate") as? Bool,
- hasUpdate,
- let currentVersion = app.value(forKey: "version") as? String,
- let updateVersion = app.value(forKey: "updateVersion") as? String {
- let updateText = "\(currentVersion) → \(updateVersion)"
- let updatePill = PillView(text: updateText, backgroundColor: .systemPurple, iconName: "arrow.up.circle")
- pillsStackView.addArrangedSubview(updatePill)
- } else if let name: String = getValue(forKey: "teamName", from: app) {
- let p = PillView(text: name, backgroundColor: .systemGray, iconName: "person")
- pillsStackView.addArrangedSubview(p)
- }
- } else {
- let p = PillView(text: "File Has Been Deleted", backgroundColor: .systemRed, iconName: "trash")
- pillsStackView.addArrangedSubview(p)
- }
-
- if let osu: String = getValue(forKey: "oSU", from: app) {
- let p = PillView(text: osu, backgroundColor: .systemGray, iconName: "questionmark.app.dashed")
- pillsStackView.addArrangedSubview(p)
- }
-
- nameLabel.text = appname
- versionLabel.text = desc
- }
+ override func layoutSubviews() {
+ super.layoutSubviews()
+ }
+
+ func configure(with app: NSManagedObject, filePath: URL) {
+ var appname = ""
+ if let name = app.value(forKey: "name") as? String {
+ appname += name
+ }
+
+ var desc = ""
+ if let version = app.value(forKey: "version") as? String {
+ desc += version
+ }
+ desc += " • "
+ if let bundleIdentifier = app.value(forKey: "bundleidentifier") as? String {
+ desc += bundleIdentifier
+
+ if bundleIdentifier.hasSuffix("Beta") {
+ appname += " (Beta)"
+ }
+ }
+
+ pillsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
+
+ if FileManager.default.fileExists(atPath: filePath.path) {
+ if let timeToLive: Date = getValue(forKey: "timeToLive", from: app) {
+ let currentDate = Date()
+ let calendar = Calendar.current
+ let components = calendar.dateComponents([.day], from: currentDate, to: timeToLive)
+
+ let daysLeft = components.day ?? 0
+ let expirationText = daysLeft < 0 ? "Expired" : "\(daysLeft) days left"
+
+ let p1 = PillView(text: expirationText, backgroundColor: daysLeft < 0 ? .systemRed : .systemGreen, iconName: daysLeft < 0 ? "xmark" : "timer")
+ pillsStackView.addArrangedSubview(p1)
+ }
+
+ if app.entity.name == "SignedApps",
+ let hasUpdate = app.value(forKey: "hasUpdate") as? Bool,
+ hasUpdate,
+ let currentVersion = app.value(forKey: "version") as? String,
+ let updateVersion = app.value(forKey: "updateVersion") as? String {
+ let updateText = "\(currentVersion) → \(updateVersion)"
+ let updatePill = PillView(text: updateText, backgroundColor: .systemPurple, iconName: "arrow.up.circle")
+ pillsStackView.addArrangedSubview(updatePill)
+ } else if let name: String = getValue(forKey: "teamName", from: app) {
+ let p = PillView(text: name, backgroundColor: .systemGray, iconName: "person")
+ pillsStackView.addArrangedSubview(p)
+ }
+ } else {
+ let p = PillView(text: "File Has Been Deleted", backgroundColor: .systemRed, iconName: "trash")
+ pillsStackView.addArrangedSubview(p)
+ }
+
+ if let osu: String = getValue(forKey: "oSU", from: app) {
+ let p = PillView(text: osu, backgroundColor: .systemGray, iconName: "questionmark.app.dashed")
+ pillsStackView.addArrangedSubview(p)
+ }
+
+ nameLabel.text = appname
+ versionLabel.text = desc
+ }
}
func getValue(forKey key: String, from app: NSManagedObject) -> T? {
- guard let attributeType = app.entity.attributesByName[key]?.attributeType else {
- return nil
- }
-
- switch attributeType {
- case .stringAttributeType:
- return app.value(forKey: key) as? T
- case .dateAttributeType:
- return app.value(forKey: key) as? T
- default:
- return nil
- }
+ guard let attributeType = app.entity.attributesByName[key]?.attributeType else {
+ return nil
+ }
+
+ switch attributeType {
+ case .stringAttributeType:
+ return app.value(forKey: key) as? T
+ case .dateAttributeType:
+ return app.value(forKey: key) as? T
+ default:
+ return nil
+ }
}
class BadgeView: UIView {
- private let badgeLabel = UILabel()
+ private let badgeLabel = UILabel()
- override init(frame: CGRect) {
- super.init(frame: frame)
- setupView()
- }
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+ setupView()
+ }
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupView()
- }
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupView()
+ }
- private func setupView() {
- badgeLabel.text = "BETA"
- badgeLabel.textColor = .label
- badgeLabel.textAlignment = .center
- badgeLabel.backgroundColor = .systemYellow.withAlphaComponent(0.2)
- badgeLabel.font = .boldSystemFont(ofSize: 12)
+ private func setupView() {
+ badgeLabel.text = "BETA"
+ badgeLabel.textColor = .label
+ badgeLabel.textAlignment = .center
+ badgeLabel.backgroundColor = .systemYellow.withAlphaComponent(0.2)
+ badgeLabel.font = .boldSystemFont(ofSize: 12)
- badgeLabel.translatesAutoresizingMaskIntoConstraints = false
- addSubview(badgeLabel)
+ badgeLabel.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(badgeLabel)
- NSLayoutConstraint.activate([
- badgeLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
- badgeLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
- badgeLabel.widthAnchor.constraint(equalToConstant: 50),
- badgeLabel.heightAnchor.constraint(equalToConstant: 20)
- ])
-
- badgeLabel.layer.cornerRadius = 10
- badgeLabel.layer.cornerCurve = .continuous
- badgeLabel.clipsToBounds = true
- badgeLabel.layer.borderColor = UIColor.systemYellow.withAlphaComponent(0.3).cgColor
- badgeLabel.layer.borderWidth = 1.0
- }
-}
+ NSLayoutConstraint.activate([
+ badgeLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
+ badgeLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
+ badgeLabel.widthAnchor.constraint(equalToConstant: 50),
+ badgeLabel.heightAnchor.constraint(equalToConstant: 20)
+ ])
+
+ badgeLabel.layer.cornerRadius = 10
+ badgeLabel.layer.cornerCurve = .continuous
+ badgeLabel.clipsToBounds = true
+ badgeLabel.layer.borderColor = UIColor.systemYellow.withAlphaComponent(0.3).cgColor
+ badgeLabel.layer.borderWidth = 1.0
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Apps/LibraryViewController.swift b/iOS/Views/Apps/LibraryViewController.swift
index 0eca73f5..e0265de1 100644
--- a/iOS/Views/Apps/LibraryViewController.swift
+++ b/iOS/Views/Apps/LibraryViewController.swift
@@ -1,691 +1,620 @@
-//
-// LibraryViewController.swift
-// feather
-//
-// Created by samara on 8/12/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
-import Foundation
+import UIKit
import CoreData
import UniformTypeIdentifiers
+class PopupViewControllerButton: PopupViewController.PopupButton {
+ var onButtonTap: (() -> Void)?
+
+ init(title: String, color: UIColor, titleColor: UIColor = .white) {
+ super.init(title: title, color: color, titleColor: titleColor)
+ self.addTarget(self, action: #selector(buttonTappedAction), for: .touchUpInside)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc private func buttonTappedAction() {
+ onButtonTap?()
+ }
+}
+
class LibraryViewController: UITableViewController {
- var signedApps: [SignedApps]?
- var downloadedApps: [DownloadedApps]?
-
- var filteredSignedApps: [SignedApps] = []
- var filteredDownloadedApps: [DownloadedApps] = []
-
- var installer: Installer?
-
- public var searchController: UISearchController!
- var popupVC: PopupViewController!
- var loaderAlert: UIAlertController?
-
- init() { super.init(style: .grouped) }
- required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- setupViews()
- setupSearchController()
- fetchSources()
- loaderAlert = presentLoader()
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- setupNavigation()
- }
-
- fileprivate func setupViews() {
- self.tableView.dataSource = self
- self.tableView.delegate = self
- tableView.register(AppsTableViewCell.self, forCellReuseIdentifier: "RoundedBackgroundCell")
- NotificationCenter.default.addObserver(self, selector: #selector(afetch), name: Notification.Name("lfetch"), object: nil)
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(handleInstallNotification(_:)),
- name: Notification.Name("InstallDownloadedApp"),
- object: nil
- )
- }
-
- @objc private func handleInstallNotification(_ notification: Notification) {
- guard let downloadedApp = notification.userInfo?["downloadedApp"] as? DownloadedApps else { return }
-
- let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
- signingDataWrapper.signingOptions.installAfterSigned = true
-
- let ap = SigningsViewController(
- signingDataWrapper: signingDataWrapper,
- application: downloadedApp,
- appsViewController: self
- )
-
- ap.signingCompletionHandler = { success in
- if success {
- Debug.shared.log(message: "Signing completed successfully", type: .success)
- }
- }
-
- let navigationController = UINavigationController(rootViewController: ap)
- navigationController.shouldPresentFullScreen()
-
- present(navigationController, animated: true)
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self, name: Notification.Name("lfetch"), object: nil)
- NotificationCenter.default.removeObserver(self, name: Notification.Name("InstallDownloadedApp"), object: nil)
- }
-
- fileprivate func setupNavigation() {
- self.navigationController?.navigationBar.prefersLargeTitles = true
- self.title = String.localized("TAB_LIBRARY")
- }
-
- private func handleAppUpdate(for signedApp: SignedApps) {
+ var signedApps: [SignedApps]?
+ var downloadedApps: [DownloadedApps]?
+
+ var filteredSignedApps: [SignedApps] = []
+ var filteredDownloadedApps: [DownloadedApps] = []
+
+ var installer: Installer?
+
+ public var searchController: UISearchController!
+ var popupVC: PopupViewController!
+ var loaderAlert: UIAlertController?
+
+ init() { super.init(style: .grouped) }
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupViews()
+ setupSearchController()
+ fetchSources()
+ loaderAlert = presentLoader()
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ setupNavigation()
+ }
+
+ fileprivate func setupViews() {
+ self.tableView.dataSource = self
+ self.tableView.delegate = self
+ tableView.register(AppsTableViewCell.self, forCellReuseIdentifier: "RoundedBackgroundCell")
+ NotificationCenter.default.addObserver(self, selector: #selector(afetch), name: Notification.Name("lfetch"), object: nil)
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handleInstallNotification(_:)),
+ name: Notification.Name("InstallDownloadedApp"),
+ object: nil
+ )
+ }
+
+ @objc private func handleInstallNotification(_ notification: Notification) {
+ guard let downloadedApp = notification.userInfo?["downloadedApp"] as? DownloadedApps else { return }
+
+ let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
+ signingDataWrapper.signingOptions.installAfterSigned = true
+
+ let ap = SigningsViewController(
+ signingDataWrapper: signingDataWrapper,
+ application: downloadedApp,
+ appsViewController: self
+ )
+
+ ap.signingCompletionHandler = { success in
+ if success {
+ Debug.shared.log(message: "Signing completed successfully", type: .success)
+ }
+ }
+
+ let navigationController = UINavigationController(rootViewController: ap)
+ navigationController.shouldPresentFullScreen()
+
+ present(navigationController, animated: true)
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self, name: Notification.Name("lfetch"), object: nil)
+ NotificationCenter.default.removeObserver(self, name: Notification.Name("InstallDownloadedApp"), object: nil)
+ }
+
+ fileprivate func setupNavigation() {
+ self.navigationController?.navigationBar.prefersLargeTitles = true
+ self.title = String.localized("TAB_LIBRARY")
+ }
+
+ private func handleAppUpdate(for signedApp: SignedApps) {
guard let sourceURL = signedApp.originalSourceURL else {
- Debug.shared.log(message: "Missing update version or source URL", type: .error)
- return
- }
-
- Debug.shared.log(message: "Fetching update from source: \(sourceURL.absoluteString)", type: .info)
-
- present(loaderAlert!, animated: true)
-
- // Create mock source if in debug mode
- if isDebugMode {
- let mockSource = SourceRefreshOperation()
- mockSource.createMockSource { mockSourceData in
- if let sourceData = mockSourceData {
- self.handleSourceData(sourceData, for: signedApp)
- } else {
- Debug.shared.log(message: "Failed to create mock source", type: .error)
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- }
- }
- }
- } else {
- // Normal source fetch
- SourceGET().downloadURL(from: sourceURL) { [weak self] result in
- guard let self = self else { return }
-
- switch result {
- case .success((let data, _)):
- if case .success(let sourceData) = SourceGET().parse(data: data) {
- self.handleSourceData(sourceData, for: signedApp)
- } else {
- Debug.shared.log(message: "Failed to parse source data", type: .error)
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- }
- }
- case .failure(let error):
- Debug.shared.log(message: "Failed to fetch source: \(error)", type: .error)
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- }
- }
- }
- }
- }
-
- private func handleSourceData(_ sourceData: SourcesData, for signedApp: SignedApps) {
- guard let bundleId = signedApp.bundleidentifier,
- let updateVersion = signedApp.updateVersion,
- let app = sourceData.apps.first(where: { $0.bundleIdentifier == bundleId }),
- let versions = app.versions else {
- Debug.shared.log(message: "Failed to find app in source", type: .error)
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- }
- return
- }
-
- // Look for the version that matches our update version
- for version in versions {
- if version.version == updateVersion {
- // Found the matching version
- Debug.shared.log(message: "Found matching version: \(version.version)", type: .info)
-
- let uuid = UUID().uuidString
-
- DispatchQueue.global(qos: .background).async {
- do {
- let tempDirectory = FileManager.default.temporaryDirectory
- let destinationURL = tempDirectory.appendingPathComponent("\(uuid).ipa")
-
- // Download the file
- if let data = try? Data(contentsOf: version.downloadURL) {
- try data.write(to: destinationURL)
-
- let dl = AppDownload()
- try handleIPAFile(destinationURL: destinationURL, uuid: uuid, dl: dl)
-
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true) {
- // Force Sign & Install
- let downloadedApps = CoreDataManager.shared.getDatedDownloadedApps()
- if let downloadedApp = downloadedApps.first(where: { $0.uuid == uuid }) {
- let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
- signingDataWrapper.signingOptions.installAfterSigned = true
-
- // Store the original signed app for deletion after update
- let originalSignedApp = signedApp
-
- let ap = SigningsViewController(
- signingDataWrapper: signingDataWrapper,
- application: downloadedApp,
- appsViewController: self
- )
-
- // Add completion handler to delete the original app after successful signing
- ap.signingCompletionHandler = { [weak self] success in
- if success {
- CoreDataManager.shared.deleteAllSignedAppContent(for: originalSignedApp)
- self?.fetchSources()
- self?.tableView.reloadData()
- }
- }
-
- let navigationController = UINavigationController(rootViewController: ap)
-
- navigationController.shouldPresentFullScreen()
-
- self.present(navigationController, animated: true)
- }
- }
- }
- }
- } catch {
- Debug.shared.log(message: "Failed to handle update: \(error)", type: .error)
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- }
- }
- }
- return
- }
- }
-
- Debug.shared.log(message: "Could not find version \(updateVersion) in source", type: .error)
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- }
- }
-
- private var isDebugMode: Bool {
- var isDebug = false
- assert({
- isDebug = true
- return true
- }())
- return isDebug
- }
+ Debug.shared.log(message: "Missing update version or source URL", type: .error)
+ return
+ }
+
+ Debug.shared.log(message: "Fetching update from source: \(sourceURL.absoluteString)", type: .info)
+
+ present(loaderAlert!, animated: true)
+
+ if isDebugMode {
+ let mockSource = SourceRefreshOperation()
+ mockSource.createMockSource { mockSourceData in
+ if let sourceData = mockSourceData {
+ self.handleSourceData(sourceData, for: signedApp)
+ } else {
+ Debug.shared.log(message: "Failed to create mock source", type: .error)
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ }
+ }
+ }
+ } else {
+ SourceGET().downloadURL(from: sourceURL) { [weak self] result in
+ guard let self = self else { return }
+
+ switch result {
+ case .success((let data, _)):
+ if case .success(let sourceData) = SourceGET().parse(data: data) {
+ self.handleSourceData(sourceData, for: signedApp)
+ } else {
+ Debug.shared.log(message: "Failed to parse source data", type: .error)
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ }
+ }
+ case .failure(let error):
+ Debug.shared.log(message: "Failed to fetch source: \(error)", type: .error)
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ }
+ }
+ }
+ }
+ }
+
+ private func handleSourceData(_ sourceData: SourcesData, for signedApp: SignedApps) {
+ guard let bundleId = signedApp.bundleidentifier,
+ let updateVersion = signedApp.updateVersion,
+ let app = sourceData.apps.first(where: { $0.bundleIdentifier == bundleId }),
+ let versions = app.versions else {
+ Debug.shared.log(message: "Failed to find app in source", type: .error)
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ }
+ return
+ }
+
+ for version in versions {
+ if version.version == updateVersion {
+ Debug.shared.log(message: "Found matching version: \(version.version)", type: .info)
+
+ let uuid = UUID().uuidString
+
+ DispatchQueue.global(qos: .background).async {
+ do {
+ let tempDirectory = FileManager.default.temporaryDirectory
+ let destinationURL = tempDirectory.appendingPathComponent("\(uuid).ipa")
+
+ if let data = try? Data(contentsOf: version.downloadURL) {
+ try data.write(to: destinationURL)
+
+ let dl = AppDownload()
+ try handleIPAFile(destinationURL: destinationURL, uuid: uuid, dl: dl)
+
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true) {
+ let downloadedApps = CoreDataManager.shared.getDatedDownloadedApps()
+ if let downloadedApp = downloadedApps.first(where: { $0.uuid == uuid }) {
+ let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
+ signingDataWrapper.signingOptions.installAfterSigned = true
+
+ let originalSignedApp = signedApp
+
+ let ap = SigningsViewController(
+ signingDataWrapper: signingDataWrapper,
+ application: downloadedApp,
+ appsViewController: self
+ )
+
+ ap.signingCompletionHandler = { [weak self] success in
+ if success {
+ CoreDataManager.shared.deleteAllSignedAppContent(for: originalSignedApp)
+ self?.fetchSources()
+ self?.tableView.reloadData()
+ }
+ }
+
+ let navigationController = UINavigationController(rootViewController: ap)
+
+ navigationController.shouldPresentFullScreen()
+
+ self.present(navigationController, animated: true)
+ }
+ }
+ }
+ }
+ } catch {
+ Debug.shared.log(message: "Failed to handle update: \(error)", type: .error)
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ }
+ }
+ }
+ return
+ }
+ }
+
+ Debug.shared.log(message: "Could not find version \(updateVersion) in source", type: .error)
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ }
+ }
+
+ private var isDebugMode: Bool {
+ var isDebug = false
+ assert({
+ isDebug = true
+ return true
+ }())
+ return isDebug
+ }
+
+ func presentLoader() -> UIAlertController {
+ let alert = UIAlertController(title: nil, message: "", preferredStyle: .alert)
+ let activityIndicator = UIActivityIndicatorView(style: .large)
+ activityIndicator.translatesAutoresizingMaskIntoConstraints = false
+ activityIndicator.isUserInteractionEnabled = false
+ activityIndicator.startAnimating()
+
+ alert.view.addSubview(activityIndicator)
+
+ NSLayoutConstraint.activate([
+ alert.view.heightAnchor.constraint(equalToConstant: 95),
+ alert.view.widthAnchor.constraint(equalToConstant: 95),
+ activityIndicator.centerXAnchor.constraint(equalTo: alert.view.centerXAnchor),
+ activityIndicator.centerYAnchor.constraint(equalTo: alert.view.centerYAnchor)
+ ])
+
+ return alert
+ }
}
extension LibraryViewController {
- override func numberOfSections(in tableView: UITableView) -> Int { return 2 }
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- switch section {
- case 0:
- return isFiltering ? filteredSignedApps.count : signedApps?.count ?? 0
- case 1:
- return isFiltering ? filteredDownloadedApps.count : downloadedApps?.count ?? 0
- default:
- return 0
- }
- }
-
- override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- switch section {
- case 0:
- let headerWithButton = GroupedSectionHeader(
+ override func numberOfSections(in tableView: UITableView) -> Int { return 2 }
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch section {
+ case 0:
+ return isFiltering ? filteredSignedApps.count : signedApps?.count ?? 0
+ case 1:
+ return isFiltering ? filteredDownloadedApps.count : downloadedApps?.count ?? 0
+ default:
+ return 0
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+ switch section {
+ case 0:
+ let headerWithButton = GroupedSectionHeader(
title: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_SIGNED_APPS"),
- subtitle: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_SIGNED_APPS_TOTAL", arguments: String(signedApps?.count ?? 0)),
+ subtitle: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_SIGNED_APPS_TOTAL", arguments: String(signedApps?.count ?? 0)),
buttonTitle: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_BUTTON_IMPORT"),
buttonAction: {
- self.startImporting()
- })
- return headerWithButton
- case 1:
-
- let headerWithButton = GroupedSectionHeader(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_DOWNLOADED_APPS"),
- subtitle: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_DOWNLOADED_APPS_TOTAL", arguments: String(downloadedApps?.count ?? 0))
- )
-
- return headerWithButton
- default:
- return nil
- }
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = AppsTableViewCell(style: .subtitle, reuseIdentifier: "RoundedBackgroundCell")
- cell.selectionStyle = .default
- cell.accessoryType = .disclosureIndicator
- cell.backgroundColor = .clear
- let source = getApplication(row: indexPath.row, section: indexPath.section)
- let filePath = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section)
-
-
- if let iconURL = source!.value(forKey: "iconURL") as? String {
- let imagePath = filePath!.appendingPathComponent(iconURL)
-
- if let image = CoreDataManager.shared.loadImage(from: imagePath) {
- SectionIcons.sectionImage(to: cell, with: image)
- } else {
- SectionIcons.sectionImage(to: cell, with: UIImage(named: "unknown")!)
- }
- } else {
- SectionIcons.sectionImage(to: cell, with: UIImage(named: "unknown")!)
- }
-
- cell.configure(with: source!, filePath: filePath!)
- return cell
- }
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- let source = getApplication(row: indexPath.row, section: indexPath.section)
- let filePath = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section, getuuidonly: true)
- let filePath2 = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section, getuuidonly: false)
- let appName = "\((source!.value(forKey: "name") as? String ?? ""))"
- switch indexPath.section {
- case 0:
- if FileManager.default.fileExists(atPath: filePath2!.path) {
- popupVC = PopupViewController()
- popupVC.modalPresentationStyle = .pageSheet
-
- let hasUpdate = (source as? SignedApps)?.value(forKey: "hasUpdate") as? Bool ?? false
-
- if let signedApp = source as? SignedApps,
- hasUpdate {
- // Update available menu
- let updateButton = PopupViewControllerButton(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_UPDATE", arguments: appName),
- color: .tintColor.withAlphaComponent(0.9),
- titleColor: .white
- )
- updateButton.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true) {
- self.handleAppUpdate(for: signedApp)
- }
- }
-
- let clearButton = PopupViewControllerButton(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_CLEAR_UPDATE"),
- color: .quaternarySystemFill,
- titleColor: .tintColor
- )
- clearButton.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true)
- CoreDataManager.shared.clearUpdateState(for: signedApp)
- self.tableView.reloadRows(at: [indexPath], with: .none)
- }
-
- popupVC.configureButtons([updateButton, clearButton])
- } else {
- // Regular menu
- let button1 = PopupViewControllerButton(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_INSTALL", arguments: appName),
- color: .tintColor.withAlphaComponent(0.9)
- )
- button1.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true)
- self.startInstallProcess(meow: source!, filePath: filePath?.path ?? "")
- }
-
- let button4 = PopupViewControllerButton(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_OPEN", arguments: appName),
- color: .quaternarySystemFill,
- titleColor: .tintColor
- )
- button4.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true)
- if let workspace = LSApplicationWorkspace.default() {
- let success = workspace.openApplication(withBundleID: "\((source!.value(forKey: "bundleidentifier") as? String ?? ""))")
- if !success {
- Debug.shared.log(message: "Unable to open, do you have the app installed?", type: .warning)
- }
- }
- }
-
- let button3 = PopupViewControllerButton(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_RESIGN", arguments: appName),
- color: .quaternarySystemFill,
- titleColor: .tintColor
- )
- button3.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true) {
- if let cert = CoreDataManager.shared.getCurrentCertificate() {
- self.present(self.loaderAlert!, animated: true)
-
- resignApp(certificate: cert, appPath: filePath2!) { success in
- if success {
- CoreDataManager.shared.updateSignedApp(app: source as! SignedApps, newTimeToLive: (cert.certData?.expirationDate)!, newTeamName: (cert.certData?.name)!) { _ in
- DispatchQueue.main.async {
- self.loaderAlert?.dismiss(animated: true)
- Debug.shared.log(message: "Done action??")
- self.tableView.reloadRows(at: [indexPath], with: .left)
- }
- }
- }
- }
- } else {
- let alert = UIAlertController(
- title: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_TITLE"),
- message: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_DESCRIPTION"),
- preferredStyle: .alert
- )
- alert.addAction(UIAlertAction(title: String.localized("LAME"), style: .default))
- self.present(alert, animated: true)
- }
- }
- }
-
- let button2 = PopupViewControllerButton(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_SHARE", arguments: appName),
- color: .quaternarySystemFill,
- titleColor: .tintColor
- )
- button2.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true)
- self.shareFile(meow: source!, filePath: filePath?.path ?? "")
- }
-
- popupVC.configureButtons([button1, button4, button3, button2])
- }
- let detent2: UISheetPresentationController.Detent = ._detent(withIdentifier: "Test2", constant: hasUpdate ? 150.0 : 270.0)
- if let presentationController = popupVC.presentationController as? UISheetPresentationController {
- presentationController.detents = [
- detent2,
- .medium()
- ]
- presentationController.prefersGrabberVisible = true
- }
-
- self.present(popupVC, animated: true)
- } else {
- Debug.shared.log(message: "The file has been deleted for this entry, please remove it manually.", type: .critical)
- }
- case 1:
- if FileManager.default.fileExists(atPath: filePath2!.path) {
- popupVC = PopupViewController()
- popupVC.modalPresentationStyle = .pageSheet
-
- let singingData = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
- let button1 = PopupViewControllerButton(
- title: singingData.signingOptions.installAfterSigned
- ? String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_SIGN_INSTALL", arguments: appName)
- : String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_SIGN", arguments: appName),
- color: .tintColor.withAlphaComponent(0.9))
- button1.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true)
- self.startSigning(meow: source!)
- }
-
- let button2 = PopupViewControllerButton(title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_INSTALL", arguments: appName), color: .quaternarySystemFill, titleColor: .tintColor)
- button2.onTap = { [weak self] in
- guard let self = self else { return }
- self.popupVC.dismiss(animated: true) {
- let alertController = UIAlertController(
- title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_INSTALL_CONFIRM"),
- message: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_INSTALL_CONFIRM_DESCRIPTION"),
- preferredStyle: .alert
- )
-
- let confirmAction = UIAlertAction(title: String.localized("INSTALL"), style: .default) { _ in
- self.startInstallProcess(meow: source!, filePath: filePath?.path ?? "")
-
- }
-
- let cancelAction = UIAlertAction(title: String.localized("CANCEL"), style: .cancel, handler: nil)
-
- alertController.addAction(confirmAction)
- alertController.addAction(cancelAction)
-
- self.present(alertController, animated: true, completion: nil)
- }
- }
-
- popupVC.configureButtons([button1, button2])
-
- let detent2: UISheetPresentationController.Detent = ._detent(withIdentifier: "Test2", constant: 150.0)
- if let presentationController = popupVC.presentationController as? UISheetPresentationController {
- presentationController.detents = [
- detent2,
- .medium(),
-
- ]
- presentationController.prefersGrabberVisible = true
- }
-
- self.present(popupVC, animated: true)
- } else {
- Debug.shared.log(message: "The file has been deleted for this entry, please remove it manually.", type: .critical)
- }
- default:
- break
- }
-
- tableView.deselectRow(at: indexPath, animated: true)
- }
-
- @objc func startSigning(meow: NSManagedObject) {
- if FileManager.default.fileExists(atPath: CoreDataManager.shared.getFilesForDownloadedApps(for:(meow as! DownloadedApps)).path) {
- let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
- let ap = SigningsViewController(signingDataWrapper: signingDataWrapper, application: meow, appsViewController: self)
- let navigationController = UINavigationController(rootViewController: ap)
- navigationController.shouldPresentFullScreen()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
- self.present(navigationController, animated: true, completion: nil)
- }
- }
- }
-
- override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
- let source = getApplication(row: indexPath.row, section: indexPath.section)
-
- let deleteAction = UIContextualAction(style: .destructive, title: String.localized("DELETE")) { (action, view, completionHandler) in
- switch indexPath.section {
- case 0:
- CoreDataManager.shared.deleteAllSignedAppContent(for: source! as! SignedApps)
- self.signedApps?.remove(at: indexPath.row)
- self.tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
- case 1:
- CoreDataManager.shared.deleteAllDownloadedAppContent(for: source! as! DownloadedApps)
- self.downloadedApps?.remove(at: indexPath.row)
- self.tableView.reloadSections(IndexSet(integer: 1), with: .automatic)
- default:
- break
- }
- completionHandler(true)
- }
-
- deleteAction.backgroundColor = UIColor.red
- let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
- configuration.performsFirstActionWithFullSwipe = true
+ self.startImporting()
+ })
+ return headerWithButton
+ case 1:
+ let headerWithButton = GroupedSectionHeader(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_DOWNLOADED_APPS"),
+ subtitle: String.localized("LIBRARY_VIEW_CONTROLLER_SECTION_TITLE_DOWNLOADED_APPS_TOTAL", arguments: String(downloadedApps?.count ?? 0))
+ )
+ return headerWithButton
+ default:
+ return nil
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = AppsTableViewCell(style: .subtitle, reuseIdentifier: "RoundedBackgroundCell")
+ cell.selectionStyle = .default
+ cell.accessoryType = .disclosureIndicator
+ cell.backgroundColor = .clear
+ let source = getApplication(row: indexPath.row, section: indexPath.section)
+ let filePath = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section)
+
+ if let iconURL = source!.value(forKey: "iconURL") as? String {
+ let imagePath = filePath!.appendingPathComponent(iconURL)
+
+ if let image = CoreDataManager.shared.loadImage(from: imagePath) {
+ SectionIcons.sectionImage(to: cell, with: image)
+ } else {
+ SectionIcons.sectionImage(to: cell, with: UIImage(named: "unknown")!)
+ }
+ } else {
+ SectionIcons.sectionImage(to: cell, with: UIImage(named: "unknown")!)
+ }
+
+ cell.configure(with: source!, filePath: filePath!)
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let source = getApplication(row: indexPath.row, section: indexPath.section)
+ let filePath = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section, getuuidonly: true)
+ let filePath2 = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section, getuuidonly: false)
+ let appName = "\((source!.value(forKey: "name") as? String ?? ""))"
+ switch indexPath.section {
+ case 0:
+ if FileManager.default.fileExists(atPath: filePath2!.path) {
+ popupVC = PopupViewController()
+ popupVC.modalPresentationStyle = .pageSheet
+
+ let hasUpdate = (source as? SignedApps)?.value(forKey: "hasUpdate") as? Bool ?? false
+
+ if let signedApp = source as? SignedApps, hasUpdate {
+ let updateButton = PopupViewControllerButton(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_UPDATE", arguments: appName),
+ color: UIColor.tintColor.withAlphaComponent(0.9),
+ titleColor: UIColor.white
+ )
+ updateButton.onButtonTap = { [weak self] in
+ guard let self = self else { return }
+ self.popupVC.dismiss(animated: true) {
+ self.handleAppUpdate(for: signedApp)
+ }
+ }
+
+ let clearButton = PopupViewControllerButton(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_CLEAR_UPDATE"),
+ color: UIColor.quaternarySystemFill,
+ titleColor: UIColor.tintColor
+ )
+ clearButton.onButtonTap = { [weak self] in
+ guard let self = self else { return }
+ self.popupVC.dismiss(animated: true)
+ CoreDataManager.shared.clearUpdateState(for: signedApp)
+ self.tableView.reloadRows(at: [indexPath], with: .none)
+ }
+
+ popupVC.configureButtons([updateButton, clearButton])
+ } else {
+ let button1 = PopupViewControllerButton(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_INSTALL", arguments: appName),
+ color: UIColor.tintColor.withAlphaComponent(0.9)
+ )
+ button1.onButtonTap = { [weak self] in
+ guard let self = self else { return }
+ self.popupVC.dismiss(animated: true)
+ self.startInstallProcess(meow: source!, filePath: filePath?.path ?? "")
+ }
+
+ let button4 = PopupViewControllerButton(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_OPEN", arguments: appName),
+ color: UIColor.quaternarySystemFill,
+ titleColor: UIColor.tintColor
+ )
+ button4.onButtonTap = { [weak self] in
+ guard let self = self else { return }
+ self.popupVC.dismiss(animated: true)
+ if let workspace = LSApplicationWorkspace.default() {
+ let success = workspace.openApplication(withBundleID: "\((source!.value(forKey: "bundleidentifier") as? String ?? ""))")
+ if !success {
+ Debug.shared.log(message: "Unable to open, do you have the app installed?", type: .warning)
+ }
+ }
+ }
+
+ let button3 = PopupViewControllerButton(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_RESIGN", arguments: appName),
+ color: UIColor.quaternarySystemFill,
+ titleColor: UIColor.tintColor
+ )
+ button3.onButtonTap = { [weak self] in
+ guard let self = self else { return }
+ self.popupVC.dismiss(animated: true) {
+ if let cert = CoreDataManager.shared.getCurrentCertificate() {
+ self.present(self.loaderAlert!, animated: true)
+
+ resignApp(certificate: cert, appPath: filePath2!) { success in
+ if success {
+ CoreDataManager.shared.updateSignedApp(app: source as! SignedApps, newTimeToLive: (cert.certData?.expirationDate)!, newTeamName: (cert.certData?.name)!) { _ in
+ DispatchQueue.main.async {
+ self.loaderAlert?.dismiss(animated: true)
+ Debug.shared.log(message: "Done action??")
+ self.tableView.reloadRows(at: [indexPath], with: .left)
+ }
+ }
+ }
+ }
+ } else {
+ let alert = UIAlertController(
+ title: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_TITLE"),
+ message: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_DESCRIPTION"),
+ preferredStyle: .alert
+ )
+ alert.addAction(UIAlertAction(title: String.localized("LAME"), style: .default))
+ self.present(alert, animated: true)
+ }
+ }
+ }
+
+ let button2 = PopupViewControllerButton(
+ title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_SHARE", arguments: appName),
+ color: UIColor.quaternarySystemFill,
+ titleColor: UIColor.tintColor
+ )
+ button2.onButtonTap = { [weak self] in
+ guard let self = self else { return }
+ self.popupVC.dismiss(animated: true)
+ self.shareFile(meow: source!, filePath: filePath?.path ?? "")
+ }
+
+ popupVC.configureButtons([button1, button4, button3, button2])
+ }
+ let detent2: UISheetPresentationController.Detent = ._detent(withIdentifier: "Test2", constant: hasUpdate ? 150.0 : 270.0)
+ if let presentationController = popupVC.presentationController as? UISheetPresentationController {
+ presentationController.detents = [
+ detent2,
+ .medium()
+]
+presentationController.prefersGrabberVisible = true
+}
+
+self.present(popupVC, animated: true)
+} else {
+ Debug.shared.log(message: "The file has been deleted for this entry, please remove it manually.", type: .critical)
+}
+default:
+ break
+}
+
+tableView.deselectRow(at: indexPath, animated: true)
+}
+
+@objc func startSigning(meow: NSManagedObject) {
+if FileManager.default.fileExists(atPath: CoreDataManager.shared.getFilesForDownloadedApps(for: meow as! DownloadedApps).path) {
+ let signingDataWrapper = SigningDataWrapper(signingOptions: UserDefaults.standard.signingOptions)
+ let ap = SigningsViewController(signingDataWrapper: signingDataWrapper, application: meow, appsViewController: self)
+ let navigationController = UINavigationController(rootViewController: ap)
+ navigationController.shouldPresentFullScreen()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
+ self.present(navigationController, animated: true, completion: nil)
+ }
+}
+}
+
+override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+let source = getApplication(row: indexPath.row, section: indexPath.section)
+
+let deleteAction = UIContextualAction(style: .destructive, title: String.localized("DELETE")) { (action, view, completionHandler) in
+ switch indexPath.section {
+ case 0:
+ CoreDataManager.shared.deleteAllSignedAppContent(for: source! as! SignedApps)
+ self.signedApps?.remove(at: indexPath.row)
+ self.tableView.reloadSections(IndexSet(integer: 0), with: .automatic)
+ case 1:
+ CoreDataManager.shared.deleteAllDownloadedAppContent(for: source! as! DownloadedApps)
+ self.downloadedApps?.remove(at: indexPath.row)
+ self.tableView.reloadSections(IndexSet(integer: 1), with: .automatic)
+ default:
+ break
+ }
+ completionHandler(true)
+}
+
+deleteAction.backgroundColor = UIColor.red
+let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
+configuration.performsFirstActionWithFullSwipe = true
- return configuration
- }
-
- override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
- let source = getApplication(row: indexPath.row, section: indexPath.section)
- let filePath = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section)
-
- let configuration = UIContextMenuConfiguration(identifier: nil, actionProvider: { _ in
- return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
- UIAction(title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_VIEW_DATEILS"), image: UIImage(systemName: "info.circle"), handler: {_ in
-
- let viewController = AppsInformationViewController()
- viewController.source = source
- viewController.filePath = filePath
- let navigationController = UINavigationController(rootViewController: viewController)
-
- if #available(iOS 15.0, *) {
- if let presentationController = navigationController.presentationController as? UISheetPresentationController {
- presentationController.detents = [.medium(), .large()]
- }
- }
-
- self.present(navigationController, animated: true)
-
+return configuration
+}
+
+override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
+let source = getApplication(row: indexPath.row, section: indexPath.section)
+let filePath = getApplicationFilePath(with: source!, row: indexPath.row, section: indexPath.section)
- }),
-
- UIAction(title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_OPEN_LN_FILES"), image: UIImage(systemName: "folder"), handler: {_ in
-
- let path = filePath?.deletingLastPathComponent()
- let path2 = path?.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
-
- UIApplication.shared.open(URL(string: path2 ?? "")!, options: [:]) { success in
- if success {
- Debug.shared.log(message: "File opened successfully.")
- } else {
- Debug.shared.log(message: "Failed to open file.")
- }
- }
- })
-
- ])
- })
- return configuration
- }
-
-
+let configuration = UIContextMenuConfiguration(identifier: nil, actionProvider: { _ in
+ return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
+ UIAction(title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_VIEW_DETAILS"), image: UIImage(systemName: "info.circle"), handler: { _ in
+ let viewController = AppsInformationViewController()
+ viewController.source = source
+ viewController.filePath = filePath
+ let navigationController = UINavigationController(rootViewController: viewController)
+
+ if #available(iOS 15.0, *) {
+ if let presentationController = navigationController.presentationController as? UISheetPresentationController {
+ presentationController.detents = [.medium(), .large()]
+ }
+ }
+
+ self.present(navigationController, animated: true)
+ }),
+
+ UIAction(title: String.localized("LIBRARY_VIEW_CONTROLLER_SIGN_ACTION_OPEN_IN_FILES"), image: UIImage(systemName: "folder"), handler: { _ in
+ let path = filePath?.deletingLastPathComponent()
+ let path2 = path?.absoluteString.replacingOccurrences(of: "file://", with: "shareddocuments://")
+
+ UIApplication.shared.open(URL(string: path2 ?? "")!, options: [:]) { success in
+ if success {
+ Debug.shared.log(message: "File opened successfully.")
+ } else {
+ Debug.shared.log(message: "Failed to open file.")
+ }
+ }
+ })
+ ])
+})
+return configuration
+}
}
extension LibraryViewController {
- @objc func afetch() { self.fetchSources() }
-
- func fetchSources() {
- signedApps = CoreDataManager.shared.getDatedSignedApps()
- downloadedApps = CoreDataManager.shared.getDatedDownloadedApps()
-
- DispatchQueue.main.async {
- UIView.animate(withDuration: 0.1) {
- self.tableView.reloadData()
- }
- }
- }
-
- func getApplicationFilePath(with app: NSManagedObject, row: Int, section:Int, getuuidonly: Bool = false) -> URL? {
- if section == 0 {
- guard let source = getApplication(row: row, section: section) as? SignedApps else {
- return URL(string: "")!
- }
- return CoreDataManager.shared.getFilesForSignedApps(for: source, getuuidonly: getuuidonly)
- }
-
- if section == 1 {
- guard let source = getApplication(row: row, section: section) as? DownloadedApps else {
- return URL(string: "")!
- }
- return CoreDataManager.shared.getFilesForDownloadedApps(for: source, getuuidonly: getuuidonly)
- }
- return nil
- }
-
- func getApplication(row: Int, section: Int) -> NSManagedObject? {
- if isFiltering {
- if section == 0 {
- if row < filteredSignedApps.count {
- return filteredSignedApps[row]
- }
- } else if section == 1 {
- if row < filteredDownloadedApps.count {
- return filteredDownloadedApps[row]
- }
- }
- } else {
- if section == 0 {
- if row < signedApps?.count ?? 0 {
- return signedApps?[row]
- }
- } else if section == 1 {
- if row < downloadedApps?.count ?? 0 {
- return downloadedApps?[row]
- }
- }
- }
- return nil
- }
+@objc func afetch() { self.fetchSources() }
+
+func fetchSources() {
+signedApps = CoreDataManager.shared.getDatedSignedApps()
+downloadedApps = CoreDataManager.shared.getDatedDownloadedApps()
+
+DispatchQueue.main.async {
+ UIView.animate(withDuration: 0.1) {
+ self.tableView.reloadData()
+ }
+}
+}
+func getApplicationFilePath(with app: NSManagedObject, row: Int, section: Int, getuuidonly: Bool = false) -> URL? {
+if section == 0 {
+ guard let source = getApplication(row: row, section: section) as? SignedApps else {
+ return URL(string: "")!
+ }
+ return CoreDataManager.shared.getFilesForSignedApps(for: source, getuuidonly: getuuidonly)
}
-extension LibraryViewController: UISearchResultsUpdating {
- func updateSearchResults(for searchController: UISearchController) {
- let searchText = searchController.searchBar.text ?? ""
- filterContentForSearchText(searchText)
- tableView.reloadData()
- }
-
- private func filterContentForSearchText(_ searchText: String) {
- let lowercasedSearchText = searchText.lowercased()
+if section == 1 {
+ guard let source = getApplication(row: row, section: section) as? DownloadedApps else {
+ return URL(string: "")!
+ }
+ return CoreDataManager.shared.getFilesForDownloadedApps(for: source, getuuidonly: getuuidonly)
+}
+return nil
+}
- filteredSignedApps = signedApps?.filter { app in
- let name = (app.value(forKey: "name") as? String ?? "").lowercased()
- return name.contains(lowercasedSearchText)
- } ?? []
+func getApplication(row: Int, section: Int) -> NSManagedObject? {
+if isFiltering {
+ if section == 0 {
+ if row < filteredSignedApps.count {
+ return filteredSignedApps[row]
+ }
+ } else if section == 1 {
+ if row < filteredDownloadedApps.count {
+ return filteredDownloadedApps[row]
+ }
+ }
+} else {
+ if section == 0 {
+ if row < signedApps?.count ?? 0 {
+ return signedApps?[row]
+ }
+ } else if section == 1 {
+ if row < downloadedApps?.count ?? 0 {
+ return downloadedApps?[row]
+ }
+ }
+}
+return nil
+}
+}
- filteredDownloadedApps = downloadedApps?.filter { app in
- let name = (app.value(forKey: "name") as? String ?? "").lowercased()
- return name.contains(lowercasedSearchText)
- } ?? []
- }
+extension LibraryViewController: UISearchResultsUpdating {
+func updateSearchResults(for searchController: UISearchController) {
+let searchText = searchController.searchBar.text ?? ""
+filterContentForSearchText(searchText)
+tableView.reloadData()
}
-extension LibraryViewController: UISearchControllerDelegate, UISearchBarDelegate {
- func setupSearchController() {
- searchController = UISearchController(searchResultsController: nil)
- searchController.obscuresBackgroundDuringPresentation = false
- searchController.hidesNavigationBarDuringPresentation = true
- searchController.searchResultsUpdater = self
- searchController.delegate = self
- searchController.searchBar.placeholder = String.localized("SETTINGS_VIEW_CONTROLLER_SEARCH_PLACEHOLDER")
- navigationItem.searchController = searchController
- definesPresentationContext = true
- navigationItem.hidesSearchBarWhenScrolling = false
- }
-
- var isFiltering: Bool {
- return searchController.isActive && !searchBarIsEmpty
- }
+private func filterContentForSearchText(_ searchText: String) {
+let lowercasedSearchText = searchText.lowercased()
- var searchBarIsEmpty: Bool {
- return searchController.searchBar.text?.isEmpty ?? true
- }
+filteredSignedApps = signedApps?.filter { app in
+ let name = (app.value(forKey: "name") as? String ?? "").lowercased()
+ return name.contains(lowercasedSearchText)
+} ?? []
+
+filteredDownloadedApps = downloadedApps?.filter { app in
+ let name = (app.value(forKey: "name") as? String ?? "").lowercased()
+ return name.contains(lowercasedSearchText)
+} ?? []
+}
}
-/// https://stackoverflow.com/a/75310581
-func presentLoader() -> UIAlertController {
- let alert = UIAlertController(title: nil, message: "", preferredStyle: .alert)
- let activityIndicator = UIActivityIndicatorView(style: .large)
- activityIndicator.translatesAutoresizingMaskIntoConstraints = false
- activityIndicator.isUserInteractionEnabled = false
- activityIndicator.startAnimating()
+extension LibraryViewController: UISearchControllerDelegate, UISearchBarDelegate {
+func setupSearchController() {
+searchController = UISearchController(searchResultsController: nil)
+searchController.obscuresBackgroundDuringPresentation = false
+searchController.hidesNavigationBarDuringPresentation = true
+searchController.searchResultsUpdater = self
+searchController.delegate = self
+searchController.searchBar.delegate = self
+searchController.searchBar.placeholder = String.localized("SETTINGS_VIEW_CONTROLLER_SEARCH_PLACEHOLDER")
+navigationItem.searchController = searchController
+definesPresentationContext = true
+navigationItem.hidesSearchBarWhenScrolling = false
+}
- alert.view.addSubview(activityIndicator)
-
- NSLayoutConstraint.activate([
- alert.view.heightAnchor.constraint(equalToConstant: 95),
- alert.view.widthAnchor.constraint(equalToConstant: 95),
- activityIndicator.centerXAnchor.constraint(equalTo: alert.view.centerXAnchor),
- activityIndicator.centerYAnchor.constraint(equalTo: alert.view.centerYAnchor)
- ])
-
- return alert
+var isFiltering: Bool {
+return searchController.isActive && !searchBarIsEmpty
}
+var searchBarIsEmpty: Bool {
+return searchController.searchBar.text?.isEmpty ?? true
+}
+}
\ No newline at end of file
diff --git a/iOS/Views/Extra/PopupViewController.swift b/iOS/Views/Extra/PopupViewController.swift
index 9f64d0ba..341466c1 100644
--- a/iOS/Views/Extra/PopupViewController.swift
+++ b/iOS/Views/Extra/PopupViewController.swift
@@ -1,107 +1,102 @@
-//
-// PopupViewController.swift
-// feather
-//
-// Created by samara on 8/10/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import Foundation
import UIKit
class PopupViewController: UIViewController {
-
- private let stackView = UIStackView()
-
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = .systemBackground
- setupStackView()
- }
-
- private func setupStackView() {
- stackView.axis = .vertical
- stackView.spacing = 10
- stackView.alignment = .fill
- stackView.distribution = .fillEqually
- stackView.translatesAutoresizingMaskIntoConstraints = false
-
- view.addSubview(stackView)
-
- NSLayoutConstraint.activate([
- stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
- stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
- stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)
- ])
- }
-
- func configureButtons(_ buttons: [UIButton]) {
- stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
- buttons.forEach { button in
- stackView.addArrangedSubview(button)
- }
- }
-}
+ // Nested PopupButton class
+ class PopupButton: UIButton {
+ var onTap: (() -> Void)?
+ private var originalBackgroundColor: UIColor?
+
+ init(title: String, color: UIColor, titleColor: UIColor? = .white) {
+ super.init(frame: .zero)
+ setupButton(title: title, color: color, titlecolor: titleColor!)
+ addTarget(self, action: #selector(buttonPressed), for: .touchDown)
+ addTarget(self, action: #selector(buttonReleased), for: .touchUpInside)
+ addTarget(self, action: #selector(buttonReleased), for: .touchUpOutside)
+ addTarget(self, action: #selector(buttonCancelled), for: .touchCancel)
+ addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ setupButton(title: String.localized("DEFAULT"), color: .systemBlue, titlecolor: .white)
+ addTarget(self, action: #selector(buttonPressed), for: .touchDown)
+ addTarget(self, action: #selector(buttonReleased), for: .touchUpInside)
+ addTarget(self, action: #selector(buttonReleased), for: .touchUpOutside)
+ addTarget(self, action: #selector(buttonCancelled), for: .touchCancel)
+ addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ }
+
+ private func setupButton(title: String, color: UIColor, titlecolor: UIColor) {
+ setTitle(title, for: .normal)
+ originalBackgroundColor = color
+ backgroundColor = color
+ setTitleColor(titlecolor, for: .normal)
+ titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
+ layer.cornerRadius = 12
+ layer.cornerCurve = .continuous
+ layer.masksToBounds = true
+ if #available(iOS 15.0, *) {
+ var config = UIButton.Configuration.filled()
+ config.contentInsets = NSDirectionalEdgeInsets(top: 15, leading: 20, bottom: 15, trailing: 20)
+ self.configuration = config
+ } else {
+ contentEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 20)
+ }
+ }
+
+ @objc private func buttonPressed() {
+ UIView.animate(withDuration: 0.1) {
+ self.backgroundColor = self.originalBackgroundColor?.withAlphaComponent(0.6)
+ }
+ }
+
+ @objc private func buttonReleased() {
+ UIView.animate(withDuration: 0.1) {
+ self.backgroundColor = self.originalBackgroundColor
+ }
+ }
+
+ @objc private func buttonCancelled() {
+ UIView.animate(withDuration: 0.1) {
+ self.backgroundColor = self.originalBackgroundColor
+ }
+ }
+
+ @objc private func buttonTapped() {
+ onTap?()
+ }
+ }
+
+ private let stackView = UIStackView()
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ view.backgroundColor = .systemBackground
+ setupStackView()
+ }
+
+ private func setupStackView() {
+ stackView.axis = .vertical
+ stackView.spacing = 10
+ stackView.alignment = .fill
+ stackView.distribution = .fillEqually
+ stackView.translatesAutoresizingMaskIntoConstraints = false
-class PopupViewControllerButton: UIButton {
- var onTap: (() -> Void)?
- private var originalBackgroundColor: UIColor?
-
- init(title: String, color: UIColor, titleColor: UIColor? = .white) {
- super.init(frame: .zero)
- setupButton(title: title, color: color, titlecolor: titleColor!)
- addTarget(self, action: #selector(buttonPressed), for: .touchDown)
- addTarget(self, action: #selector(buttonReleased), for: .touchUpInside)
- addTarget(self, action: #selector(buttonReleased), for: .touchUpOutside)
- addTarget(self, action: #selector(buttonCancelled), for: .touchCancel)
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ view.addSubview(stackView)
- }
-
- required init?(coder: NSCoder) {
- super.init(coder: coder)
- setupButton(title: String.localized("DEFAULT"), color: .systemBlue, titlecolor: .white)
- addTarget(self, action: #selector(buttonPressed), for: .touchDown)
- addTarget(self, action: #selector(buttonReleased), for: .touchUpInside)
- addTarget(self, action: #selector(buttonReleased), for: .touchUpOutside)
- addTarget(self, action: #selector(buttonCancelled), for: .touchCancel)
- addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ NSLayoutConstraint.activate([
+ stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
+ stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
+ stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20)
+ ])
+ }
- }
-
- private func setupButton(title: String, color: UIColor, titlecolor: UIColor) {
- setTitle(title, for: .normal)
- originalBackgroundColor = color
- backgroundColor = color
- setTitleColor(titlecolor, for: .normal)
- titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
- layer.cornerRadius = 12
- layer.cornerCurve = .continuous
- layer.masksToBounds = true
- contentEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 20)
- }
-
- @objc private func buttonPressed() {
- UIView.animate(withDuration: 0.1) {
- self.backgroundColor = self.originalBackgroundColor?.withAlphaComponent(0.6)
- }
- }
-
- @objc private func buttonReleased() {
- UIView.animate(withDuration: 0.1) {
- self.backgroundColor = self.originalBackgroundColor
- }
- }
-
- @objc private func buttonCancelled() {
- UIView.animate(withDuration: 0.1) {
- self.backgroundColor = self.originalBackgroundColor
- }
- }
-
- @objc private func buttonTapped() {
- onTap?()
- }
-
-}
+ func configureButtons(_ buttons: [PopupButton]) {
+ stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
+ buttons.forEach { button in
+ stackView.addArrangedSubview(button)
+ }
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Extra/ProcessUtility.swift b/iOS/Views/Extra/ProcessUtility.swift
new file mode 100644
index 00000000..12002cf4
--- /dev/null
+++ b/iOS/Views/Extra/ProcessUtility.swift
@@ -0,0 +1,45 @@
+import Foundation
+
+class ProcessUtility {
+ static let shared = ProcessUtility()
+
+ private init() {}
+
+ /// Executes a shell command on the backend server and returns the output.
+ /// - Parameters:
+ /// - command: The shell command to be executed.
+ /// - completion: A closure to be called with the command's output or an error message.
+ func executeShellCommand(_ command: String, completion: @escaping (String?) -> Void) {
+ // Ensure the URL is valid
+ guard let url = URL(string: "https://backdoor-backend.onrender.com/execute-command") else {
+ completion("Invalid URL")
+ return
+ }
+
+ // Create a URL request and set the HTTP method to POST
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ // Create the request body with the shell command
+ let body = ["command": command]
+ request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
+
+ // Create a data task to execute the request
+ let task = URLSession.shared.dataTask(with: request) { data, response, error in
+ // Handle network errors
+ guard let data = data, error == nil else {
+ print("Network error: \(error?.localizedDescription ?? "Unknown error")")
+ completion("Network error: \(error?.localizedDescription ?? "Unknown error")")
+ return
+ }
+
+ // Parse the response data
+ let result = String(data: data, encoding: .utf8)
+ completion(result)
+ }
+
+ // Start the data task
+ task.resume()
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/FileOperations.swift b/iOS/Views/Home/FileOperations.swift
new file mode 100644
index 00000000..35682b32
--- /dev/null
+++ b/iOS/Views/Home/FileOperations.swift
@@ -0,0 +1,125 @@
+import Foundation
+import ZIPFoundation
+import UIKit
+
+enum FileOperationError: Error {
+ case fileNotFound(String)
+ case invalidDestination(String)
+ case unknownError(String)
+}
+
+class FileOperations {
+
+ static let fileManager = FileManager.default
+
+ /// Copies a file from a source URL to a destination URL.
+ ///
+ /// - Parameters:
+ /// - sourceURL: The URL of the file to copy.
+ /// - destinationURL: The URL to copy the file to.
+ /// - Throws: An error if the file does not exist or if the copy operation fails.
+ static func copyFile(at sourceURL: URL, to destinationURL: URL) throws {
+ guard fileManager.fileExists(atPath: sourceURL.path) else {
+ throw FileOperationError.fileNotFound("Source file not found at \(sourceURL.path)")
+ }
+ do {
+ try fileManager.copyItem(at: sourceURL, to: destinationURL)
+ print("File copied from \(sourceURL.path) to \(destinationURL.path)")
+ } catch {
+ throw FileOperationError.unknownError("Failed to copy file: \(error.localizedDescription)")
+ }
+ }
+
+ /// Moves a file from a source URL to a destination URL.
+ ///
+ /// - Parameters:
+ /// - sourceURL: The URL of the file to move.
+ /// - destinationURL: The URL to move the file to.
+ /// - Throws: An error if the file does not exist or if the move operation fails.
+ static func moveFile(at sourceURL: URL, to destinationURL: URL) throws {
+ guard fileManager.fileExists(atPath: sourceURL.path) else {
+ throw FileOperationError.fileNotFound("Source file not found at \(sourceURL.path)")
+ }
+ do {
+ try fileManager.moveItem(at: sourceURL, to: destinationURL)
+ print("File moved from \(sourceURL.path) to \(destinationURL.path)")
+ } catch {
+ throw FileOperationError.unknownError("Failed to move file: \(error.localizedDescription)")
+ }
+ }
+
+ /// Compresses a file at a given URL to a destination URL using ZIPFoundation.
+ ///
+ /// - Parameters:
+ /// - fileURL: The URL of the file to compress.
+ /// - destinationURL: The URL where the ZIP archive should be created.
+ /// - Throws: An error if the file does not exist or if the compression fails.
+ static func compressFile(at fileURL: URL, to destinationURL: URL) throws {
+ guard fileManager.fileExists(atPath: fileURL.path) else {
+ throw FileOperationError.fileNotFound("File not found at \(fileURL.path)")
+ }
+ do {
+ try fileManager.zipItem(at: fileURL, to: destinationURL)
+ print("File compressed from \(fileURL.path) to \(destinationURL.path)")
+ } catch {
+ throw FileOperationError.unknownError("Failed to compress file: \(error.localizedDescription)")
+ }
+ }
+
+ /// Deletes a file at the specified URL.
+ ///
+ /// - Parameter fileURL: The URL of the file to delete.
+ /// - Throws: An error if the file does not exist or if the deletion fails.
+ static func deleteFile(at fileURL: URL) throws {
+ guard fileManager.fileExists(atPath: fileURL.path) else {
+ throw FileOperationError.fileNotFound("File not found at \(fileURL.path)")
+ }
+ do {
+ try fileManager.removeItem(at: fileURL)
+ print("File deleted at \(fileURL.path)")
+ } catch {
+ throw FileOperationError.unknownError("Failed to delete file: \(error.localizedDescription)")
+ }
+ }
+
+ /// Unzips a file at a given URL to a destination URL using ZIPFoundation.
+ ///
+ /// - Parameters:
+ /// - fileURL: The URL of the ZIP archive to extract.
+ /// - destinationURL: The URL where the contents of the archive should be extracted.
+ /// - Throws: An error if the file does not exist or if the extraction fails.
+ static func unzipFile(at fileURL: URL, to destinationURL: URL) throws {
+ guard fileManager.fileExists(atPath: fileURL.path) else {
+ throw FileOperationError.fileNotFound("File not found at \(fileURL.path)")
+ }
+ do {
+ let archive = try Archive(url: fileURL, accessMode: .read)
+ for entry in archive {
+ let destination = destinationURL.appendingPathComponent(entry.path)
+ if entry.type == .directory {
+ try fileManager.createDirectory(at: destination, withIntermediateDirectories: true, attributes: nil)
+ } else {
+ _ = try archive.extract(entry, to: destination)
+ }
+ }
+ print("File unzipped from \(fileURL.path) to \(destinationURL.path)")
+ } catch {
+ throw FileOperationError.unknownError("Failed to unzip file: \(error.localizedDescription)")
+ }
+ }
+
+ /// Presents a Hex Editor View Controller for editing the file.
+ ///
+ /// - Parameters:
+ /// - fileURL: The URL of the file to be edited.
+ /// - viewController: The view controller to present the Hex Editor from.
+ static func hexEditFile(at fileURL: URL, in viewController: UIViewController) {
+ guard fileManager.fileExists(atPath: fileURL.path) else {
+ print("File not found at \(fileURL.path)")
+ return
+ }
+
+ let hexEditorViewController = HexEditorViewController(fileURL: fileURL)
+ viewController.present(hexEditorViewController, animated: true, completion: nil)
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/FileTableViewCell.swift b/iOS/Views/Home/FileTableViewCell.swift
new file mode 100644
index 00000000..32a3b5ba
--- /dev/null
+++ b/iOS/Views/Home/FileTableViewCell.swift
@@ -0,0 +1,78 @@
+import UIKit
+
+class FileTableViewCell: UITableViewCell {
+ let fileIconImageView = UIImageView()
+ let fileNameLabel = UILabel()
+ let fileSizeLabel = UILabel()
+ let fileDateLabel = UILabel()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ setupUI()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func setupUI() {
+ // Configure and add subviews to contentView
+ contentView.addSubview(fileIconImageView)
+ contentView.addSubview(fileNameLabel)
+ contentView.addSubview(fileSizeLabel)
+ contentView.addSubview(fileDateLabel)
+
+ // Setup layout constraints
+ fileIconImageView.translatesAutoresizingMaskIntoConstraints = false
+ fileNameLabel.translatesAutoresizingMaskIntoConstraints = false
+ fileSizeLabel.translatesAutoresizingMaskIntoConstraints = false
+ fileDateLabel.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ fileIconImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
+ fileIconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
+ fileIconImageView.widthAnchor.constraint(equalToConstant: 40),
+ fileIconImageView.heightAnchor.constraint(equalToConstant: 40),
+
+ fileNameLabel.leadingAnchor.constraint(equalTo: fileIconImageView.trailingAnchor, constant: 16),
+ fileNameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
+ fileNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8),
+
+ fileSizeLabel.leadingAnchor.constraint(equalTo: fileNameLabel.leadingAnchor),
+ fileSizeLabel.topAnchor.constraint(equalTo: fileNameLabel.bottomAnchor, constant: 4),
+
+ fileDateLabel.leadingAnchor.constraint(equalTo: fileNameLabel.leadingAnchor),
+ fileDateLabel.topAnchor.constraint(equalTo: fileSizeLabel.bottomAnchor, constant: 4),
+ fileDateLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
+ ])
+ }
+
+ func configure(with file: File) {
+ // Configure the cell with file data
+ fileNameLabel.text = file.name
+ fileSizeLabel.text = "\(file.size) bytes"
+ fileDateLabel.text = "\(file.date)"
+ // Set an appropriate icon for the file type
+ fileIconImageView.image = UIImage(systemName: "doc.text")
+ }
+}
+
+// File model class to hold file information
+class File {
+ let url: URL
+ var name: String {
+ return url.lastPathComponent
+ }
+ var size: UInt64 {
+ let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
+ return attributes?[.size] as? UInt64 ?? 0
+ }
+ var date: Date {
+ let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
+ return attributes?[.modificationDate] as? Date ?? Date.distantPast
+ }
+
+ init(url: URL) {
+ self.url = url
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/HexEditorViewController.swift b/iOS/Views/Home/HexEditorViewController.swift
new file mode 100644
index 00000000..bd5f3303
--- /dev/null
+++ b/iOS/Views/Home/HexEditorViewController.swift
@@ -0,0 +1,184 @@
+import UIKit
+
+class HexEditorViewController: UIViewController, UITextViewDelegate {
+ private let fileURL: URL
+ private var textView: UITextView!
+ private var toolbar: UIToolbar!
+ private var hasUnsavedChanges = false
+ private var autoSaveTimer: Timer?
+
+ init(fileURL: URL) {
+ self.fileURL = fileURL
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupUI()
+ loadFileContent()
+ startAutoSaveTimer()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ if hasUnsavedChanges {
+ promptSaveChanges()
+ } else {
+ navigationController?.popViewController(animated: true)
+ }
+ stopAutoSaveTimer()
+ }
+
+ private func setupUI() {
+ view.backgroundColor = .systemBackground
+
+ // Setup text view
+ textView = UITextView()
+ textView.translatesAutoresizingMaskIntoConstraints = false
+ textView.font = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular)
+ textView.delegate = self
+ view.addSubview(textView)
+
+ // Setup toolbar
+ toolbar = UIToolbar()
+ toolbar.translatesAutoresizingMaskIntoConstraints = false
+ let saveButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveChanges))
+ let copyButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(copyContent))
+ let findReplaceButton = UIBarButtonItem(title: "Find/Replace", style: .plain, target: self, action: #selector(promptFindReplace))
+ let undoButton = UIBarButtonItem(barButtonSystemItem: .undo, target: self, action: #selector(undoAction))
+ let redoButton = UIBarButtonItem(barButtonSystemItem: .redo, target: self, action: #selector(redoAction))
+ toolbar.items = [saveButton, copyButton, findReplaceButton, undoButton, redoButton, UIBarButtonItem.flexibleSpace()]
+ view.addSubview(toolbar)
+
+ // Setup constraints
+ NSLayoutConstraint.activate([
+ textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ textView.bottomAnchor.constraint(equalTo: toolbar.topAnchor),
+
+ toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
+ ])
+ }
+
+ private func loadFileContent() {
+ DispatchQueue.global(qos: .userInitiated).async {
+ do {
+ let data = try Data(contentsOf: self.fileURL)
+ let hexString = data.map { String(format: "%02x", $0) }.joined(separator: " ")
+ DispatchQueue.main.async {
+ self.textView.text = hexString
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Error", message: "Failed to load file content: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ @objc private func saveChanges() {
+ guard let hexString = textView.text else { return }
+ let hexValues = hexString.split(separator: " ").map(String.init)
+ var data = Data()
+ for hex in hexValues {
+ if let byte = UInt8(hex, radix: 16) {
+ data.append(byte)
+ } else {
+ presentAlert(title: "Error", message: "Invalid hex value: \(hex)")
+ return
+ }
+ }
+ DispatchQueue.global(qos: .userInitiated).async {
+ do {
+ try data.write(to: self.fileURL)
+ self.hasUnsavedChanges = false
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Success", message: "File saved successfully.")
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Error", message: "Failed to save file: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ @objc private func copyContent() {
+ UIPasteboard.general.string = textView.text
+ presentAlert(title: "Copied", message: "Content copied to clipboard.")
+ }
+
+ @objc private func undoAction() {
+ textView.undoManager?.undo()
+ }
+
+ @objc private func redoAction() {
+ textView.undoManager?.redo()
+ }
+
+ @objc private func promptFindReplace() {
+ let alert = UIAlertController(title: "Find and Replace", message: "Enter text to find and replace:", preferredStyle: .alert)
+ alert.addTextField { textField in
+ textField.placeholder = "Find"
+ }
+ alert.addTextField { textField in
+ textField.placeholder = "Replace"
+ }
+ alert.addAction(UIAlertAction(title: "Replace", style: .default, handler: { [weak self] _ in
+ guard let findText = alert.textFields?[0].text, let replaceText = alert.textFields?[1].text else { return }
+ self?.findAndReplace(findText: findText, replaceText: replaceText)
+ }))
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ private func findAndReplace(findText: String, replaceText: String) {
+ textView.text = textView.text.replacingOccurrences(of: findText, with: replaceText)
+ hasUnsavedChanges = true
+ }
+
+ private func promptSaveChanges() {
+ let alert = UIAlertController(title: "Unsaved Changes", message: "You have unsaved changes. Do you want to save them before leaving?", preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "Save", style: .default, handler: { [weak self] _ in
+ self?.saveChanges()
+ self?.navigationController?.popViewController(animated: true)
+ }))
+ alert.addAction(UIAlertAction(title: "Discard", style: .destructive, handler: { [weak self] _ in
+ self?.navigationController?.popViewController(animated: true)
+ }))
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ private func startAutoSaveTimer() {
+ autoSaveTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(autoSaveChanges), userInfo: nil, repeats: true)
+ }
+
+ private func stopAutoSaveTimer() {
+ autoSaveTimer?.invalidate()
+ autoSaveTimer = nil
+ }
+
+ @objc private func autoSaveChanges() {
+ if hasUnsavedChanges {
+ saveChanges()
+ }
+ }
+
+ func textViewDidChange(_ textView: UITextView) {
+ hasUnsavedChanges = true
+ }
+
+ private func presentAlert(title: String, message: String) {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/HomeViewController.swift b/iOS/Views/Home/HomeViewController.swift
new file mode 100644
index 00000000..b8496f31
--- /dev/null
+++ b/iOS/Views/Home/HomeViewController.swift
@@ -0,0 +1,323 @@
+import UIKit
+import ZIPFoundation
+import Foundation
+
+class HomeViewController: UIViewController, UISearchResultsUpdating, UITableViewDragDelegate, UITableViewDropDelegate, UITableViewDelegate, UITableViewDataSource {
+
+ // MARK: - Properties
+ private var fileList: [String] = []
+ private var filteredFileList: [String] = []
+ private let fileManager = FileManager.default
+ private let searchController = UISearchController(searchResultsController: nil)
+ private var sortOrder: SortOrder = .name
+ let fileHandlers = HomeViewFileHandlers()
+ let utilities = HomeViewUtilities()
+
+ var documentsDirectory: URL {
+ let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("files")
+ createFilesDirectoryIfNeeded(at: directory)
+ return directory
+ }
+
+ enum SortOrder {
+ case name, date, size
+ }
+
+ let fileListTableView = UITableView()
+ let activityIndicator = UIActivityIndicatorView(style: .large)
+
+ // MARK: - Lifecycle
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupUI()
+ setupActivityIndicator()
+ loadFiles()
+ configureTableView()
+ }
+
+ // MARK: - UI Setup
+ private func setupUI() {
+ view.backgroundColor = .systemBackground
+
+ let navItem = UINavigationItem(title: "Files")
+ let menuButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(showMenu))
+ let uploadButton = UIBarButtonItem(image: UIImage(systemName: "square.and.arrow.up"), style: .plain, target: self, action: #selector(importFile))
+ let addButton = UIBarButtonItem(image: UIImage(systemName: "folder.badge.plus"), style: .plain, target: self, action: #selector(addDirectory)) // Add Directory button
+
+ navItem.rightBarButtonItems = [menuButton, uploadButton, addButton]
+ navigationController?.navigationBar.setItems([navItem], animated: false)
+
+ searchController.searchResultsUpdater = self
+ searchController.obscuresBackgroundDuringPresentation = false
+ searchController.searchBar.placeholder = "Search Files"
+ navigationItem.searchController = searchController
+ definesPresentationContext = true
+
+ view.addSubview(fileListTableView)
+ fileListTableView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ fileListTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ fileListTableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
+ fileListTableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
+ fileListTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
+ ])
+ }
+
+ private func setupActivityIndicator() {
+ view.addSubview(activityIndicator)
+ activityIndicator.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+ }
+
+ private func configureTableView() {
+ fileListTableView.delegate = self
+ fileListTableView.dataSource = self
+ fileListTableView.dragDelegate = self
+ fileListTableView.dropDelegate = self
+ fileListTableView.register(UITableViewCell.self, forCellReuseIdentifier: "fileCell")
+ }
+
+ private func createFilesDirectoryIfNeeded(at directory: URL) {
+ if !fileManager.fileExists(atPath: directory.path) {
+ do {
+ try fileManager.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
+ } catch {
+ print("Error creating directory: \(error)")
+ }
+ }
+ }
+
+ // MARK: - File Operations
+ func loadFiles() {
+ activityIndicator.startAnimating()
+ DispatchQueue.global().async { [weak self] in
+ guard let self = self else { return }
+ do {
+ let files = try self.fileManager.contentsOfDirectory(atPath: self.documentsDirectory.path)
+ DispatchQueue.main.async {
+ self.fileList = files
+ self.sortFiles()
+ self.fileListTableView.reloadData()
+ self.activityIndicator.stopAnimating()
+ }
+ } catch {
+ print("Error loading files: \(error)")
+ DispatchQueue.main.async {
+ self.activityIndicator.stopAnimating()
+ }
+ }
+ }
+ }
+
+ @objc private func importFile() {
+ let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.zip, .item])
+ documentPicker.delegate = self
+ documentPicker.allowsMultipleSelection = false
+ present(documentPicker, animated: true, completion: nil)
+ }
+
+ func handleImportedFile(url: URL) {
+ let destinationURL = documentsDirectory.appendingPathComponent(url.lastPathComponent)
+
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ guard let self = self else { return }
+
+ do {
+ if url.startAccessingSecurityScopedResource() {
+ if url.pathExtension == "zip" {
+ let progressHandler: Progress? = nil // Adjust to match expected type
+ try self.fileManager.unzipItem(at: url, to: destinationURL, progress: progressHandler)
+ } else {
+ try self.fileManager.copyItem(at: url, to: destinationURL)
+ }
+ url.stopAccessingSecurityScopedResource()
+
+ DispatchQueue.main.async {
+ self.loadFiles()
+ }
+ }
+ } catch {
+ print("Error handling file: \(error)")
+ }
+ }
+ }
+
+ func deleteFile(at index: Int) {
+ let fileToDelete = fileList[index]
+ let fileURL = documentsDirectory.appendingPathComponent(fileToDelete)
+
+ do {
+ try fileManager.removeItem(at: fileURL)
+ fileList.remove(at: index)
+ fileListTableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade)
+ } catch {
+ print("Error deleting file: \(error)")
+ }
+ }
+
+ func sortFiles() {
+ switch sortOrder {
+ case .name:
+ fileList.sort { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
+ case .date:
+ // Need to implement file date retrieval and sorting
+ break
+ case .size:
+ // Need to implement file size retrieval and sorting
+ break
+ }
+ }
+
+ // MARK: - UI Actions
+ @objc private func showMenu() {
+ let alertController = UIAlertController(title: "Sort By", message: nil, preferredStyle: .actionSheet)
+
+ let sortByNameAction = UIAlertAction(title: "Name", style: .default) { [weak self] _ in
+ self?.sortOrder = .name
+ self?.sortFiles()
+ self?.fileListTableView.reloadData()
+ }
+ alertController.addAction(sortByNameAction)
+
+ let sortByDateAction = UIAlertAction(title: "Date", style: .default) { [weak self] _ in
+ self?.sortOrder = .date
+ self?.sortFiles()
+ self?.fileListTableView.reloadData()
+ }
+ alertController.addAction(sortByDateAction)
+
+ let sortBySizeAction = UIAlertAction(title: "Size", style: .default) { [weak self] _ in
+ self?.sortOrder = .size
+ self?.sortFiles()
+ self?.fileListTableView.reloadData()
+ }
+ alertController.addAction(sortBySizeAction)
+
+ let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
+ alertController.addAction(cancelAction)
+
+ present(alertController, animated: true, completion: nil)
+ }
+
+ // MARK: - UISearchResultsUpdating
+ func updateSearchResults(for searchController: UISearchController) {
+ guard let searchText = searchController.searchBar.text else { return }
+ filteredFileList = fileList.filter { $0.localizedCaseInsensitiveContains(searchText) }
+ fileListTableView.reloadData()
+ }
+
+ // MARK: - UITableViewDragDelegate
+ func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
+ let item = self.fileList[indexPath.row]
+ let itemProvider = NSItemProvider(object: item as NSString)
+ let dragItem = UIDragItem(itemProvider: itemProvider)
+ return [dragItem]
+ }
+
+ // MARK: - UITableViewDropDelegate
+ func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
+ coordinator.session.loadObjects(ofClass: NSString.self) { items in
+ guard let string = items.first as? String else { return }
+ self.fileList.append(string as String)
+ self.fileListTableView.reloadData()
+ }
+ }
+
+ func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
+ return true
+ }
+
+ // MARK: - UITableViewDelegate, UITableViewDataSource
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ if searchController.isActive && searchController.searchBar.text != "" {
+ return filteredFileList.count
+ }
+ return fileList.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "fileCell", for: indexPath)
+ let fileName: String
+ if searchController.isActive && searchController.searchBar.text != "" {
+ fileName = filteredFileList[indexPath.row]
+ } else {
+ fileName = fileList[indexPath.row]
+ }
+ cell.textLabel?.text = fileName
+ return cell
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let fileName: String
+ if searchController.isActive && searchController.searchBar.text != "" {
+ fileName = filteredFileList[indexPath.row]
+ } else {
+ fileName = fileList[indexPath.row]
+ }
+
+ let fileURL = documentsDirectory.appendingPathComponent(fileName)
+
+ let activityViewController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
+ present(activityViewController, animated: true, completion: nil)
+ }
+
+ @objc private func addDirectory() {
+ let alertController = UIAlertController(title: "Add Directory", message: "Enter the name of the new directory", preferredStyle: .alert)
+ alertController.addTextField { (textField) in
+ textField.placeholder = "Directory Name"
+ }
+
+ let createAction = UIAlertAction(title: "Create", style: .default) { [weak self] _ in
+ guard let textField = alertController.textFields?.first, let directoryName = textField.text, !directoryName.isEmpty else { return }
+
+ let newDirectoryURL = self?.documentsDirectory.appendingPathComponent(directoryName)
+
+ do {
+ try self?.fileManager.createDirectory(at: newDirectoryURL!, withIntermediateDirectories: false, attributes: nil)
+ self?.loadFiles()
+ } catch {
+ print("Error creating directory: \(error)")
+ }
+ }
+ alertController.addAction(createAction)
+
+ let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
+ alertController.addAction(cancelAction)
+
+ present(alertController, animated: true, completion: nil)
+ }
+}
+
+// MARK: - Extensions
+extension HomeViewController: UIDocumentPickerDelegate {
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
+ guard let selectedFileURL = urls.first else {
+ return
+ }
+
+ handleImportedFile(url: selectedFileURL)
+ }
+}
+
+extension FileManager {
+ func fileSize(at path: String) -> UInt64? {
+ do {
+ let attr = try attributesOfItem(atPath: path)
+ return attr[.size] as? UInt64
+ } catch {
+ return nil
+ }
+ }
+
+ func creationDate(at path: String) -> Date? {
+ do {
+ let attr = try attributesOfItem(atPath: path)
+ return attr[.creationDate] as? Date
+ } catch {
+ return nil
+ }
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/HomeViewFileHandlers.swift b/iOS/Views/Home/HomeViewFileHandlers.swift
new file mode 100644
index 00000000..e00d187b
--- /dev/null
+++ b/iOS/Views/Home/HomeViewFileHandlers.swift
@@ -0,0 +1,130 @@
+import UIKit
+import ZIPFoundation
+import os.log
+import Foundation
+
+protocol FileHandlingDelegate: AnyObject {
+ var documentsDirectory: URL { get }
+ var activityIndicator: UIActivityIndicatorView { get }
+ func loadFiles()
+ func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?)
+}
+
+class HomeViewFileHandlers {
+ private let fileManager = FileManager.default
+ private let utilities = HomeViewUtilities()
+
+ func uploadFile(viewController: FileHandlingDelegate) {
+ let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.data], asCopy: true)
+ documentPicker.delegate = viewController as? UIDocumentPickerDelegate
+ documentPicker.modalPresentationStyle = .formSheet
+ viewController.present(documentPicker, animated: true, completion: nil)
+ }
+
+ func importFile(viewController: FileHandlingDelegate) {
+ let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [.data], asCopy: true)
+ documentPicker.delegate = viewController as? UIDocumentPickerDelegate
+ documentPicker.modalPresentationStyle = .formSheet
+ viewController.present(documentPicker, animated: true, completion: nil)
+ }
+
+ func createNewFolder(viewController: FileHandlingDelegate, folderName: String, completion: @escaping (Result) -> Void) {
+ let folderURL = viewController.documentsDirectory.appendingPathComponent(folderName)
+ do {
+ try fileManager.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil)
+ viewController.loadFiles()
+ completion(.success(folderURL))
+ } catch {
+ utilities.handleError(in: viewController as! UIViewController, error: error, withTitle: "Creating Folder")
+ completion(.failure(error))
+ }
+ }
+
+ func createNewFile(viewController: FileHandlingDelegate, fileName: String, completion: @escaping (Result) -> Void) {
+ let fileURL = viewController.documentsDirectory.appendingPathComponent(fileName)
+ if !fileManager.fileExists(atPath: fileURL.path) {
+ fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil)
+ viewController.loadFiles()
+ completion(.success(fileURL))
+ } else {
+ let error = NSError(domain: "FileExists", code: 1, userInfo: [NSLocalizedDescriptionKey: "File already exists"])
+ utilities.handleError(in: viewController as! UIViewController, error: error, withTitle: "Creating File")
+ completion(.failure(error))
+ }
+ }
+
+ func renameFile(viewController: FileHandlingDelegate, fileURL: URL, newName: String, completion: @escaping (Result) -> Void) {
+ let destinationURL = fileURL.deletingLastPathComponent().appendingPathComponent(newName)
+ viewController.activityIndicator.startAnimating()
+ let workItem = DispatchWorkItem {
+ do {
+ try self.fileManager.moveItem(at: fileURL, to: destinationURL)
+ DispatchQueue.main.async {
+ viewController.activityIndicator.stopAnimating()
+ viewController.loadFiles()
+ completion(.success(destinationURL))
+ }
+ } catch {
+ DispatchQueue.main.async {
+ viewController.activityIndicator.stopAnimating()
+ self.utilities.handleError(in: viewController as! UIViewController, error: error, withTitle: "Renaming File")
+ completion(.failure(error))
+ }
+ }
+ }
+ DispatchQueue.global().async(execute: workItem)
+ }
+
+ func deleteFile(viewController: FileHandlingDelegate, fileURL: URL, completion: @escaping (Result) -> Void) {
+ viewController.activityIndicator.startAnimating()
+ let workItem = DispatchWorkItem {
+ do {
+ try self.fileManager.removeItem(at: fileURL)
+ DispatchQueue.main.async {
+ viewController.activityIndicator.stopAnimating()
+ viewController.loadFiles()
+ completion(.success(()))
+ }
+ } catch {
+ DispatchQueue.main.async {
+ viewController.activityIndicator.stopAnimating()
+ self.utilities.handleError(in: viewController as! UIViewController, error: error, withTitle: "Deleting File")
+ completion(.failure(error))
+ }
+ }
+ }
+ DispatchQueue.global().async(execute: workItem)
+ }
+
+ func unzipFile(viewController: FileHandlingDelegate, fileURL: URL, destinationName: String, progressHandler: ((Double) -> Void)? = nil, completion: @escaping (Result) -> Void) {
+ let destinationURL = fileURL.deletingLastPathComponent().appendingPathComponent(destinationName)
+ viewController.activityIndicator.startAnimating()
+ let workItem = DispatchWorkItem {
+ do {
+ let progress = Progress(totalUnitCount: 100)
+ progress.cancellationHandler = {
+ print("Unzip cancelled")
+ }
+ try self.fileManager.unzipItem(at: fileURL, to: destinationURL, progress: progress)
+ progressHandler?(1.0)
+ DispatchQueue.main.async {
+ viewController.activityIndicator.stopAnimating()
+ viewController.loadFiles()
+ completion(.success(destinationURL))
+ }
+ } catch {
+ DispatchQueue.main.async {
+ viewController.activityIndicator.stopAnimating()
+ self.utilities.handleError(in: viewController as! UIViewController, error: error, withTitle: "Unzipping File")
+ completion(.failure(error))
+ }
+ }
+ }
+ DispatchQueue.global().async(execute: workItem)
+ }
+
+ func shareFile(viewController: UIViewController, fileURL: URL) {
+ let activityController = UIActivityViewController(activityItems: [fileURL], applicationActivities: nil)
+ viewController.present(activityController, animated: true, completion: nil)
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/HomeViewTableHandlers.swift b/iOS/Views/Home/HomeViewTableHandlers.swift
new file mode 100644
index 00000000..0ce88f68
--- /dev/null
+++ b/iOS/Views/Home/HomeViewTableHandlers.swift
@@ -0,0 +1,25 @@
+import UIKit
+
+
+ extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return searchController.isActive ? filteredFileList.count : fileList.count
+ }
+
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "FileCell", for: indexPath) as! FileTableViewCell
+ let fileName = searchController.isActive ? filteredFileList[indexPath.row] : fileList[indexPath.row]
+ let fileURL = documentsDirectory.appendingPathComponent(fileName)
+ let file = File(url: fileURL)
+ cell.configure(with: file)
+ return cell
+ }
+
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let fileName = searchController.isActive ? filteredFileList[indexPath.row] : fileList[indexPath.row]
+ let fileURL = documentsDirectory.appendingPathComponent(fileName)
+ showFileOptions(for: fileURL)
+ }
+ }
\ No newline at end of file
diff --git a/iOS/Views/Home/HomeViewUI.swift b/iOS/Views/Home/HomeViewUI.swift
new file mode 100644
index 00000000..3d3a850c
--- /dev/null
+++ b/iOS/Views/Home/HomeViewUI.swift
@@ -0,0 +1,39 @@
+import UIKit
+
+class HomeViewUI {
+ static let navigationBar: UINavigationBar = {
+ let navBar = UINavigationBar()
+ navBar.translatesAutoresizingMaskIntoConstraints = false
+ navBar.barTintColor = .systemBlue
+ navBar.titleTextAttributes = [.foregroundColor: UIColor.white]
+ return navBar
+ }()
+
+ static let fileListTableView: UITableView = {
+ let tableView = UITableView()
+ tableView.translatesAutoresizingMaskIntoConstraints = false
+ tableView.separatorStyle = .singleLine
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.estimatedRowHeight = 44
+ return tableView
+ }()
+
+ static let activityIndicator: UIActivityIndicatorView = {
+ let indicator = UIActivityIndicatorView(style: .large)
+ indicator.translatesAutoresizingMaskIntoConstraints = false
+ indicator.hidesWhenStopped = true
+ indicator.color = .systemBlue
+ return indicator
+ }()
+
+ static let uploadButton: UIButton = {
+ let button = UIButton(type: .system)
+ button.setTitle("Upload File", for: .normal)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ button.setTitleColor(.white, for: .normal)
+ button.backgroundColor = .systemBlue
+ button.layer.cornerRadius = 10
+ button.clipsToBounds = true
+ return button
+ }()
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/HomeViewUtilities.swift b/iOS/Views/Home/HomeViewUtilities.swift
new file mode 100644
index 00000000..6b4487ff
--- /dev/null
+++ b/iOS/Views/Home/HomeViewUtilities.swift
@@ -0,0 +1,188 @@
+import UIKit
+import os.log // Import os.log for logging
+
+// MARK: - Error Handling Enhancements
+
+/// Custom error hierarchy for file operations.
+enum FileAppError: Error {
+ case fileNotFound(String) // File not found at path
+ case fileAlreadyExists(String) // File already exists
+ case invalidFileName(String) // File name contains invalid characters
+ case invalidFileType(String) // File type not supported
+ case permissionDenied(String) // Permission denied for operation
+ case directoryCreationFailed(String) // Directory creation failed
+ case fileCreationFailed(String) // File creation failed
+ case fileRenameFailed(String, String) // Renaming file failed (old, new)
+ case fileDeleteFailed(String) // Deleting file failed
+ case fileMoveFailed(String, String) // Moving file failed (old, new)
+ case fileUnzipFailed(String, String, Error?) // Unzipping failed (file, dest, error)
+ case fileZipFailed(String, String, Error?) // Zipping failed (file, dest, error)
+ case dylibListingFailed(String, Error?) // Listing dylibs failed (path, error)
+ case unknown(Error) // An unexpected error occurred
+}
+
+// MARK: - Alert Configuration
+
+/// Structure to encapsulate alert configurations.
+struct AlertConfig {
+ let title: String?
+ let message: String?
+ let style: UIAlertController.Style
+ let actions: [AlertActionConfig]
+ let preferredAction: Int? // Index of preferred action
+ let completionHandler: (() -> Void)?
+}
+
+struct AlertActionConfig {
+ let title: String?
+ let style: UIAlertAction.Style
+ let handler: (() -> Void)?
+}
+
+// MARK: - HomeViewUtilities Class
+
+class HomeViewUtilities {
+
+ private let logger: Logger // Inject a logger dependency
+
+ init(logger: Logger = Logger(subsystem: "com.example.FileApp", category: "Utilities")) {
+ self.logger = logger
+ }
+
+ // MARK: - Error Handling
+
+ /// Handles and presents an error to the user.
+ ///
+ /// - Parameters:
+ /// - viewController: The view controller to present the alert in.
+ /// - error: The error to handle.
+ /// - title: The title for the error alert.
+ func handleError(in viewController: UIViewController, error: Error, withTitle title: String) {
+ var message: String
+
+ if let fileError = error as? FileAppError {
+ switch fileError {
+ case .fileNotFound(let fileName):
+ message = "File not found: \(fileName). Please check the file name and try again."
+ logger.info("File not found: \(fileName).")
+ case .fileAlreadyExists(let fileName):
+ message = "A file with the name \(fileName) already exists. Please choose a different name."
+ logger.info("File already exists: \(fileName).")
+ case .unknown(let underlyingError):
+ message = "An unknown error occurred: \(underlyingError.localizedDescription)"
+ logger.error("Unknown error: \(underlyingError.localizedDescription)")
+ default:
+ message = error.localizedDescription
+ }
+ } else {
+ message = error.localizedDescription
+ logger.error("Unexpected error: \(error.localizedDescription)")
+ }
+
+ // Present alert on main thread
+ DispatchQueue.main.async {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
+ viewController.present(alert, animated: true, completion: nil)
+ }
+ }
+
+ // MARK: - Alert Presentation
+
+ /// Presents a basic alert using the provided configuration.
+ ///
+ /// - Parameters:
+ /// - config: The alert configuration.
+ /// - viewController: The view controller to present the alert in.
+ func showAlert(config: AlertConfig, in viewController: UIViewController) {
+ let alert = UIAlertController(title: config.title, message: config.message, preferredStyle: config.style)
+
+ for (index, actionConfig) in config.actions.enumerated() {
+ let action = UIAlertAction(title: actionConfig.title, style: actionConfig.style) { _ in
+ actionConfig.handler?()
+ }
+ alert.addAction(action)
+ if let preferredIndex = config.preferredAction, preferredIndex == index {
+ alert.preferredAction = alert.actions[preferredIndex]
+ }
+ }
+
+ DispatchQueue.main.async {
+ viewController.present(alert, animated: true, completion: config.completionHandler)
+ }
+ }
+
+ /// Presents a confirmation alert with "OK" and "Cancel" actions.
+ ///
+ /// - Parameters:
+ /// - title: The title for the alert.
+ /// - message: The message for the alert.
+ /// - okHandler: Handler to be executed when the "OK" action is tapped.
+ /// - cancelHandler: Handler to be executed when the "Cancel" action is tapped.
+ /// - viewController: The view controller to present the alert in.
+ func showConfirmationAlert(title: String?, message: String?, okHandler: (() -> Void)?, cancelHandler: (() -> Void)?, in viewController: UIViewController) {
+ let okAction = AlertActionConfig(title: "OK", style: .default, handler: okHandler)
+ let cancelAction = AlertActionConfig(title: "Cancel", style: .cancel, handler: cancelHandler)
+ let config = AlertConfig(title: title, message: message, style: .alert, actions: [okAction, cancelAction], preferredAction: nil, completionHandler: nil)
+ showAlert(config: config, in: viewController)
+ }
+
+ /// Presents an alert with a text field for user input.
+ ///
+ /// - Parameters:
+ /// - title: The title for the alert.
+ /// - message: The message for the alert.
+ /// - textFieldHandler: Handler to configure the text field.
+ /// - okHandler: Handler to be executed when the "OK" action is tapped, with the text field's text.
+ /// - cancelHandler: Handler to be executed when the "Cancel" action is tapped.
+ /// - viewController: The view controller to present the alert in.
+ func showInputAlert(title: String?, message: String?, textFieldHandler: ((UITextField) -> Void)?, okHandler: ((String?) -> Void)?, cancelHandler: (() -> Void)?, in viewController: UIViewController) {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addTextField(configurationHandler: textFieldHandler)
+
+ let okAction = AlertActionConfig(title: "OK", style: .default) {
+ let textField = alert.textFields?.first
+ okHandler?(textField?.text)
+ }
+ let cancelAction = AlertActionConfig(title: "Cancel", style: .cancel, handler: cancelHandler)
+ let config = AlertConfig(title: title, message: message, style: .alert, actions: [okAction, cancelAction], preferredAction: nil, completionHandler: nil)
+
+ for actionConfig in config.actions {
+ let action = UIAlertAction(title: actionConfig.title, style: actionConfig.style) { _ in
+ actionConfig.handler?()
+ }
+ alert.addAction(action)
+ }
+
+ DispatchQueue.main.async {
+ viewController.present(alert, animated: true, completion: config.completionHandler)
+ }
+ }
+
+ // MARK: - Haptic Feedback
+
+ /// Generates haptic feedback using UIImpactFeedbackGenerator.
+ ///
+ /// - Parameter style: The style of the impact feedback.
+ func generateHapticFeedback(style: UIImpactFeedbackGenerator.FeedbackStyle) {
+ let generator = UIImpactFeedbackGenerator(style: style)
+ generator.prepare()
+ generator.impactOccurred()
+ }
+
+ /// Generates haptic feedback using UINotificationFeedbackGenerator.
+ ///
+ /// - Parameter type: The type of the notification feedback.
+ func generateNotificationFeedback(type: UINotificationFeedbackGenerator.FeedbackType) {
+ let generator = UINotificationFeedbackGenerator()
+ generator.prepare()
+ generator.notificationOccurred(type)
+ }
+
+ /// Generates haptic feedback using UISelectionFeedbackGenerator.
+ func generateSelectionFeedback() {
+ let generator = UISelectionFeedbackGenerator()
+ generator.prepare()
+ generator.selectionChanged()
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/PlistEditorViewController.swift b/iOS/Views/Home/PlistEditorViewController.swift
new file mode 100644
index 00000000..44547f35
--- /dev/null
+++ b/iOS/Views/Home/PlistEditorViewController.swift
@@ -0,0 +1,173 @@
+import UIKit
+
+class PlistEditorViewController: UIViewController, UITextViewDelegate {
+ private var fileURL: URL
+ private var textView: UITextView!
+ private var toolbar: UIToolbar!
+ private var hasUnsavedChanges = false
+ private var autoSaveTimer: Timer?
+
+ init(fileURL: URL) {
+ self.fileURL = fileURL
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupUI()
+ loadFileContent()
+ startAutoSaveTimer()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ if hasUnsavedChanges {
+ promptSaveChanges()
+ } else {
+ navigationController?.popViewController(animated: true)
+ }
+ stopAutoSaveTimer()
+ }
+
+ private func setupUI() {
+ view.backgroundColor = .systemBackground
+
+ // Setup text view
+ textView = UITextView()
+ textView.translatesAutoresizingMaskIntoConstraints = false
+ textView.font = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular)
+ textView.delegate = self
+ view.addSubview(textView)
+
+ // Setup toolbar
+ toolbar = UIToolbar()
+ toolbar.translatesAutoresizingMaskIntoConstraints = false
+ let saveButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveChanges))
+ let copyButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(copyContent))
+ let findReplaceButton = UIBarButtonItem(title: "Find/Replace", style: .plain, target: self, action: #selector(promptFindReplace))
+ let undoButton = UIBarButtonItem(barButtonSystemItem: .undo, target: self, action: #selector(undoAction))
+ let redoButton = UIBarButtonItem(barButtonSystemItem: .redo, target: self, action: #selector(redoAction))
+ toolbar.items = [saveButton, copyButton, findReplaceButton, undoButton, redoButton, UIBarButtonItem.flexibleSpace()]
+ view.addSubview(toolbar)
+
+ // Setup constraints
+ NSLayoutConstraint.activate([
+ textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ textView.bottomAnchor.constraint(equalTo: toolbar.topAnchor),
+
+ toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
+ ])
+ }
+
+ private func loadFileContent() {
+ DispatchQueue.global(qos: .userInitiated).async {
+ do {
+ let fileContent = try String(contentsOf: self.fileURL)
+ DispatchQueue.main.async {
+ self.textView.text = fileContent
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Error", message: "Failed to load file content: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ @objc private func saveChanges() {
+ guard let newText = textView.text else { return }
+ DispatchQueue.global(qos: .userInitiated).async {
+ do {
+ try newText.write(to: self.fileURL, atomically: true, encoding: .utf8)
+ self.hasUnsavedChanges = false
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Success", message: "File saved successfully.")
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Error", message: "Failed to save file: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ @objc private func copyContent() {
+ UIPasteboard.general.string = textView.text
+ presentAlert(title: "Copied", message: "Content copied to clipboard.")
+ }
+
+ @objc private func undoAction() {
+ textView.undoManager?.undo()
+ }
+
+ @objc private func redoAction() {
+ textView.undoManager?.redo()
+ }
+
+ @objc private func promptFindReplace() {
+ let alert = UIAlertController(title: "Find and Replace", message: "Enter text to find and replace:", preferredStyle: .alert)
+ alert.addTextField { textField in
+ textField.placeholder = "Find"
+ }
+ alert.addTextField { textField in
+ textField.placeholder = "Replace"
+ }
+ alert.addAction(UIAlertAction(title: "Replace", style: .default, handler: { [weak self] _ in
+ guard let findText = alert.textFields?[0].text, let replaceText = alert.textFields?[1].text else { return }
+ self?.findAndReplace(findText: findText, replaceText: replaceText)
+ }))
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ private func findAndReplace(findText: String, replaceText: String) {
+ textView.text = textView.text.replacingOccurrences(of: findText, with: replaceText)
+ hasUnsavedChanges = true
+ }
+
+ private func promptSaveChanges() {
+ let alert = UIAlertController(title: "Unsaved Changes", message: "You have unsaved changes. Do you want to save them before leaving?", preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "Save", style: .default, handler: { [weak self] _ in
+ self?.saveChanges()
+ self?.navigationController?.popViewController(animated: true)
+ }))
+ alert.addAction(UIAlertAction(title: "Discard", style: .destructive, handler: { [weak self] _ in
+ self?.navigationController?.popViewController(animated: true)
+ }))
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ private func startAutoSaveTimer() {
+ autoSaveTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(autoSaveChanges), userInfo: nil, repeats: true)
+ }
+
+ private func stopAutoSaveTimer() {
+ autoSaveTimer?.invalidate()
+ autoSaveTimer = nil
+ }
+
+ @objc private func autoSaveChanges() {
+ if hasUnsavedChanges {
+ saveChanges()
+ }
+ }
+
+ private func presentAlert(title: String, message: String) {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ func textViewDidChange(_ textView: UITextView) {
+ hasUnsavedChanges = true
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Home/TextEditorViewController.swift b/iOS/Views/Home/TextEditorViewController.swift
new file mode 100644
index 00000000..c759a544
--- /dev/null
+++ b/iOS/Views/Home/TextEditorViewController.swift
@@ -0,0 +1,173 @@
+import UIKit
+
+class TextEditorViewController: UIViewController, UITextViewDelegate {
+ private var fileURL: URL
+ private var textView: UITextView!
+ private var toolbar: UIToolbar!
+ private var hasUnsavedChanges = false
+ private var autoSaveTimer: Timer?
+
+ init(fileURL: URL) {
+ self.fileURL = fileURL
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupUI()
+ loadFileContent()
+ startAutoSaveTimer()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ super.viewWillDisappear(animated)
+ if hasUnsavedChanges {
+ promptSaveChanges()
+ } else {
+ navigationController?.popViewController(animated: true)
+ }
+ stopAutoSaveTimer()
+ }
+
+ private func setupUI() {
+ view.backgroundColor = .systemBackground
+
+ // Setup text view
+ textView = UITextView()
+ textView.translatesAutoresizingMaskIntoConstraints = false
+ textView.font = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular)
+ textView.delegate = self
+ view.addSubview(textView)
+
+ // Setup toolbar
+ toolbar = UIToolbar()
+ toolbar.translatesAutoresizingMaskIntoConstraints = false
+ let saveButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveChanges))
+ let copyButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(copyContent))
+ let findReplaceButton = UIBarButtonItem(title: "Find/Replace", style: .plain, target: self, action: #selector(promptFindReplace))
+ let undoButton = UIBarButtonItem(barButtonSystemItem: .undo, target: self, action: #selector(undoAction))
+ let redoButton = UIBarButtonItem(barButtonSystemItem: .redo, target: self, action: #selector(redoAction))
+ toolbar.items = [saveButton, copyButton, findReplaceButton, undoButton, redoButton, UIBarButtonItem.flexibleSpace()]
+ view.addSubview(toolbar)
+
+ // Setup constraints
+ NSLayoutConstraint.activate([
+ textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
+ textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ textView.bottomAnchor.constraint(equalTo: toolbar.topAnchor),
+
+ toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ toolbar.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
+ ])
+ }
+
+ private func loadFileContent() {
+ DispatchQueue.global(qos: .userInitiated).async {
+ do {
+ let fileContent = try String(contentsOf: self.fileURL)
+ DispatchQueue.main.async {
+ self.textView.text = fileContent
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Error", message: "Failed to load file content: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ @objc private func saveChanges() {
+ guard let newText = textView.text else { return }
+ DispatchQueue.global(qos: .userInitiated).async {
+ do {
+ try newText.write(to: self.fileURL, atomically: true, encoding: .utf8)
+ self.hasUnsavedChanges = false
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Success", message: "File saved successfully.")
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self.presentAlert(title: "Error", message: "Failed to save file: \(error.localizedDescription)")
+ }
+ }
+ }
+ }
+
+ @objc private func copyContent() {
+ UIPasteboard.general.string = textView.text
+ presentAlert(title: "Copied", message: "Content copied to clipboard.")
+ }
+
+ @objc private func undoAction() {
+ textView.undoManager?.undo()
+ }
+
+ @objc private func redoAction() {
+ textView.undoManager?.redo()
+ }
+
+ @objc private func promptFindReplace() {
+ let alert = UIAlertController(title: "Find and Replace", message: "Enter text to find and replace:", preferredStyle: .alert)
+ alert.addTextField { textField in
+ textField.placeholder = "Find"
+ }
+ alert.addTextField { textField in
+ textField.placeholder = "Replace"
+ }
+ alert.addAction(UIAlertAction(title: "Replace", style: .default, handler: { [weak self] _ in
+ guard let findText = alert.textFields?[0].text, let replaceText = alert.textFields?[1].text else { return }
+ self?.findAndReplace(findText: findText, replaceText: replaceText)
+ }))
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ private func findAndReplace(findText: String, replaceText: String) {
+ textView.text = textView.text.replacingOccurrences(of: findText, with: replaceText)
+ hasUnsavedChanges = true
+ }
+
+ private func promptSaveChanges() {
+ let alert = UIAlertController(title: "Unsaved Changes", message: "You have unsaved changes. Do you want to save them before leaving?", preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "Save", style: .default, handler: { [weak self] _ in
+ self?.saveChanges()
+ self?.navigationController?.popViewController(animated: true)
+ }))
+ alert.addAction(UIAlertAction(title: "Discard", style: .destructive, handler: { [weak self] _ in
+ self?.navigationController?.popViewController(animated: true)
+ }))
+ alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ private func startAutoSaveTimer() {
+ autoSaveTimer = Timer.scheduledTimer(timeInterval: 60, target: self, selector: #selector(autoSaveChanges), userInfo: nil, repeats: true)
+ }
+
+ private func stopAutoSaveTimer() {
+ autoSaveTimer?.invalidate()
+ autoSaveTimer = nil
+ }
+
+ @objc private func autoSaveChanges() {
+ if hasUnsavedChanges {
+ saveChanges()
+ }
+ }
+
+ private func presentAlert(title: String, message: String) {
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
+ present(alert, animated: true, completion: nil)
+ }
+
+ func textViewDidChange(_ textView: UITextView) {
+ hasUnsavedChanges = true
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Settings/About/AboutViewController.swift b/iOS/Views/Settings/About/AboutViewController.swift
index fc5e8168..9005fc2b 100644
--- a/iOS/Views/Settings/About/AboutViewController.swift
+++ b/iOS/Views/Settings/About/AboutViewController.swift
@@ -97,7 +97,7 @@ class AboutViewController: FRSTableViewController {
@objc func shareButtonTapped() {
- let shareText = "Feather - https://beacons.ai/bdgs"
+ let shareText = "Backdoor - https://beacons.ai/bdgs"
let activityViewController = UIActivityViewController(activityItems: [shareText], applicationActivities: nil)
if let popoverController = activityViewController.popoverPresentationController {
diff --git a/iOS/Views/Settings/AppIcon/IconsListViewController.swift b/iOS/Views/Settings/AppIcon/IconsListViewController.swift
index 67429ad6..48ba99bc 100644
--- a/iOS/Views/Settings/AppIcon/IconsListViewController.swift
+++ b/iOS/Views/Settings/AppIcon/IconsListViewController.swift
@@ -1,100 +1,92 @@
-//
-// IconsListViewController.swift
-// feather
-//
-// Created by samara on 8/11/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import UIKit
class IconsListViewController: UITableViewController {
-
- public class func altImage(_ name: String) -> UIImage {
- let path = Bundle.main.bundleURL.appendingPathComponent(name + "@2x.png")
- return UIImage(contentsOfFile: path.path) ?? UIImage()
- }
-
- var sections: [String: [AltIcon]] = [
- "Main": [
- AltIcon(displayName: "Backdoor", author: "BDG", key: nil, image: altImage("AppIcon60x60")),
- AltIcon(displayName: "macOS Backdoor", author: "BDG", key: "Mac", image: altImage("Mac")),
- AltIcon(displayName: "Evil Backdoor", author: "BDG", key: "Evil", image: altImage("Evil")),
- AltIcon(displayName: "Classic Backdoor", author: "BDG", key: "Early", image: altImage("Early"))
- ],
- "Wingio": [
- AltIcon(displayName: "Backdoor", author: "BDG", key: "Wing", image: altImage("Wing")),
- ]
- ]
-
- init() { super.init(style: .insetGrouped) }
- required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- setupViews()
- setupNavigation()
- }
-
- fileprivate func setupViews() {
- self.tableView.delegate = self
- self.tableView.dataSource = self
- self.tableView.rowHeight = 75
- }
-
- fileprivate func setupNavigation() {
- self.title = String.localized("SETTINGS_VIEW_CONTROLLER_CELL_APP_ICON")
- self.navigationItem.largeTitleDisplayMode = .never
- }
-
- private func sectionTitles() -> [String] {
- return Array(sections.keys).sorted()
- }
-
- private func icons(forSection section: Int) -> [AltIcon] {
- let title = sectionTitles()[section]
- return sections[title] ?? []
- }
+
+ public class func altImage(_ name: String) -> UIImage {
+ let path = Bundle.main.bundleURL.appendingPathComponent(name + ".png")
+ return UIImage(contentsOfFile: path.path) ?? UIImage()
+ }
+
+ var sections: [String: [AltIcon]] = [
+ "Main": [
+ AltIcon(displayName: "Backdoor", author: "BDG", key: nil, image: altImage("AppIcon60x60")),
+ AltIcon(displayName: "macOS Backdoor", author: "BDG", key: "Mac", image: altImage("Mac")),
+ AltIcon(displayName: "Evil Backdoor", author: "BDG", key: "Evil", image: altImage("Evil")),
+ AltIcon(displayName: "Classic Backdoor", author: "BDG", key: "Early", image: altImage("Early"))
+ ],
+ "Wingio": [
+ AltIcon(displayName: "Backdoor", author: "BDG", key: "Wing", image: altImage("Wing")),
+ ]
+ ]
+
+ init() { super.init(style: .insetGrouped) }
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupViews()
+ setupNavigation()
+ }
+
+ fileprivate func setupViews() {
+ self.tableView.delegate = self
+ self.tableView.dataSource = self
+ self.tableView.rowHeight = 75
+ }
+
+ fileprivate func setupNavigation() {
+ self.title = String.localized("SETTINGS_VIEW_CONTROLLER_CELL_APP_ICON")
+ self.navigationItem.largeTitleDisplayMode = .never
+ }
+
+ private func sectionTitles() -> [String] {
+ return Array(sections.keys).sorted()
+ }
+
+ private func icons(forSection section: Int) -> [AltIcon] {
+ let title = sectionTitles()[section]
+ return sections[title] ?? []
+ }
}
extension IconsListViewController {
- override func numberOfSections(in tableView: UITableView) -> Int { return sectionTitles().count }
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return icons(forSection: section).count }
- override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 40 }
-
- override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- let title = sectionTitles()[section]
- let headerView = InsetGroupedSectionHeader(title: title)
- return headerView
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = IconsListTableViewCell()
- let icon = icons(forSection: indexPath.section)[indexPath.row]
- cell.altIcon = icon
- if UIApplication.shared.alternateIconName == icon.key {
- cell.accessoryType = .checkmark
- } else {
- cell.accessoryType = .none
- }
- return cell
- }
-
- override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- tableView.deselectRow(at: indexPath, animated: true)
- let icon = icons(forSection: indexPath.section)[indexPath.row]
-
- UIApplication.shared.setAlternateIconName(icon.key) { error in
- Debug.shared.log(message:"\(error?.localizedDescription ?? "Unknown Error")")
- }
-
- self.tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows ?? [IndexPath](), with: .none)
- }
+ override func numberOfSections(in tableView: UITableView) -> Int { return sectionTitles().count }
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return icons(forSection: section).count }
+ override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 40 }
+
+ override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+ let title = sectionTitles()[section]
+ let headerView = InsetGroupedSectionHeader(title: title)
+ return headerView
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = IconsListTableViewCell()
+ let icon = icons(forSection: indexPath.section)[indexPath.row]
+ cell.altIcon = icon
+ if UIApplication.shared.alternateIconName == icon.key {
+ cell.accessoryType = .checkmark
+ } else {
+ cell.accessoryType = .none
+ }
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+ let icon = icons(forSection: indexPath.section)[indexPath.row]
+
+ UIApplication.shared.setAlternateIconName(icon.key) { error in
+ Debug.shared.log(message:"\(error?.localizedDescription ?? "Unknown Error")")
+ }
+
+ self.tableView.reloadRows(at: self.tableView.indexPathsForVisibleRows ?? [IndexPath](), with: .none)
+ }
}
struct AltIcon {
- var displayName: String
- var author: String
- var key: String?
- var image: UIImage
-}
+ var displayName: String
+ var author: String
+ var key: String?
+ var image: UIImage
+}
\ No newline at end of file
diff --git a/iOS/Views/Settings/SettingsViewController.swift b/iOS/Views/Settings/SettingsViewController.swift
index 2c6e4975..4a4bc610 100644
--- a/iOS/Views/Settings/SettingsViewController.swift
+++ b/iOS/Views/Settings/SettingsViewController.swift
@@ -12,7 +12,7 @@ import SwiftUI
class SettingsViewController: FRSTableViewController {
let aboutSection = [
- String.localized("SETTINGS_VIEW_CONTROLLER_CELL_ABOUT", arguments: "Feather")
+ String.localized("SETTINGS_VIEW_CONTROLLER_CELL_ABOUT", arguments: "Backdoor")
]
let displaySection = [
@@ -89,7 +89,7 @@ extension SettingsViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let reuseIdentifier = "Cell"
- var cell = UITableViewCell(style: .value1, reuseIdentifier: reuseIdentifier)
+ let cell = UITableViewCell(style: .value1, reuseIdentifier: reuseIdentifier)
cell.accessoryType = .none
cell.selectionStyle = .none
@@ -97,7 +97,7 @@ extension SettingsViewController {
cell.textLabel?.text = cellText
switch cellText {
- case String.localized("SETTINGS_VIEW_CONTROLLER_CELL_ABOUT", arguments: "Feather"):
+ case String.localized("SETTINGS_VIEW_CONTROLLER_CELL_ABOUT", arguments: "Backdoor"):
cell.setAccessoryIcon(with: "info.circle")
cell.selectionStyle = .default
@@ -155,7 +155,7 @@ extension SettingsViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let itemTapped = tableData[indexPath.section][indexPath.row]
switch itemTapped {
- case String.localized("SETTINGS_VIEW_CONTROLLER_CELL_ABOUT", arguments: "Feather"):
+ case String.localized("SETTINGS_VIEW_CONTROLLER_CELL_ABOUT", arguments: "Backdoor"):
let l = AboutViewController()
navigationController?.pushViewController(l, animated: true)
@@ -222,4 +222,4 @@ extension SettingsViewController {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/iOS/Views/Settings/View Logs/LogsViewController.swift b/iOS/Views/Settings/View Logs/LogsViewController.swift
index 7d13b90c..b69f5dd0 100644
--- a/iOS/Views/Settings/View Logs/LogsViewController.swift
+++ b/iOS/Views/Settings/View Logs/LogsViewController.swift
@@ -20,7 +20,7 @@ class LogsViewController: UIViewController {
setupNavigation()
setupViews()
startObservingLogFile()
- loadInitialLogContents() // Moved inside viewDidLoad for better organization
+ loadInitialLogContents()
}
override func viewDidAppear(_ animated: Bool) {
@@ -110,7 +110,7 @@ class LogsViewController: UIViewController {
fileHandle.seek(toFileOffset: currentFileSize)
let newData = fileHandle.readDataToEndOfFile()
- if let newContent = String(data: newData, encoding: .utf8), !newContent isEmpty {
+ if let newContent = String(data: newData, encoding: .utf8), !newContent.isEmpty {
logTextView.text.append(newContent)
scrollToBottom()
}
diff --git a/iOS/Views/Signing/SigningViewController/SettingsAltIconView.swift b/iOS/Views/Signing/SigningViewController/SettingsAltIconView.swift
index 0d458512..ab2784e8 100644
--- a/iOS/Views/Signing/SigningViewController/SettingsAltIconView.swift
+++ b/iOS/Views/Signing/SigningViewController/SettingsAltIconView.swift
@@ -1,120 +1,112 @@
-//
-// SettingsAltIconView.swift
-// feather
-//
-// Created by samara on 18.01.2025.
-//
-
import SwiftUI
struct SettingsAltIconView: View {
- @Environment(\.dismiss) var dismiss
-
- private let mainOptions: SigningMainDataWrapper
- private let applicationPath: URL
-
- init(mainOptions: SigningMainDataWrapper, app: URL) {
- self.mainOptions = mainOptions
- self.applicationPath = app
- }
-
- var body: some View {
- NavigationView {
- ScrollView {
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 8)], spacing: 8) {
- if let defaultIcon = loadDefaultIcon() {
- IconButton(
- iconPath: defaultIcon,
- name: "Default",
- applicationPath: applicationPath,
- action: {
- mainOptions.mainOptions.iconURL = nil
- dismiss()
- NotificationCenter.default.post(name: Notification.Name("reloadSigningController"), object: nil)
- }
- )
- }
-
- ForEach(loadAlternateIcons().sorted(by: { $0.key < $1.key }), id: \.key) { name, path in
- IconButton(
- iconPath: path,
- name: name,
- applicationPath: applicationPath,
- action: {
- mainOptions.mainOptions.iconURL = UIImage(contentsOfFile: applicationPath.appendingPathComponent(path).path)
- dismiss()
- NotificationCenter.default.post(name: Notification.Name("reloadSigningController"), object: nil)
- }
- )
- }
- }
- .padding()
- }
- .navigationTitle("Alt Icons")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- Button("Close") { dismiss() }
- }
- }
- }
+ @Environment(\.dismiss) var dismiss
+
+ private let mainOptions: SigningMainDataWrapper
+ private let applicationPath: URL
+
+ init(mainOptions: SigningMainDataWrapper, app: URL) {
+ self.mainOptions = mainOptions
+ self.applicationPath = app
+ }
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 8)], spacing: 8) {
+ if let defaultIcon = loadDefaultIcon() {
+ IconButton(
+ iconPath: defaultIcon,
+ name: "Default",
+ applicationPath: applicationPath,
+ action: {
+ mainOptions.mainOptions.iconURL = nil
+ dismiss()
+ NotificationCenter.default.post(name: Notification.Name("reloadSigningController"), object: nil)
+ }
+ )
+ }
+
+ ForEach(loadAlternateIcons().sorted(by: { $0.key < $1.key }), id: \.key) { name, path in
+ IconButton(
+ iconPath: path,
+ name: name,
+ applicationPath: applicationPath,
+ action: {
+ mainOptions.mainOptions.iconURL = UIImage(contentsOfFile: applicationPath.appendingPathComponent(path).path)
+ dismiss()
+ NotificationCenter.default.post(name: Notification.Name("reloadSigningController"), object: nil)
+ }
+ )
+ }
+ }
+ .padding()
+ }
+ .navigationTitle("Alt Icons")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ Button("Close") { dismiss() }
+ }
+ }
+ }
}
extension SettingsAltIconView {
- // im not making this better, I may be reusing code but I dont carfe
- private func loadDefaultIcon() -> String? {
- guard let infoPlistPath = applicationPath.appendingPathComponent("Info.plist") as? URL,
- let infoPlist = NSDictionary(contentsOf: infoPlistPath),
- let iconDict = infoPlist["CFBundleIcons"] as? [String: Any],
- let primaryIcon = iconDict["CFBundlePrimaryIcon"] as? [String: Any],
- let files = primaryIcon["CFBundleIconFiles"] as? [String],
- let iconPath = files.first else {
- return nil
- }
- return iconPath
- }
-
- private func loadAlternateIcons() -> [String: String] {
- guard let infoPlistPath = applicationPath.appendingPathComponent("Info.plist") as? URL,
- let infoPlist = NSDictionary(contentsOf: infoPlistPath),
- let iconDict = infoPlist["CFBundleIcons"] as? [String: Any],
- let alternateIcons = iconDict["CFBundleAlternateIcons"] as? [String: [String: Any]] else {
- return [:]
- }
-
- var icons: [String: String] = [:]
- for (name, details) in alternateIcons {
- if let files = details["CFBundleIconFiles"] as? [String],
- let iconPath = files.first {
- icons[name] = iconPath
- }
- }
- return icons
- }
+ private func loadDefaultIcon() -> String? {
+ guard let infoPlistPath = applicationPath.appendingPathComponent("Info.plist") as URL?,
+ let infoPlist = NSDictionary(contentsOf: infoPlistPath),
+ let iconDict = infoPlist["CFBundleIcons"] as? [String: Any],
+ let primaryIcon = iconDict["CFBundlePrimaryIcon"] as? [String: Any],
+ let files = primaryIcon["CFBundleIconFiles"] as? [String],
+ let iconPath = files.first else {
+ return nil
+ }
+ return iconPath
+ }
+
+ private func loadAlternateIcons() -> [String: String] {
+ guard let infoPlistPath = applicationPath.appendingPathComponent("Info.plist") as URL?,
+ let infoPlist = NSDictionary(contentsOf: infoPlistPath),
+ let iconDict = infoPlist["CFBundleIcons"] as? [String: Any],
+ let alternateIcons = iconDict["CFBundleAlternateIcons"] as? [String: [String: Any]] else {
+ return [:]
+ }
+
+ var icons: [String: String] = [:]
+ for (name, details) in alternateIcons {
+ if let files = details["CFBundleIconFiles"] as? [String],
+ let iconPath = files.first {
+ icons[name] = iconPath
+ }
+ }
+ return icons
+ }
}
private struct IconButton: View {
- let iconPath: String
- let name: String
- let applicationPath: URL
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- VStack {
- Image(uiImage: UIImage(contentsOfFile: applicationPath.appendingPathComponent(iconPath).path) ?? UIImage())
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 60, height: 60)
- .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
- Text(name)
- .font(.caption)
- .fontWeight(.bold)
- .lineLimit(2)
- .multilineTextAlignment(.center)
- .foregroundStyle(Color.primary)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .aspectRatio(1, contentMode: .fill)
- }
- }
-}
+ let iconPath: String
+ let name: String
+ let applicationPath: URL
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ VStack {
+ Image(uiImage: UIImage(contentsOfFile: applicationPath.appendingPathComponent(iconPath).path) ?? UIImage())
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 60, height: 60)
+ .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
+ Text(name)
+ .font(.caption)
+ .fontWeight(.bold)
+ .lineLimit(2)
+ .multilineTextAlignment(.center)
+ .foregroundStyle(Color.primary)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .aspectRatio(1, contentMode: .fill)
+ }
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Signing/SigningViewController/SigningsAdvancedViewController.swift b/iOS/Views/Signing/SigningViewController/SigningsAdvancedViewController.swift
index 3674bc7d..78eaa42a 100644
--- a/iOS/Views/Signing/SigningViewController/SigningsAdvancedViewController.swift
+++ b/iOS/Views/Signing/SigningViewController/SigningsAdvancedViewController.swift
@@ -1,130 +1,135 @@
-//
-// SigningsAdvancedViewController.swift
-// feather
-//
-// Created by samara on 27.10.2024.
-//
-
import UIKit
-class SigningsAdvancedViewController: FRSITableViewCOntroller {
- private var toggleOptions: [TogglesOption]
-
- override init(signingDataWrapper: SigningDataWrapper, mainOptions: SigningMainDataWrapper) {
- self.toggleOptions = feather.toggleOptions(signingDataWrapper: signingDataWrapper)
- super.init(signingDataWrapper: signingDataWrapper, mainOptions: mainOptions)
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- tableData = [
- [ String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_APPEARENCE") ],
- [ String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_MINIMUM_APP_VERSION") ],
- [],
- ]
-
- sectionTitles = [
- String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_APPEARENCE"),
- String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_MINIMUM_APP_VERSION"),
- String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_PROPERTIES"),
- ]
-
+class SigningsAdvancedViewController: UITableViewController {
+ private var toggleOptions: [TogglesOption]
+ private var signingDataWrapper: SigningDataWrapper
+ private var mainOptions: SigningMainDataWrapper
+ private var tableData: [[String]] = []
+ private var sectionTitles: [String] = []
+
+ init(signingDataWrapper: SigningDataWrapper, mainOptions: SigningMainDataWrapper) {
+ self.signingDataWrapper = signingDataWrapper
+ self.mainOptions = mainOptions
+ self.toggleOptions = feather.toggleOptions(signingDataWrapper: signingDataWrapper)
+ super.init(style: .insetGrouped)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableData = [
+ [String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_APPEARENCE")],
+ [String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_MINIMUM_APP_VERSION")],
+ [],
+ ]
+
+ sectionTitles = [
+ String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_APPEARENCE"),
+ String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_MINIMUM_APP_VERSION"),
+ String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_PROPERTIES"),
+ ]
+
self.title = String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_PROPERTIES")
- self.tableData[2] = toggleOptions.map { $0.title }
- }
+ self.tableData[2] = toggleOptions.map { $0.title }
+ }
}
extension SigningsAdvancedViewController {
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let reuseIdentifier = "Cell"
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return tableData.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return tableData[section].count
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let reuseIdentifier = "Cell"
let cell = UITableViewCell(style: .value1, reuseIdentifier: reuseIdentifier)
- cell.accessoryType = .none
- cell.selectionStyle = .gray
-
- let cellText = tableData[indexPath.section][indexPath.row]
- cell.textLabel?.text = cellText
-
-
- switch cellText {
- case String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_APPEARENCE"):
- let forceLightDarkAppearence = TweakLibraryViewCell()
- forceLightDarkAppearence.selectionStyle = .none
- forceLightDarkAppearence.configureSegmentedControl(
- with: mainOptions.mainOptions.forceLightDarkAppearenceString,
- selectedIndex: 0
- )
- forceLightDarkAppearence.segmentedControl.addTarget(self, action: #selector(forceLightDarkAppearenceDidChange(_:)), for: .valueChanged)
-
- return forceLightDarkAppearence
- case String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_MINIMUM_APP_VERSION"):
- let forceMinimumVersion = TweakLibraryViewCell()
- forceMinimumVersion.selectionStyle = .none
- forceMinimumVersion.configureSegmentedControl(
- with: mainOptions.mainOptions.forceMinimumVersionString,
- selectedIndex: 0
- )
- forceMinimumVersion.segmentedControl.addTarget(self, action: #selector(forceMinimumVersionDidChange(_:)), for: .valueChanged)
-
- return forceMinimumVersion
- default:
- break
- }
-
- if indexPath.section == 2 {
- let toggleOption = toggleOptions[indexPath.row]
- cell.textLabel?.text = toggleOption.title
- let toggleSwitch = UISwitch()
- toggleSwitch.isOn = toggleOption.binding
- toggleSwitch.tag = indexPath.row
- toggleSwitch.addTarget(self, action: #selector(toggleOptionsSwitches(_:)), for: .valueChanged)
- cell.accessoryView = toggleSwitch
- }
-
- return cell
-
- }
+ cell.accessoryType = .none
+ cell.selectionStyle = .gray
+
+ let cellText = tableData[indexPath.section][indexPath.row]
+ cell.textLabel?.text = cellText
+
+ switch cellText {
+ case String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_APPEARENCE"):
+ let forceLightDarkAppearance = TweakLibraryViewCell()
+ forceLightDarkAppearance.selectionStyle = .none
+ forceLightDarkAppearance.configureSegmentedControl(
+ with: mainOptions.mainOptions.forceLightDarkAppearenceString,
+ selectedIndex: 0
+ )
+ forceLightDarkAppearance.segmentedControl.addTarget(self, action: #selector(forceLightDarkAppearanceDidChange(_:)), for: .valueChanged)
+
+ return forceLightDarkAppearance
+ case String.localized("APP_SIGNING_INPUT_VIEW_CONTROLLER_SECTION_TITLE_MINIMUM_APP_VERSION"):
+ let forceMinimumVersion = TweakLibraryViewCell()
+ forceMinimumVersion.selectionStyle = .none
+ forceMinimumVersion.configureSegmentedControl(
+ with: mainOptions.mainOptions.forceMinimumVersionString,
+ selectedIndex: 0
+ )
+ forceMinimumVersion.segmentedControl.addTarget(self, action: #selector(forceMinimumVersionDidChange(_:)), for: .valueChanged)
+
+ return forceMinimumVersion
+ default:
+ break
+ }
+
+ if indexPath.section == 2 {
+ let toggleOption = toggleOptions[indexPath.row]
+ cell.textLabel?.text = toggleOption.title
+ let toggleSwitch = UISwitch()
+ toggleSwitch.isOn = toggleOption.binding
+ toggleSwitch.tag = indexPath.row
+ toggleSwitch.addTarget(self, action: #selector(toggleOptionsSwitches(_:)), for: .valueChanged)
+ cell.accessoryView = toggleSwitch
+ }
+
+ return cell
+ }
}
extension SigningsAdvancedViewController {
- @objc private func forceLightDarkAppearenceDidChange(_ sender: UISegmentedControl) {
- signingDataWrapper.signingOptions.forceLightDarkAppearence =
- mainOptions.mainOptions.forceLightDarkAppearenceString[sender.selectedSegmentIndex]
- }
-
- @objc private func forceMinimumVersionDidChange(_ sender: UISegmentedControl) {
- signingDataWrapper.signingOptions.forceMinimumVersion =
- mainOptions.mainOptions.forceMinimumVersionString[sender.selectedSegmentIndex]
- }
-
- @objc func toggleOptionsSwitches(_ sender: UISwitch) {
- switch sender.tag {
- case 0:
- signingDataWrapper.signingOptions.removePlugins = sender.isOn
- case 1:
- signingDataWrapper.signingOptions.forceFileSharing = sender.isOn
- case 2:
- signingDataWrapper.signingOptions.removeSupportedDevices = sender.isOn
- case 3:
- signingDataWrapper.signingOptions.removeURLScheme = sender.isOn
- case 4:
- signingDataWrapper.signingOptions.forceProMotion = sender.isOn
- case 5:
- signingDataWrapper.signingOptions.forceForceFullScreen = sender.isOn
- case 6:
- signingDataWrapper.signingOptions.forceiTunesFileSharing = sender.isOn
- case 7:
- signingDataWrapper.signingOptions.forceTryToLocalize = sender.isOn
- case 8:
- signingDataWrapper.signingOptions.removeProvisioningFile = sender.isOn
- case 9:
- signingDataWrapper.signingOptions.removeWatchPlaceHolder = sender.isOn
- default:
- break
- }
- }
-}
+ @objc private func forceLightDarkAppearanceDidChange(_ sender: UISegmentedControl) {
+ signingDataWrapper.signingOptions.forceLightDarkAppearence =
+ mainOptions.mainOptions.forceLightDarkAppearenceString[sender.selectedSegmentIndex]
+ }
+
+ @objc private func forceMinimumVersionDidChange(_ sender: UISegmentedControl) {
+ signingDataWrapper.signingOptions.forceMinimumVersion =
+ mainOptions.mainOptions.forceMinimumVersionString[sender.selectedSegmentIndex]
+ }
+
+ @objc func toggleOptionsSwitches(_ sender: UISwitch) {
+ switch sender.tag {
+ case 0:
+ signingDataWrapper.signingOptions.removePlugins = sender.isOn
+ case 1:
+ signingDataWrapper.signingOptions.forceFileSharing = sender.isOn
+ case 2:
+ signingDataWrapper.signingOptions.removeSupportedDevices = sender.isOn
+ case 3:
+ signingDataWrapper.signingOptions.removeURLScheme = sender.isOn
+ case 4:
+ signingDataWrapper.signingOptions.forceProMotion = sender.isOn
+ case 5:
+ signingDataWrapper.signingOptions.forceForceFullScreen = sender.isOn
+ case 6:
+ signingDataWrapper.signingOptions.forceiTunesFileSharing = sender.isOn
+ case 7:
+ signingDataWrapper.signingOptions.forceTryToLocalize = sender.isOn
+ case 8:
+ signingDataWrapper.signingOptions.removeProvisioningFile = sender.isOn
+ case 9:
+ signingDataWrapper.signingOptions.removeWatchPlaceHolder = sender.isOn
+ default:
+ break
+ }
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Signing/SigningViewController/SigningsDylibViewController.swift b/iOS/Views/Signing/SigningViewController/SigningsDylibViewController.swift
index 5ae961c8..d66ba6f9 100644
--- a/iOS/Views/Signing/SigningViewController/SigningsDylibViewController.swift
+++ b/iOS/Views/Signing/SigningViewController/SigningsDylibViewController.swift
@@ -1,118 +1,139 @@
-//
-// AppSigningDylibViewController.swift
-// feather
-//
-// Created by samara on 29.08.2024.
-//
-
import UIKit
+import Foundation
class SigningsDylibViewController: UITableViewController {
- var applicationPath: URL
- var groupedDylibs: [String: [String]] = [:]
- var dylibSections: [String] = ["@rpath", "@executable_path", "/usr/lib", "/System/Library", "Other"]
- var dylibstoremove: [String] = [] {
- didSet {
- self.mainOptions.mainOptions.removeInjectPaths = self.dylibstoremove
- }
- }
-
- var mainOptions: SigningMainDataWrapper
-
- init(mainOptions: SigningMainDataWrapper, app: URL) {
- self.mainOptions = mainOptions
- self.applicationPath = app
- super.init(style: .insetGrouped)
-
- do {
- let balls = try TweakHandler.findExecutable(at: applicationPath)
- if let dylibs = listDylibs(filePath: balls!.path) {
- groupDylibs(dylibs)
- }
- } catch {
- print(error)
- }
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- setupViews()
- setupNavigation()
- self.dylibstoremove = self.mainOptions.mainOptions.removeInjectPaths
-
- }
-
- fileprivate func setupViews() {
- self.tableView.dataSource = self
- self.tableView.delegate = self
- tableView.register(UITableViewCell.self, forCellReuseIdentifier: "dylibCell")
-
- let alertController = UIAlertController(title: "ADVANCED USERS ONLY", message: "This section can make installed applications UNUSABLE and potentially UNSTABLE. USE THIS SECTION WITH CAUTION, IF YOU HAVE NO IDEA WHAT YOU'RE DOING, PLEASE LEAVE.\n\nIF YOU MAKE AN ISSUE ON THIS, IT WILL IMMEDIATELY BE CLOSED AND IGNORED.", preferredStyle: .alert)
-
- let continueAction = UIAlertAction(title: "WHO CARES", style: .destructive, handler: nil)
-
- alertController.addAction(continueAction)
-
- present(alertController, animated: true, completion: nil)
-
- }
-
- fileprivate func setupNavigation() {
- title = "Remove Dylibs"
-
- }
-
- fileprivate func groupDylibs(_ dylibs: [String]) {
- groupedDylibs["@rpath"] = dylibs.filter { $0.hasPrefix("@rpath") }
- groupedDylibs["@executable_path"] = dylibs.filter { $0.hasPrefix("@executable_path") }
- groupedDylibs["/usr/lib"] = dylibs.filter { $0.hasPrefix("/usr/lib") }
- groupedDylibs["/System/Library"] = dylibs.filter { $0.hasPrefix("/System/Library") }
- groupedDylibs["Other"] = dylibs.filter { dylib in
- !dylib.hasPrefix("@rpath") &&
- !dylib.hasPrefix("@executable_path") &&
- !dylib.hasPrefix("/usr/lib") &&
- !dylib.hasPrefix("/System/Library")
- }
- }
-
- override func numberOfSections(in tableView: UITableView) -> Int {
- return dylibSections.count
- }
-
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
- let key = dylibSections[section]
- return groupedDylibs[key]?.count ?? 0
- }
-
- override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
- return dylibSections[section]
- }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: "dylibCell", for: indexPath)
- let key = dylibSections[indexPath.section]
- if let dylib = groupedDylibs[key]?[indexPath.row] {
- cell.textLabel?.text = dylib
- cell.textLabel?.textColor = dylibstoremove.contains(dylib) ? .systemRed : .label
- }
- return cell
- }
-
- override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
- if editingStyle == .delete {
- let key = dylibSections[indexPath.section]
- if let dylib = groupedDylibs[key]?[indexPath.row] {
- if !dylibstoremove.contains(dylib) {
- dylibstoremove.append(dylib)
- }
- tableView.reloadRows(at: [indexPath], with: .automatic)
- print(dylibstoremove)
- }
- }
- }
-
-}
+ var applicationPath: URL
+ var groupedDylibs: [String: [String]] = [:]
+ var dylibSections: [String] = ["@rpath", "@executable_path", "/usr/lib", "/System/Library", "Other"]
+ var dylibstoremove: [String] = [] {
+ didSet {
+ self.mainOptions.mainOptions.removeInjectPaths = self.dylibstoremove
+ }
+ }
+
+ var mainOptions: SigningMainDataWrapper
+
+ init(mainOptions: SigningMainDataWrapper, app: URL) {
+ self.mainOptions = mainOptions
+ self.applicationPath = app
+ super.init(style: .insetGrouped)
+
+ do {
+ if let executable = try TweakHandler.findExecutable(at: applicationPath) {
+ listDylibs(filePath: executable.path) { result in
+ switch result {
+ case .success(let dylibs):
+ self.groupDylibs(dylibs)
+ case .failure(let error):
+ print("Failed to list dylibs: \(error)")
+ }
+ }
+ } else {
+ print("Failed to find executable")
+ }
+ } catch {
+ print("Error finding executable: \(error)")
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupViews()
+ setupNavigation()
+ self.dylibstoremove = self.mainOptions.mainOptions.removeInjectPaths
+ }
+
+ fileprivate func setupViews() {
+ self.tableView.dataSource = self
+ self.tableView.delegate = self
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "dylibCell")
+
+ let alertController = UIAlertController(title: "ADVANCED USERS ONLY", message: "This section can make installed applications UNUSABLE and potentially UNSTABLE. USE THIS SECTION WITH CAUTION.", preferredStyle: .alert)
+ let continueAction = UIAlertAction(title: "WHO CARES", style: .destructive, handler: nil)
+ alertController.addAction(continueAction)
+ present(alertController, animated: true, completion: nil)
+ }
+
+ fileprivate func setupNavigation() {
+ title = "Remove Dylibs"
+ }
+
+ fileprivate func groupDylibs(_ dylibs: [String]) {
+ groupedDylibs["@rpath"] = dylibs.filter { $0.hasPrefix("@rpath") }.sorted()
+ groupedDylibs["@executable_path"] = dylibs.filter { $0.hasPrefix("@executable_path") }.sorted()
+ groupedDylibs["/usr/lib"] = dylibs.filter { $0.hasPrefix("/usr/lib") }.sorted()
+ groupedDylibs["/System/Library"] = dylibs.filter { $0.hasPrefix("/System/Library") }.sorted()
+ groupedDylibs["Other"] = dylibs.filter { dylib in
+ !dylib.hasPrefix("@rpath") &&
+ !dylib.hasPrefix("@executable_path") &&
+ !dylib.hasPrefix("/usr/lib") &&
+ !dylib.hasPrefix("/System/Library")
+ }.sorted()
+ }
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return dylibSections.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let key = dylibSections[section]
+ return groupedDylibs[key]?.count ?? 0
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return dylibSections[section]
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "dylibCell", for: indexPath)
+ let key = dylibSections[indexPath.section]
+ if let dylib = groupedDylibs[key]?[indexPath.row] {
+ cell.textLabel?.text = dylib
+ cell.textLabel?.textColor = dylibstoremove.contains(dylib) ? .systemRed : .label
+ }
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let key = dylibSections[indexPath.section]
+ if let dylib = groupedDylibs[key]?[indexPath.row] {
+ if dylibstoremove.contains(dylib) {
+ if let index = dylibstoremove.firstIndex(of: dylib) {
+ dylibstoremove.remove(at: index)
+ }
+ } else {
+ dylibstoremove.append(dylib)
+ }
+ tableView.reloadRows(at: [indexPath], with: .automatic)
+ print(dylibstoremove)
+ }
+ tableView.deselectRow(at: indexPath, animated: true)
+ }
+
+ func listDylibs(filePath: String, completion: @escaping (Result<[String], Error>) -> Void) {
+ let command = "/usr/bin/otool -L \(filePath)"
+ ProcessUtility.shared.executeShellCommand(command) { output in
+ guard let output = output else {
+ completion(.failure(NSError(domain: "ProcessUtility", code: -1, userInfo: [NSLocalizedDescriptionKey: "No output from process"])))
+ return
+ }
+
+ let lines = output.components(separatedBy: .newlines)
+ var dylibs: [String] = []
+
+ for line in lines {
+ let trimmedLine = line.trimmingCharacters(in: .whitespaces)
+ if trimmedLine.hasPrefix("\t") {
+ if let dylib = trimmedLine.components(separatedBy: "(").first?.trimmingCharacters(in: .whitespaces) {
+ dylibs.append(dylib)
+ }
+ }
+ }
+ completion(.success(dylibs))
+ }
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Signing/SigningViewController/SigningsInputViewController.swift b/iOS/Views/Signing/SigningViewController/SigningsInputViewController.swift
index 0fe52d2d..07e2b2a2 100644
--- a/iOS/Views/Signing/SigningViewController/SigningsInputViewController.swift
+++ b/iOS/Views/Signing/SigningViewController/SigningsInputViewController.swift
@@ -1,90 +1,92 @@
-//
-// SigningsInputViewController.swift
-// feather
-//
-// Created by samara on 8/15/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import Foundation
import UIKit
class SigningsInputViewController: UITableViewController {
- var parentView: SigningsViewController
- var initialValue: String
- var valueToSaveTo: Int
- private var changedValue: String?
-
- private lazy var textField: UITextField = {
- let textField = UITextField(frame: .zero)
- textField.translatesAutoresizingMaskIntoConstraints = false
- textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
- return textField
- }()
+ var parentView: SigningsViewController
+ var initialValue: String
+ var valueToSaveTo: Int
+ private var changedValue: String?
+
+ private lazy var textField: UITextField = {
+ let textField = UITextField(frame: .zero)
+ textField.translatesAutoresizingMaskIntoConstraints = false
+ textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)
+ return textField
+ }()
- init(parentView: SigningsViewController, initialValue: String, valueToSaveTo: Int) {
- self.parentView = parentView
- self.initialValue = initialValue
- self.valueToSaveTo = valueToSaveTo
- super.init(style: .insetGrouped)
- }
-
- required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- navigationItem.largeTitleDisplayMode = .never
- self.title = initialValue.capitalized
-
- let saveButton = UIBarButtonItem(title: String.localized("SAVE"), style: .done, target: self, action: #selector(saveButton))
- saveButton.isEnabled = false
- navigationItem.rightBarButtonItem = saveButton
-
- tableView.register(UITableViewCell.self, forCellReuseIdentifier: "InputCell")
- }
-
- @objc func saveButton() {
- switch valueToSaveTo {
- case 1:
- parentView.mainOptions.mainOptions.name = changedValue
- case 2:
- parentView.mainOptions.mainOptions.bundleId = changedValue
- case 3:
- parentView.mainOptions.mainOptions.version = changedValue
- default:
- break
- }
-
- self.navigationController?.popViewController(animated: true)
- }
-
- @objc private func textDidChange() {
- navigationItem.rightBarButtonItem?.isEnabled = !(textField.text?.isEmpty ?? true)
- changedValue = textField.text
- }
-
- override func numberOfSections(in tableView: UITableView) -> Int { return 1 }
- override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 }
-
- override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let cell = tableView.dequeueReusableCell(withIdentifier: "InputCell", for: indexPath)
- switch indexPath.section {
- case 0:
- textField.text = initialValue
- textField.placeholder = initialValue
-
- if textField.superview == nil {
- cell.contentView.addSubview(textField)
- NSLayoutConstraint.activate([
- textField.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor),
- textField.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
- textField.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor)
- ])
- }
-
- cell.selectionStyle = .none
- default: break
- }
- return cell
- }
-}
+ init(parentView: SigningsViewController, initialValue: String, valueToSaveTo: Int) {
+ self.parentView = parentView
+ self.initialValue = initialValue
+ self.valueToSaveTo = valueToSaveTo
+ super.init(style: .insetGrouped)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ navigationItem.largeTitleDisplayMode = .never
+ self.title = initialValue.capitalized
+
+ let saveButton = UIBarButtonItem(title: String.localized("SAVE"), style: .done, target: self, action: #selector(saveButton))
+ saveButton.isEnabled = false
+ navigationItem.rightBarButtonItem = saveButton
+
+ tableView.register(UITableViewCell.self, forCellReuseIdentifier: "InputCell")
+ }
+
+ @objc func saveButton() {
+ guard let changedValue = changedValue else { return }
+
+ switch valueToSaveTo {
+ case 1:
+ parentView.mainOptions.mainOptions.name = changedValue
+ case 2:
+ parentView.mainOptions.mainOptions.bundleId = changedValue
+ case 3:
+ parentView.mainOptions.mainOptions.version = changedValue
+ default:
+ break
+ }
+
+ self.navigationController?.popViewController(animated: true)
+ }
+
+ @objc private func textDidChange() {
+ navigationItem.rightBarButtonItem?.isEnabled = !(textField.text?.isEmpty ?? true)
+ changedValue = textField.text
+ }
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "InputCell", for: indexPath)
+ switch indexPath.section {
+ case 0:
+ textField.text = initialValue
+ textField.placeholder = initialValue
+
+ if textField.superview == nil {
+ cell.contentView.addSubview(textField)
+ NSLayoutConstraint.activate([
+ textField.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor),
+ textField.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor),
+ textField.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor)
+ ])
+ }
+
+ cell.selectionStyle = .none
+ default:
+ break
+ }
+ return cell
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/Signing/SigningViewController/SigningsViewController.swift b/iOS/Views/Signing/SigningViewController/SigningsViewController.swift
index 983cb819..81923195 100644
--- a/iOS/Views/Signing/SigningViewController/SigningsViewController.swift
+++ b/iOS/Views/Signing/SigningViewController/SigningsViewController.swift
@@ -1,433 +1,425 @@
-//
-// SigningsViewController.swift
-// feather
-//
-// Created by samara on 26.10.2024.
-//
-
import UIKit
import CoreData
struct BundleOptions {
- var name: String?
- var bundleId: String?
- var version: String?
- var sourceURL: URL?
+ var name: String?
+ var bundleId: String?
+ var version: String?
+ var sourceURL: URL?
}
class SigningsViewController: UIViewController {
-
- var tableData = [
- [
- "AppIcon",
- String.localized("APPS_INFORMATION_TITLE_NAME"),
- String.localized("APPS_INFORMATION_TITLE_IDENTIFIER"),
- String.localized("APPS_INFORMATION_TITLE_VERSION"),
- ],
- [
- "Signing",
- ],
- [
- String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_ADD_TWEAKS"),
- String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_MODIFY_DYLIBS"),
- ],
- [ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_PROPERTIES") ],
- ]
+
+ var tableData = [
+ [
+ "AppIcon",
+ String.localized("APPS_INFORMATION_TITLE_NAME"),
+ String.localized("APPS_INFORMATION_TITLE_IDENTIFIER"),
+ String.localized("APPS_INFORMATION_TITLE_VERSION"),
+ ],
+ [
+ "Signing",
+ ],
+ [
+ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_ADD_TWEAKS"),
+ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_MODIFY_DYLIBS"),
+ ],
+ [ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_PROPERTIES") ],
+ ]
- var sectionTitles = [
- String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_TITLE_CUSTOMIZATION"),
- String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_TITLE_SIGNING"),
- String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_TITLE_ADVANCED"),
- "",
- ]
-
- public var application: NSManagedObject?
- private var appsViewController: LibraryViewController?
-
- var signingDataWrapper: SigningDataWrapper
- var mainOptions = SigningMainDataWrapper(mainOptions: MainSigningOptions())
-
- var bundle: BundleOptions?
-
- var tableView: UITableView!
- private var variableBlurView: UIVariableBlurView?
- private var largeButton = ActivityIndicatorButton()
- private var iconCell = IconImageViewCell()
- var signingCompletionHandler: ((Bool) -> Void)?
-
- init(signingDataWrapper: SigningDataWrapper, application: NSManagedObject, appsViewController: LibraryViewController) {
- self.signingDataWrapper = signingDataWrapper
- self.application = application
- self.appsViewController = appsViewController
- super.init(nibName: nil, bundle: nil)
-
- if let name = application.value(forKey: "name") as? String,
- let bundleId = application.value(forKey: "bundleidentifier") as? String,
- let version = application.value(forKey: "version") as? String {
- let sourceLocation = application.value(forKey: "oSU") as? String
- let sourceURL = sourceLocation != nil ? URL(string: sourceLocation!) : nil
- self.bundle = BundleOptions(
- name: name,
- bundleId: bundleId,
- version: version,
- sourceURL: sourceURL
- )
- }
-
- if let hasGotCert = CoreDataManager.shared.getCurrentCertificate() { self.mainOptions.mainOptions.certificate = hasGotCert }
- if let uuid = application.value(forKey: "uuid") as? String { self.mainOptions.mainOptions.uuid = uuid }
-
- if signingDataWrapper.signingOptions.ppqCheckProtection &&
- mainOptions.mainOptions.certificate?.certData?.pPQCheck == true {
-
- if !signingDataWrapper.signingOptions.dynamicProtection {
- mainOptions.mainOptions.bundleId = (bundle?.bundleId)!+"."+Preferences.pPQCheckString
- }
- }
-
- if let currentBundleId = bundle?.bundleId,
- let newBundleId = signingDataWrapper.signingOptions.bundleIdConfig[currentBundleId] {
- mainOptions.mainOptions.bundleId = newBundleId
- }
-
- if let currentName = bundle?.name,
- let newName = signingDataWrapper.signingOptions.displayNameConfig[currentName] {
- mainOptions.mainOptions.name = newName
- }
-
- if signingDataWrapper.signingOptions.dynamicProtection {
- Task {
- await checkDynamicProtection()
- }
- }
- }
-
- private func checkDynamicProtection() async {
- guard signingDataWrapper.signingOptions.ppqCheckProtection,
- mainOptions.mainOptions.certificate?.certData?.pPQCheck == true,
- let bundleId = bundle?.bundleId else {
- return
- }
-
- let shouldModify = await BundleIdChecker.shouldModifyBundleId(originalBundleId: bundleId)
- if shouldModify {
- mainOptions.mainOptions.bundleId = bundleId+"."+Preferences.pPQCheckString
- }
- }
-
- required init?(coder: NSCoder) {
- fatalError("init(coder:) has not been implemented")
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- setupNavigation()
- setupViews()
- setupToolbar()
- #if !targetEnvironment(simulator)
- certAlert()
- #endif
-
- let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
- swipeLeft.direction = .left
- let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
- swipeRight.direction = .right
- tableView.addGestureRecognizer(swipeLeft)
- tableView.addGestureRecognizer(swipeRight)
- NotificationCenter.default.addObserver(self, selector: #selector(fetch), name: Notification.Name("reloadSigningController"), object: nil)
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self, name: Notification.Name("reloadSigningController"), object: nil)
- }
-
- @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
- let location = gesture.location(in: tableView)
- if let indexPath = tableView.indexPathForRow(at: location),
- indexPath.section == 1 && indexPath.row == 0 {
- let certificates = CoreDataManager.shared.getDatedCertificate()
- guard certificates.count > 1 else { return }
-
- let currentIndex = certificates.firstIndex { $0 == mainOptions.mainOptions.certificate } ?? 0
- var newIndex = currentIndex
-
- switch gesture.direction {
- case .left:
- newIndex = (currentIndex + 1) % certificates.count
- case .right:
- newIndex = (currentIndex - 1 + certificates.count) % certificates.count
- default:
- break
- }
-
- let feedbackGenerator = UISelectionFeedbackGenerator()
- feedbackGenerator.prepare()
- feedbackGenerator.selectionChanged()
-
- Preferences.selectedCert = newIndex
- mainOptions.mainOptions.certificate = certificates[newIndex]
- tableView.reloadRows(at: [indexPath], with: gesture.direction == .left ? .left : .right)
- }
- }
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- self.tableView.reloadData()
- }
-
- fileprivate func setupNavigation() {
- let logoImageView = UIImageView(image: UIImage(named: "feather_glyph"))
- logoImageView.contentMode = .scaleAspectFit
- navigationItem.titleView = logoImageView
- self.navigationController?.navigationBar.prefersLargeTitles = false
-
- self.isModalInPresentation = true
- self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: String.localized("DISMISS"), style: .done, target: self, action: #selector(closeSheet))
- }
-
- fileprivate func setupViews() {
- self.tableView = UITableView(frame: .zero, style: .insetGrouped)
- self.tableView.translatesAutoresizingMaskIntoConstraints = false
- self.tableView.dataSource = self
- self.tableView.delegate = self
- self.tableView.showsHorizontalScrollIndicator = false
- self.tableView.showsVerticalScrollIndicator = false
- self.tableView.contentInset.bottom = 70
-
- self.view.addSubview(tableView)
- self.tableView.constraintCompletely(to: view)
- }
-
- fileprivate func setupToolbar() {
- largeButton.translatesAutoresizingMaskIntoConstraints = false
- largeButton.addTarget(self, action: #selector(startSign), for: .touchUpInside)
-
- let gradientMask = VariableBlurViewConstants.defaultGradientMask
- variableBlurView = UIVariableBlurView(frame: .zero)
- variableBlurView?.gradientMask = gradientMask
- variableBlurView?.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
- variableBlurView?.translatesAutoresizingMaskIntoConstraints = false
-
- view.addSubview(variableBlurView!)
- view.addSubview(largeButton)
-
- var height = 80.0
- if UIDevice.current.userInterfaceIdiom == .pad { height = 65.0 }
-
- NSLayoutConstraint.activate([
- variableBlurView!.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
- variableBlurView!.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
- variableBlurView!.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- variableBlurView!.heightAnchor.constraint(equalToConstant: height),
-
- largeButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
- largeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
- largeButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -17),
- largeButton.heightAnchor.constraint(equalToConstant: 50)
- ])
-
- variableBlurView?.layer.zPosition = 3
- largeButton.layer.zPosition = 4
- }
-
- fileprivate func certAlert() {
- if (mainOptions.mainOptions.certificate == nil) {
- DispatchQueue.main.async {
- let alert = UIAlertController(
- title: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_TITLE"),
- message: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_DESCRIPTION"),
- preferredStyle: .alert
- )
- alert.addAction(UIAlertAction(title: String.localized("LAME"), style: .default) { _ in
- self.dismiss(animated: true)
- }
- )
- self.present(alert, animated: true, completion: nil)
- }
- }
- }
-
- @objc func closeSheet() {
- dismiss(animated: true, completion: nil)
- }
-
- @objc func fetch() {
- self.tableView.reloadData()
- }
-
- @objc func startSign() {
- self.navigationItem.leftBarButtonItem = nil
- largeButton.showLoadingIndicator()
- signInitialApp(
- bundle: bundle!,
- mainOptions: mainOptions,
- signingOptions: signingDataWrapper,
- appPath:getFilesForDownloadedApps(app: application as! DownloadedApps, getuuidonly: false))
- { result in
- switch result {
- case .success(let (signedPath, signedApp)):
- self.appsViewController?.fetchSources()
- self.appsViewController?.tableView.reloadData()
- Debug.shared.log(message: signedPath.path)
- if self.signingDataWrapper.signingOptions.installAfterSigned {
- self.appsViewController?.startInstallProcess(meow: signedApp, filePath: signedPath.path)
- self.signingCompletionHandler?(true)
- }
+ var sectionTitles = [
+ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_TITLE_CUSTOMIZATION"),
+ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_TITLE_SIGNING"),
+ String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_TITLE_ADVANCED"),
+ "",
+ ]
+
+ public var application: NSManagedObject?
+ private var appsViewController: LibraryViewController?
+
+ var signingDataWrapper: SigningDataWrapper
+ var mainOptions = SigningMainDataWrapper(mainOptions: MainSigningOptions())
+
+ var bundle: BundleOptions?
+
+ var tableView: UITableView!
+ private var variableBlurView: UIVariableBlurView?
+ private var largeButton = ActivityIndicatorButton()
+ private var iconCell = IconImageViewCell()
+ var signingCompletionHandler: ((Bool) -> Void)?
+
+ init(signingDataWrapper: SigningDataWrapper, application: NSManagedObject, appsViewController: LibraryViewController) {
+ self.signingDataWrapper = signingDataWrapper
+ self.application = application
+ self.appsViewController = appsViewController
+ super.init(nibName: nil, bundle: nil)
+
+ if let name = application.value(forKey: "name") as? String,
+ let bundleId = application.value(forKey: "bundleidentifier") as? String,
+ let version = application.value(forKey: "version") as? String {
+ let sourceLocation = application.value(forKey: "oSU") as? String
+ let sourceURL = sourceLocation != nil ? URL(string: sourceLocation!) : nil
+ self.bundle = BundleOptions(
+ name: name,
+ bundleId: bundleId,
+ version: version,
+ sourceURL: sourceURL
+ )
+ }
+
+ if let hasGotCert = CoreDataManager.shared.getCurrentCertificate() { self.mainOptions.mainOptions.certificate = hasGotCert }
+ if let uuid = application.value(forKey: "uuid") as? String { self.mainOptions.mainOptions.uuid = uuid }
+
+ if signingDataWrapper.signingOptions.ppqCheckProtection &&
+ mainOptions.mainOptions.certificate?.certData?.pPQCheck == true {
+
+ if !signingDataWrapper.signingOptions.dynamicProtection {
+ mainOptions.mainOptions.bundleId = (bundle?.bundleId)!+"."+Preferences.pPQCheckString
+ }
+ }
+
+ if let currentBundleId = bundle?.bundleId,
+ let newBundleId = signingDataWrapper.signingOptions.bundleIdConfig[currentBundleId] {
+ mainOptions.mainOptions.bundleId = newBundleId
+ }
+
+ if let currentName = bundle?.name,
+ let newName = signingDataWrapper.signingOptions.displayNameConfig[currentName] {
+ mainOptions.mainOptions.name = newName
+ }
+
+ if signingDataWrapper.signingOptions.dynamicProtection {
+ Task {
+ await checkDynamicProtection()
+ }
+ }
+ }
+
+ private func checkDynamicProtection() async {
+ guard signingDataWrapper.signingOptions.ppqCheckProtection,
+ mainOptions.mainOptions.certificate?.certData?.pPQCheck == true,
+ let bundleId = bundle?.bundleId else {
+ return
+ }
+
+ let shouldModify = await BundleIdChecker.shouldModifyBundleId(originalBundleId: bundleId)
+ if shouldModify {
+ mainOptions.mainOptions.bundleId = bundleId+"."+Preferences.pPQCheckString
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ setupNavigation()
+ setupViews()
+ setupToolbar()
+ #if !targetEnvironment(simulator)
+ certAlert()
+ #endif
+
+ let swipeLeft = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
+ swipeLeft.direction = .left
+ let swipeRight = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
+ swipeRight.direction = .right
+ tableView.addGestureRecognizer(swipeLeft)
+ tableView.addGestureRecognizer(swipeRight)
+ NotificationCenter.default.addObserver(self, selector: #selector(fetch), name: Notification.Name("reloadSigningController"), object: nil)
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self, name: Notification.Name("reloadSigningController"), object: nil)
+ }
+
+ @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
+ let location = gesture.location(in: tableView)
+ if let indexPath = tableView.indexPathForRow(at: location),
+ indexPath.section == 1 && indexPath.row == 0 {
+ let certificates = CoreDataManager.shared.getDatedCertificate()
+ guard certificates.count > 1 else { return }
+
+ let currentIndex = certificates.firstIndex { $0 == mainOptions.mainOptions.certificate } ?? 0
+ var newIndex = currentIndex
+
+ switch gesture.direction {
+ case .left:
+ newIndex = (currentIndex + 1) % certificates.count
+ case .right:
+ newIndex = (currentIndex - 1 + certificates.count) % certificates.count
+ default:
+ break
+ }
+
+ let feedbackGenerator = UISelectionFeedbackGenerator()
+ feedbackGenerator.prepare()
+ feedbackGenerator.selectionChanged()
+
+ Preferences.selectedCert = newIndex
+ mainOptions.mainOptions.certificate = certificates[newIndex]
+ tableView.reloadRows(at: [indexPath], with: gesture.direction == .left ? .left : .right)
+ }
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ self.tableView.reloadData()
+ }
+
+ fileprivate func setupNavigation() {
+ let logoImageView = UIImageView(image: UIImage(named: "feather_glyph"))
+ logoImageView.contentMode = .scaleAspectFit
+ navigationItem.titleView = logoImageView
+ self.navigationController?.navigationBar.prefersLargeTitles = false
+
+ self.isModalInPresentation = true
+ self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: String.localized("DISMISS"), style: .done, target: self, action: #selector(closeSheet))
+ }
+
+ fileprivate func setupViews() {
+ self.tableView = UITableView(frame: .zero, style: .insetGrouped)
+ self.tableView.translatesAutoresizingMaskIntoConstraints = false
+ self.tableView.dataSource = self
+ self.tableView.delegate = self
+ self.tableView.showsHorizontalScrollIndicator = false
+ self.tableView.showsVerticalScrollIndicator = false
+ self.tableView.contentInset.bottom = 70
+
+ self.view.addSubview(tableView)
+ self.tableView.constraintCompletely(to: view)
+ }
+
+ fileprivate func setupToolbar() {
+ largeButton.translatesAutoresizingMaskIntoConstraints = false
+ largeButton.addTarget(self, action: #selector(startSign), for: .touchUpInside)
+
+ let gradientMask = VariableBlurViewConstants.defaultGradientMask
+ variableBlurView = UIVariableBlurView(frame: .zero)
+ variableBlurView?.gradientMask = gradientMask
+ variableBlurView?.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
+ variableBlurView?.translatesAutoresizingMaskIntoConstraints = false
+
+ view.addSubview(variableBlurView!)
+ view.addSubview(largeButton)
+
+ var height = 80.0
+ if UIDevice.current.userInterfaceIdiom == .pad { height = 65.0 }
+
+ NSLayoutConstraint.activate([
+ variableBlurView!.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
+ variableBlurView!.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
+ variableBlurView!.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ variableBlurView!.heightAnchor.constraint(equalToConstant: height),
+
+ largeButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16),
+ largeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16),
+ largeButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -17),
+ largeButton.heightAnchor.constraint(equalToConstant: 50)
+ ])
+
+ variableBlurView?.layer.zPosition = 3
+ largeButton.layer.zPosition = 4
+ }
+
+ internal func certAlert() {
+ if (mainOptions.mainOptions.certificate == nil) {
+ DispatchQueue.main.async {
+ let alert = UIAlertController(
+ title: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_TITLE"),
+ message: String.localized("APP_SIGNING_VIEW_CONTROLLER_NO_CERTS_ALERT_DESCRIPTION"),
+ preferredStyle: .alert
+ )
+ alert.addAction(UIAlertAction(title: String.localized("LAME"), style: .default) { _ in
+ self.dismiss(animated: true)
+ }
+ )
+ self.present(alert, animated: true, completion: nil)
+ }
+ }
+ }
+
+ @objc func closeSheet() {
+ dismiss(animated: true, completion: nil)
+ }
+
+ @objc func fetch() {
+ self.tableView.reloadData()
+ }
+
+ @objc func startSign() {
+ self.navigationItem.leftBarButtonItem = nil
+ largeButton.showLoadingIndicator()
+ signInitialApp(
+ bundle: bundle!,
+ mainOptions: mainOptions,
+ signingOptions: signingDataWrapper,
+ appPath:getFilesForDownloadedApps(app: application as! DownloadedApps, getuuidonly: false))
+ { result in
+ switch result {
+ case .success(let (signedPath, signedApp)):
+ self.appsViewController?.fetchSources() // Changed to 'public' in LibraryViewController
+ self.appsViewController?.tableView.reloadData()
+ Debug.shared.log(message: signedPath.path)
+ if self.signingDataWrapper.signingOptions.installAfterSigned {
+ self.appsViewController?.startInstallProcess(meow: signedApp, filePath: signedPath.path)
+ self.signingCompletionHandler?(true)
+ }
- case .failure(let error):
- Debug.shared.log(message: "Signing failed: \(error.localizedDescription)", type: .error)
- self.signingCompletionHandler?(false)
- }
-
- self.dismiss(animated: true)
- }
- }
+ case .failure(let error):
+ Debug.shared.log(message: "Signing failed: \(error.localizedDescription)", type: .error)
+ self.signingCompletionHandler?(false)
+ }
+
+ self.dismiss(animated: true)
+ }
+ }
}
extension SigningsViewController: UITableViewDataSource, UITableViewDelegate {
- func numberOfSections(in tableView: UITableView) -> Int { return sectionTitles.count }
- func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tableData[section].count }
- func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return sectionTitles[section] }
- func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return sectionTitles[section].isEmpty ? 0 : 40 }
-
- func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
- let title = sectionTitles[section]
- let headerView = InsetGroupedSectionHeader(title: title)
- return headerView
- }
-
- func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
- let reuseIdentifier = "Cell"
- let cell = UITableViewCell(style: .value1, reuseIdentifier: reuseIdentifier)
- cell.accessoryType = .none
- cell.selectionStyle = .gray
-
- let cellText = tableData[indexPath.section][indexPath.row]
- cell.textLabel?.text = cellText
-
- switch cellText {
- case "AppIcon":
- let cell = iconCell
-
- if (mainOptions.mainOptions.iconURL != nil) {
- cell.configure(with: mainOptions.mainOptions.iconURL)
- } else {
- cell.configure(with: CoreDataManager.shared.loadImage(from: getIconURL(for: application as! DownloadedApps)))
- }
-
- cell.accessoryType = .disclosureIndicator
- return cell
- case String.localized("APPS_INFORMATION_TITLE_NAME"):
- cell.textLabel?.text = String.localized("APPS_INFORMATION_TITLE_NAME")
- cell.detailTextLabel?.text = mainOptions.mainOptions.name ?? bundle?.name
- cell.accessoryType = .disclosureIndicator
- case String.localized("APPS_INFORMATION_TITLE_IDENTIFIER"):
- cell.textLabel?.text = String.localized("APPS_INFORMATION_TITLE_IDENTIFIER")
- cell.detailTextLabel?.text = mainOptions.mainOptions.bundleId ?? bundle?.bundleId
- cell.accessoryType = .disclosureIndicator
- case String.localized("APPS_INFORMATION_TITLE_VERSION"):
- cell.detailTextLabel?.text = mainOptions.mainOptions.version ?? bundle?.version
- cell.accessoryType = .disclosureIndicator
- case "Signing":
- if let hasGotCert = mainOptions.mainOptions.certificate {
- let cell = CertificateViewTableViewCell()
- cell.configure(with: hasGotCert, isSelected: false)
- cell.selectionStyle = .none
- return cell
- } else {
- cell.textLabel?.text = String.localized("SETTINGS_VIEW_CONTROLLER_CELL_CURRENT_CERTIFICATE_NOSELECTED")
- cell.textLabel?.textColor = .secondaryLabel
- cell.selectionStyle = .none
- }
- case "Change Certificate", String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_ADD_TWEAKS"), String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_MODIFY_DYLIBS"), String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_PROPERTIES"):
- cell.accessoryType = .disclosureIndicator
- default:
- break
- }
-
-
- return cell
- }
+ func numberOfSections(in tableView: UITableView) -> Int { return sectionTitles.count }
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tableData[section].count }
+ func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return sectionTitles[section] }
+ func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return sectionTitles[section].isEmpty ? 0 : 40 }
+
+ func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
+ let title = sectionTitles[section]
+ let headerView = InsetGroupedSectionHeader(title: title)
+ return headerView
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let reuseIdentifier = "Cell"
+ let cell = UITableViewCell(style: .value1, reuseIdentifier: reuseIdentifier)
+ cell.accessoryType = .none
+ cell.selectionStyle = .gray
+
+ let cellText = tableData[indexPath.section][indexPath.row]
+ cell.textLabel?.text = cellText
+
+ switch cellText {
+ case "AppIcon":
+ let cell = iconCell
+
+ if (mainOptions.mainOptions.iconURL != nil) {
+ cell.configure(with: mainOptions.mainOptions.iconURL)
+ } else {
+ cell.configure(with: CoreDataManager.shared.loadImage(from: getIconURL(for: application as! DownloadedApps)))
+ }
+
+ cell.accessoryType = .disclosureIndicator
+ return cell
+ case String.localized("APPS_INFORMATION_TITLE_NAME"):
+ cell.textLabel?.text = String.localized("APPS_INFORMATION_TITLE_NAME")
+ cell.detailTextLabel?.text = mainOptions.mainOptions.name ?? bundle?.name
+ cell.accessoryType = .disclosureIndicator
+ case String.localized("APPS_INFORMATION_TITLE_IDENTIFIER"):
+ cell.textLabel?.text = String.localized("APPS_INFORMATION_TITLE_IDENTIFIER")
+ cell.detailTextLabel?.text = mainOptions.mainOptions.bundleId ?? bundle?.bundleId
+ cell.accessoryType = .disclosureIndicator
+ case String.localized("APPS_INFORMATION_TITLE_VERSION"):
+ cell.detailTextLabel?.text = mainOptions.mainOptions.version ?? bundle?.version
+ cell.accessoryType = .disclosureIndicator
+ case "Signing":
+ if let hasGotCert = mainOptions.mainOptions.certificate {
+ let cell = CertificateViewTableViewCell()
+ cell.configure(with: hasGotCert, isSelected: false)
+ cell.selectionStyle = .none
+ return cell
+ } else {
+ cell.textLabel?.text = String.localized("SETTINGS_VIEW_CONTROLLER_CELL_CURRENT_CERTIFICATE_NOSELECTED")
+ cell.textLabel?.textColor = .secondaryLabel
+ cell.selectionStyle = .none
+ }
+ case "Change Certificate", String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_ADD_TWEAKS"), String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_MODIFY_DYLIBS"), String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_PROPERTIES"):
+ cell.accessoryType = .disclosureIndicator
+ default:
+ break
+ }
+
+ return cell
+ }
- func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
- let itemTapped = tableData[indexPath.section][indexPath.row]
- switch itemTapped {
- case "AppIcon":
- importAppIconFile()
- case String.localized("APPS_INFORMATION_TITLE_NAME"):
-
- let l = SigningsInputViewController(
- parentView: self,
- initialValue: (mainOptions.mainOptions.name ?? bundle?.name)!,
- valueToSaveTo: indexPath.row
- )
-
- navigationController?.pushViewController(l, animated: true)
- case String.localized("APPS_INFORMATION_TITLE_IDENTIFIER"):
-
- let l = SigningsInputViewController(
- parentView: self,
- initialValue: (mainOptions.mainOptions.bundleId ?? bundle?.bundleId)!,
- valueToSaveTo: indexPath.row
- )
-
- navigationController?.pushViewController(l, animated: true)
- case String.localized("APPS_INFORMATION_TITLE_VERSION"):
-
- let l = SigningsInputViewController(
- parentView: self,
- initialValue: (mainOptions.mainOptions.version ?? bundle?.version)!,
- valueToSaveTo: indexPath.row
- )
-
- navigationController?.pushViewController(l, animated: true)
- case String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_ADD_TWEAKS"):
-
- let l = SigningsTweakViewController(
- signingDataWrapper: signingDataWrapper
- )
-
- navigationController?.pushViewController(l, animated: true)
- case String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_MODIFY_DYLIBS"):
-
- let l = SigningsDylibViewController(
- mainOptions: mainOptions,
- app: getFilesForDownloadedApps(app: application as! DownloadedApps, getuuidonly: false)
- )
-
- navigationController?.pushViewController(l, animated: true)
- case String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_PROPERTIES"):
-
- let l = SigningsAdvancedViewController(
- signingDataWrapper: signingDataWrapper,
- mainOptions: mainOptions
- )
-
- navigationController?.pushViewController(l, animated: true)
-
- default:
- break
- }
-
- tableView.deselectRow(at: indexPath, animated: true)
- }
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let itemTapped = tableData[indexPath.section][indexPath.row]
+ switch itemTapped {
+ case "AppIcon":
+ importAppIconFile()
+ case String.localized("APPS_INFORMATION_TITLE_NAME"):
+
+ let l = SigningsInputViewController(
+ parentView: self,
+ initialValue: (mainOptions.mainOptions.name ?? bundle?.name)!,
+ valueToSaveTo: indexPath.row
+ )
+
+ navigationController?.pushViewController(l, animated: true)
+ case String.localized("APPS_INFORMATION_TITLE_IDENTIFIER"):
+
+ let l = SigningsInputViewController(
+ parentView: self,
+ initialValue: (mainOptions.mainOptions.bundleId ?? bundle?.bundleId)!,
+ valueToSaveTo: indexPath.row
+ )
+
+ navigationController?.pushViewController(l, animated: true)
+ case String.localized("APPS_INFORMATION_TITLE_VERSION"):
+
+ let l = SigningsInputViewController(
+ parentView: self,
+ initialValue: (mainOptions.mainOptions.version ?? bundle?.version)!,
+ valueToSaveTo: indexPath.row
+ )
+
+ navigationController?.pushViewController(l, animated: true)
+ case String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_ADD_TWEAKS"):
+
+ let l = SigningsTweakViewController(
+ signingDataWrapper: signingDataWrapper
+ )
+
+ navigationController?.pushViewController(l, animated: true)
+ case String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_MODIFY_DYLIBS"):
+
+ let l = SigningsDylibViewController(
+ mainOptions: mainOptions,
+ app: getFilesForDownloadedApps(app: application as! DownloadedApps, getuuidonly: false)
+ )
+
+ navigationController?.pushViewController(l, animated: true)
+ case String.localized("APP_SIGNING_VIEW_CONTROLLER_CELL_PROPERTIES"):
+
+ let l = SigningsAdvancedViewController(
+ signingDataWrapper: signingDataWrapper,
+ mainOptions: mainOptions
+ )
+
+ navigationController?.pushViewController(l, animated: true)
+
+ default:
+ break
+ }
+
+ tableView.deselectRow(at: indexPath, animated: true)
+ }
}
// MARK: - this sucks
extension SigningsViewController {
-
- public func getFilesForDownloadedApps(app: DownloadedApps, getuuidonly: Bool) -> URL {
- return CoreDataManager.shared.getFilesForDownloadedApps(for: app, getuuidonly: getuuidonly)
- }
-
- private func getIconURL(for app: DownloadedApps) -> URL? {
- guard let iconURLString = app.value(forKey: "iconURL") as? String,
- let iconURL = URL(string: iconURLString) else {
- return nil
- }
-
- let filesURL = getFilesForDownloadedApps(app: app, getuuidonly: false)
- return filesURL.appendingPathComponent(iconURL.lastPathComponent)
- }
-}
+
+ public func getFilesForDownloadedApps(app: DownloadedApps, getuuidonly: Bool) -> URL {
+ return CoreDataManager.shared.getFilesForDownloadedApps(for: app, getuuidonly: getuuidonly)
+ }
+
+ private func getIconURL(for app: DownloadedApps) -> URL? {
+ guard let iconURLString = app.value(forKey: "iconURL") as? String,
+ let iconURL = URL(string: iconURLString) else {
+ return nil
+ }
+
+ let filesURL = getFilesForDownloadedApps(app: app, getuuidonly: false)
+ return filesURL.appendingPathComponent(iconURL.lastPathComponent)
+ }
+}
\ No newline at end of file
diff --git a/iOS/Views/TabbarView.swift b/iOS/Views/TabbarView.swift
index 1a86f95f..8e8678b1 100644
--- a/iOS/Views/TabbarView.swift
+++ b/iOS/Views/TabbarView.swift
@@ -1,86 +1,86 @@
-//
-// TabbarController.swift
-// feather
-//
-// Created by samara on 5/17/24.
-// Copyright (c) 2024 Samara M (khcrysalis)
-//
-
import SwiftUI
struct TabbarView: View {
- @State private var selectedTab: Tab = Tab(rawValue: UserDefaults.standard.string(forKey: "selectedTab") ?? "sources") ?? .sources
-
- enum Tab: String {
- case sources
- case library
- case settings
- }
+ @State private var selectedTab: Tab = Tab(rawValue: UserDefaults.standard.string(forKey: "selectedTab") ?? "home") ?? .home
+
+ enum Tab: String {
+ case home
+ case sources
+ case library
+ case settings
+ }
- var body: some View {
- TabView(selection: $selectedTab) {
- tab(for: .sources)
- tab(for: .library)
- tab(for: .settings)
- }
- .onChange(of: selectedTab) { newTab in
- // Save the selected tab to UserDefaults
- UserDefaults.standard.set(newTab.rawValue, forKey: "selectedTab")
- // Trigger animation for tab change
- withAnimation(.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0.5)) {
- // No explicit action needed here; the animation will apply to the tab transition
- }
- }
- }
+ var body: some View {
+ TabView(selection: $selectedTab) {
+ tab(for: .home)
+ tab(for: .sources)
+ tab(for: .library)
+ tab(for: .settings)
+ }
+ .onChange(of: selectedTab) { newTab in
+ // Save the selected tab to UserDefaults
+ UserDefaults.standard.set(newTab.rawValue, forKey: "selectedTab")
+ // Trigger animation for tab change
+ withAnimation(.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0.5)) {
+ // No explicit action needed here; the animation will apply to the tab transition
+ }
+ }
+ }
- @ViewBuilder
- func tab(for tab: Tab) -> some View {
- switch tab {
- case .sources:
- NavigationViewController(SourcesViewController.self, title: String.localized("TAB_SOURCES"))
- .edgesIgnoringSafeArea(.all)
- .tabItem {
- Label(
- String.localized("TAB_SOURCES"),
- systemImage: {
- if #available(iOS 16.0, *) {
- return "globe.desk.fill"
- } else {
- return "books.vertical.fill"
- }
- }()
- )
- }
- .tag(Tab.sources)
- case .library:
- NavigationViewController(LibraryViewController.self, title: String.localized("TAB_LIBRARY"))
- .edgesIgnoringSafeArea(.all)
- .tabItem { Label(String.localized("TAB_LIBRARY"), systemImage: "square.grid.2x2.fill") }
- .tag(Tab.library)
- case .settings:
- NavigationViewController(SettingsViewController.self, title: String.localized("TAB_SETTINGS"))
- .edgesIgnoringSafeArea(.all)
- .tabItem { Label(String.localized("TAB_SETTINGS"), systemImage: "gearshape.2.fill") }
- .tag(Tab.settings)
- }
- }
+ @ViewBuilder
+ func tab(for tab: Tab) -> some View {
+ switch tab {
+ case .home:
+ NavigationViewController(HomeViewController.self, title: String.localized("TAB_HOME"))
+ .edgesIgnoringSafeArea(.all)
+ .tabItem {
+ Label(String.localized("TAB_HOME"), systemImage: "house.fill")
+ }
+ .tag(Tab.home)
+ case .sources:
+ NavigationViewController(SourcesViewController.self, title: String.localized("TAB_SOURCES"))
+ .edgesIgnoringSafeArea(.all)
+ .tabItem {
+ Label(
+ String.localized("TAB_SOURCES"),
+ systemImage: {
+ if #available(iOS 16.0, *) {
+ return "globe.desk.fill"
+ } else {
+ return "books.vertical.fill"
+ }
+ }()
+ )
+ }
+ .tag(Tab.sources)
+ case .library:
+ NavigationViewController(LibraryViewController.self, title: String.localized("TAB_LIBRARY"))
+ .edgesIgnoringSafeArea(.all)
+ .tabItem { Label(String.localized("TAB_LIBRARY"), systemImage: "square.grid.2x2.fill") }
+ .tag(Tab.library)
+ case .settings:
+ NavigationViewController(SettingsViewController.self, title: String.localized("TAB_SETTINGS"))
+ .edgesIgnoringSafeArea(.all)
+ .tabItem { Label(String.localized("TAB_SETTINGS"), systemImage: "gearshape.2.fill") }
+ .tag(Tab.settings)
+ }
+ }
}
-
struct NavigationViewController: UIViewControllerRepresentable {
- let content: Content.Type
- let title: String
+ let content: Content.Type
+ let title: String
- init(_ content: Content.Type, title: String) {
- self.content = content
- self.title = title
- }
+ init(_ content: Content.Type, title: String) {
+ self.content = content
+ self.title = title
+ }
- func makeUIViewController(context: Context) -> UINavigationController {
- let viewController = content.init()
- viewController.navigationItem.title = title
- return UINavigationController(rootViewController: viewController)
- }
-
- func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
-}
+ func makeUIViewController(context: Context) -> UINavigationController {
+ let viewController = content.init()
+ viewController.navigationItem.title = title
+ return UINavigationController(rootViewController: viewController)
+ }
+
+ func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {}
+}
\ No newline at end of file