diff --git a/Feather/Backend/Observable/OptionsManager.swift b/Feather/Backend/Observable/OptionsManager.swift index 8a3f96c4..4b47a623 100644 --- a/Feather/Backend/Observable/OptionsManager.swift +++ b/Feather/Backend/Observable/OptionsManager.swift @@ -74,6 +74,8 @@ struct Options: Codable, Equatable { var ppqProtection: Bool /// (Better) protection against PPQ var dynamicProtection: Bool + /// Automatically select certificate that matches target bundle identifier + var autoSelectMatchingCertificate: Bool = false /// App identifiers list which matches and replaces var identifiers: [String: String] /// App name list which matches and replaces @@ -130,6 +132,7 @@ struct Options: Codable, Equatable { ppqString: randomString(), ppqProtection: false, dynamicProtection: false, + autoSelectMatchingCertificate: false, identifiers: [:], displayNames: [:], injectionFiles: [], @@ -155,8 +158,6 @@ struct Options: Codable, Equatable { post_deleteAppAfterSigned: false ) - // MARK: duplicate values are not recommended! - enum AppAppearance: String, Codable, CaseIterable, LocalizedDescribable { case `default` case light = "Light" @@ -233,3 +234,4 @@ extension LocalizedDescribable where Self: RawRepresentable, RawValue == String return localized == self.rawValue ? self.rawValue : localized } } + diff --git a/Feather/Resources/Assets.xcassets/discord.symbolset/Contents.json b/Feather/Resources/Assets.xcassets/discord.symbolset/Contents.json new file mode 100644 index 00000000..24f139da --- /dev/null +++ b/Feather/Resources/Assets.xcassets/discord.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "discord.svg", + "idiom" : "universal" + } + ] +} diff --git a/Feather/Resources/Assets.xcassets/discord.symbolset/discord.svg b/Feather/Resources/Assets.xcassets/discord.symbolset/discord.svg new file mode 100644 index 00000000..baa9398a --- /dev/null +++ b/Feather/Resources/Assets.xcassets/discord.symbolset/discord.svg @@ -0,0 +1,199 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.4.0 + Requires Xcode 14 or greater + Generated from discord + Typeset at 100 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Feather/Resources/Assets.xcassets/github.symbolset/Contents.json b/Feather/Resources/Assets.xcassets/github.symbolset/Contents.json new file mode 100644 index 00000000..c1e81e10 --- /dev/null +++ b/Feather/Resources/Assets.xcassets/github.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "github.svg", + "idiom" : "universal" + } + ] +} diff --git a/Feather/Resources/Assets.xcassets/github.symbolset/github.svg b/Feather/Resources/Assets.xcassets/github.symbolset/github.svg new file mode 100644 index 00000000..9964bc12 --- /dev/null +++ b/Feather/Resources/Assets.xcassets/github.symbolset/github.svg @@ -0,0 +1,167 @@ + + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.4.0 + Requires Xcode 14 or greater + Generated from github.fill + Typeset at 100 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Feather/Resources/Localizable.xcstrings b/Feather/Resources/Localizable.xcstrings index fa6c6e15..2945bf7c 100644 --- a/Feather/Resources/Localizable.xcstrings +++ b/Feather/Resources/Localizable.xcstrings @@ -1504,6 +1504,23 @@ } } }, + "Auto-select Matching Certificate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Auto-select Matching Certificate" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Automatyczny wybór pasującego certyfikatu" + } + } + } + }, "Bad Password" : { "comment" : "Bad certificate password", "extractionState" : "manual", @@ -2541,6 +2558,23 @@ } } }, + "Connect to LocalDevVPN" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connect to LocalDevVPN" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Połącz do LocalDevVPN" + } + } + } + }, "Copy" : { "extractionState" : "manual", "localizations" : { @@ -3622,6 +3656,23 @@ } } }, + "Download LocalDevVPN" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Download LocalDevVPN" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pobierz LocalDevVPN" + } + } + } + }, "Download StosVPN" : { "comment" : "Don't translate StosVPN, this is for linking the download to the required VPN for idevice", "extractionState" : "manual", @@ -6943,6 +6994,12 @@ "value" : "Install After Signing" } }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zainstaluj po podpisaniu" + } + }, "vi" : { "stringUnit" : { "state" : "translated", @@ -7244,10 +7301,16 @@ "value" : "Join Us on Discord" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Hãy tham gia cùng chúng tôi trên Discord!" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dołącz do nas na Discordzie" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hãy tham gia cùng chúng tôi trên Discord!" } } } @@ -8430,10 +8493,16 @@ "value" : "No sources to copy" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Không có nguồn để sao chép" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brak źródeł do skopiowania" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Không có nguồn để sao chép" } } } @@ -12800,10 +12869,16 @@ "value" : "Sources copied to clipboard" } }, - "vi": { - "stringUnit": { - "state": "translated", - "value": "Nguồn đã được sao chép vào bộ nhớ tạm" + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Źródła skopiowane do schowka" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Nguồn đã được sao chép vào bộ nhớ tạm" } } } @@ -14687,6 +14762,23 @@ } } }, + "When enabled, Feather will automatically select the certificate whose application-identifier exactly matches the target bundle identifier." : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "When enabled, Feather will automatically select the certificate whose application-identifier exactly matches the target bundle identifier." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Gdy włączone, Feather automatycznie wybierze certyfikat, którego application-identifier dokładnie odpowiada docelowemu identyfikatorowi pakietu (bundle identifier)." + } + } + } + }, "You cannot update ‘%@‘ with itself, please use an alternative tool to update it." : { "extractionState" : "manual", "localizations" : { @@ -14789,4 +14881,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/Feather/Views/Settings/Archive & Compression/ArchiveView.swift b/Feather/Views/Settings/Archive & Compression/ArchiveView.swift index 35adcecf..9e0ddbdf 100644 --- a/Feather/Views/Settings/Archive & Compression/ArchiveView.swift +++ b/Feather/Views/Settings/Archive & Compression/ArchiveView.swift @@ -18,7 +18,7 @@ struct ArchiveView: View { var body: some View { NBList(.localized("Archive & Compression")) { Section { - Picker(.localized("Compression Level"), systemImage: "archivebox", selection: $_compressionLevel) { + Picker(.localized("Compression Level"), systemImage: "doc.zipper", selection: $_compressionLevel) { ForEach(ZipCompression.allCases, id: \.rawValue) { level in Text(level.label).tag(level) } diff --git a/Feather/Views/Settings/Certificates/CertificatesView.swift b/Feather/Views/Settings/Certificates/CertificatesView.swift index f2c99841..1f61c2c8 100644 --- a/Feather/Views/Settings/Certificates/CertificatesView.swift +++ b/Feather/Views/Settings/Certificates/CertificatesView.swift @@ -14,6 +14,7 @@ struct CertificatesView: View { @State private var _isAddingPresenting = false @State private var _isSelectedInfoPresenting: CertificatePair? + @State private var _searchText: String = "" // MARK: Fetch @FetchRequest( @@ -27,6 +28,22 @@ struct CertificatesView: View { private var _selectedCertBinding: Binding { _bindingSelectedCert ?? $_storedSelectedCert } + + // Filtered certificates based on search + private var _filteredCertificates: [CertificatePair] { + let items = Array(_certificates) + let query = _searchText.trimmingCharacters(in: .whitespacesAndNewlines) + guard !query.isEmpty else { return items } + let q = query.lowercased() + return items.filter { cert in + let nickname = (cert.nickname ?? "").lowercased() + let decoded = Storage.shared.getProvisionFileDecoded(for: cert) + let name = (decoded?.Name ?? "").lowercased() + let appIDName = (decoded?.AppIDName ?? "").lowercased() + let haystack = "\(nickname) \(name) \(appIDName)" + return haystack.contains(q) + } + } init(selectedCert: Binding? = nil) { self._bindingSelectedCert = selectedCert @@ -35,13 +52,17 @@ struct CertificatesView: View { // MARK: Body var body: some View { NBGrid { - ForEach(Array(_certificates.enumerated()), id: \.element.uuid) { index, cert in - _cellButton(for: cert, at: index) - } + ForEach(_filteredCertificates, id: \.uuid) { cert in + if let originalIndex = _originalIndex(for: cert) { + _cellButton(for: cert, originalIndex: originalIndex) + } + } } .navigationTitle(.localized("Certificates")) + .searchable(text: $_searchText, placement: .platform()) + .scrollDismissesKeyboard(.interactively) .overlay { - if _certificates.isEmpty { + if _filteredCertificates.isEmpty { if #available(iOS 17, *) { ContentUnavailableView { Label(.localized("No Certificates"), systemImage: "questionmark.folder.fill") @@ -81,7 +102,7 @@ struct CertificatesView: View { // MARK: - View extension extension CertificatesView { @ViewBuilder - private func _cellButton(for cert: CertificatePair, at index: Int) -> some View { + private func _cellButton(for cert: CertificatePair, originalIndex index: Int) -> some View { Button { _selectedCertBinding.wrappedValue = index } label: { @@ -114,6 +135,10 @@ extension CertificatesView { .buttonStyle(.plain) } + private func _originalIndex(for cert: CertificatePair) -> Int? { + return _certificates.firstIndex(where: { $0.objectID == cert.objectID }) + } + @ViewBuilder private func _actions(for cert: CertificatePair) -> some View { Button(.localized("Delete"), systemImage: "trash", role: .destructive) { diff --git a/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift b/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift index f7c70fa1..1aad1dda 100644 --- a/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift +++ b/Feather/Views/Settings/Installation/Tunnel & Pairing/TunnelView.swift @@ -60,7 +60,7 @@ struct TunnelView: View { UIApplication.open("localdevvpn://enable?scheme=feather") } } else { - Button(.localized("Download LocalDevVPN"), systemImage: "arrow.down.app") { + Button(.localized("Download LocalDevVPN"), systemImage: "link.badge.plus") { UIApplication.open("https://apps.apple.com/us/app/localdevvpn/id6755608044") } } @@ -103,4 +103,4 @@ struct TunnelView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } } -} \ No newline at end of file +} diff --git a/Feather/Views/Settings/SettingsView.swift b/Feather/Views/Settings/SettingsView.swift index 618e5287..87ace0df 100644 --- a/Feather/Views/Settings/SettingsView.swift +++ b/Feather/Views/Settings/SettingsView.swift @@ -46,10 +46,10 @@ struct SettingsView: View { Label(.localized("Signing Options"), systemImage: "signature") } NavigationLink(destination: ArchiveView()) { - Label(.localized("Archive & Compression"), systemImage: "archivebox") + Label(.localized("Archive & Compression"), systemImage: "doc.zipper") } NavigationLink(destination: InstallationView()) { - Label(.localized("Installation"), systemImage: "arrow.down.circle") + Label(.localized("Installation"), systemImage: "plus.app") } } footer: { Text(.localized("Configure the apps way of installing, its zip compression levels, and custom modifications to apps.")) @@ -83,7 +83,7 @@ extension SettingsView { } } - Button(.localized("Submit Feedback"), systemImage: "safari") { + Button(.localized("Submit Feedback"), systemImage: "square.text.square") { let bugAction: UIAlertAction = .init(title: .localized("Bug Report"), style: .default) { _ in UIApplication.open(_makeGitHubIssueURL(url: _githubUrl)) } @@ -98,11 +98,23 @@ extension SettingsView { actions: [bugAction, chooseAction] ) } - Button(.localized("GitHub Repository"), systemImage: "safari") { + Button { UIApplication.open(_githubUrl) + } label: { + Label { + Text(.localized("GitHub Repository")) + } icon: { + Image("github") + } } - Button(.localized("Join Us on Discord"), systemImage: "safari") { + Button { UIApplication.open(_discordServer) + } label: { + Label { + Text(.localized("Join Us on Discord")) + } icon: { + Image("discord") + } } } footer: { Text(.localized("If any issues occur within the app please report it via the GitHub repository. When submitting an issue, make sure to submit detailed information.")) diff --git a/Feather/Views/Signing/Shared/SigningOptionsView.swift b/Feather/Views/Signing/Shared/SigningOptionsView.swift index 90943259..17833a76 100644 --- a/Feather/Views/Signing/Shared/SigningOptionsView.swift +++ b/Feather/Views/Signing/Shared/SigningOptionsView.swift @@ -26,6 +26,17 @@ struct SigningOptionsView: View { } footer: { Text(.localized("Enabling any protection will append a random string to the bundleidentifiers of the apps you sign, this is to ensure your Apple ID does not get flagged by Apple. However, when using a signing service you can ignore this.")) } + + NBSection(.localized("Certificate")) { + _toggle( + .localized("Auto-select Matching Certificate"), + systemImage: "checkmark.shield", + isOn: $options.autoSelectMatchingCertificate, + temporaryValue: temporaryOptions?.autoSelectMatchingCertificate + ) + } footer: { + Text(.localized("When enabled, Feather will automatically select the certificate whose application-identifier exactly matches the target bundle identifier.")) + } } NBSection(.localized("General")) { @@ -122,7 +133,7 @@ struct SigningOptionsView: View { NBSection(.localized("Post Signing")) { _toggle( .localized("Install After Signing"), - systemImage: "arrow.down.circle", + systemImage: "plus.app", isOn: $options.post_installAppAfterSigned, temporaryValue: temporaryOptions?.post_installAppAfterSigned ) diff --git a/Feather/Views/Signing/SigningView.swift b/Feather/Views/Signing/SigningView.swift index 01a123f1..16e701d9 100644 --- a/Feather/Views/Signing/SigningView.swift +++ b/Feather/Views/Signing/SigningView.swift @@ -117,11 +117,22 @@ struct SigningView: View { // ppq protection if _optionsManager.options.ppqProtection, - let identifier = app.identifier, let cert = _selectedCert(), cert.ppQCheck { - _temporaryOptions.appIdentifier = "\(identifier).\(_optionsManager.options.ppqString)" + // Use the current identifier candidate: prefer temporary override if already set + let baseIdentifier = _temporaryOptions.appIdentifier ?? app.identifier + if let baseIdentifier, + !_optionsManager.options.ppqString.isEmpty + { + // Check only the last segment after the final dot + let lastSegment = baseIdentifier.split(separator: ".").last.map(String.init) + if lastSegment != _optionsManager.options.ppqString { + _temporaryOptions.appIdentifier = baseIdentifier + ".\(_optionsManager.options.ppqString)" + } else { + _temporaryOptions.appIdentifier = baseIdentifier + } + } } if @@ -137,6 +148,23 @@ struct SigningView: View { { _temporaryOptions.appName = newName } + + // Auto-select certificate whose application-identifier exactly matches the target bundle id + if _optionsManager.options.autoSelectMatchingCertificate { + if let targetIdentifier = _temporaryOptions.appIdentifier ?? app.identifier { + if let exactIndex = certificates.firstIndex(where: { cert in + guard let ai = Storage.shared.getProvisionFileDecoded(for: cert)?.Entitlements?["application-identifier"]?.value as? String else { return false } + if ai == targetIdentifier { return true } + if let dotIndex = ai.firstIndex(of: ".") { + let remainder = String(ai[ai.index(after: dotIndex)...]) + return remainder == targetIdentifier + } + return false + }) { + _temporaryCertificate = exactIndex + } + } + } } } } @@ -307,3 +335,4 @@ extension SigningView { } } } + diff --git a/README.md b/README.md index b2d89bce..7bf88f0d 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ Due to how it works right now we need both a VPN and a lockdownd pairing file, t - [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. +- [social-symbols](https://github.com/jeremieb/social-symbols/) - Additional logos. ## License diff --git a/license_plist.yml b/license_plist.yml index b2ac4c2a..b6cd8e4a 100644 --- a/license_plist.yml +++ b/license_plist.yml @@ -15,6 +15,11 @@ manual: name: idevice body: |- MIT + + - source: https://github.com/jeremieb/social-symbols + name: social-symbols + file: "Zsign/LICENSE_LC" + - name: "Zsign" file: "Zsign/LICENSE"