From e464aa549eb1779c4ab40692b228b85e4011bc5e Mon Sep 17 00:00:00 2001 From: khcrysalis Date: Thu, 26 Jun 2025 14:43:21 -0700 Subject: [PATCH 1/8] Update app-repo.json --- app-repo.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app-repo.json b/app-repo.json index 7866d9260..f5ffd1abb 100644 --- a/app-repo.json +++ b/app-repo.json @@ -14,10 +14,10 @@ "tintColor": "848ef9", "versions": [ { - "version": "2.1.1", - "date": "2025-06-16T21:36:24Z", + "version": "2.2.0", + "date": "2025-06-26T21:36:24Z", "size": 12598584, - "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v2.1.1/Feather.ipa" + "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v2.2.0/Feather.ipa" } ], "appPermissions": {}, @@ -27,10 +27,10 @@ "https://github.com/khcrysalis/Feather/blob/v1/Images/Sign.png?raw=true", "https://github.com/khcrysalis/Feather/blob/v1/Images/Store.png?raw=true" ], - "version": "2.1.1", - "versionDate": "2025-06-16T21:36:24Z", + "version": "2.2.0", + "versionDate": "2025-06-26T21:36:24Z", "size": 12598584, - "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v2.1.1/Feather.ipa" + "downloadURL": "https://github.com/khcrysalis/Feather/releases/download/v2.2.0/Feather.ipa" } ], "news": [ From 16846eec8698329b3c39035f21c247849c64d697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Thu, 26 Jun 2025 15:20:18 -0700 Subject: [PATCH 2/8] Feather(Views): fix sources navigation bar from bugging --- .../SourceAppsTableRepresentableView.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift index f43d3c8f3..a57dfcdca 100644 --- a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift +++ b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift @@ -27,20 +27,18 @@ struct SourceAppsTableRepresentableView: UIViewRepresentable { let header = UIHostingController(rootView: SourceNewsView(news: firstSource.news)) header.view.translatesAutoresizingMaskIntoConstraints = true header.view.backgroundColor = .clear - let targetSize = header.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) - header.view.frame = CGRect(origin: .zero, size: targetSize) - tableView.tableHeaderView = header.view + let fixedHeight: CGFloat = 161 + let width = tableView.bounds.width + header.view.frame = CGRect(origin: .zero, size: CGSize(width: width, height: fixedHeight)) + + DispatchQueue.main.async { + tableView.tableHeaderView = header.view + } } tableView.alpha = 0 - if let firstSource = sources.first, sources.count == 1 { - let header = UIHostingController(rootView: SourceNewsView(news: firstSource.news)) - header.view.translatesAutoresizingMaskIntoConstraints = true - header.view.backgroundColor = .clear - } - - UIView.transition(with: tableView, duration: 0.3, options: [.transitionCrossDissolve], animations: { + UIView.transition(with: tableView, duration: 0.5, options: [.transitionCrossDissolve], animations: { tableView.alpha = 1 }, completion: nil) From addc37c05cf144948263f61a116d67c2d5de4aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Thu, 26 Jun 2025 15:39:17 -0700 Subject: [PATCH 3/8] fix: makefile should fix permissions --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index c4ab111a0..bb79be462 100755 --- a/Makefile +++ b/Makefile @@ -42,6 +42,9 @@ $(SCHEMES): deps mv "$(APP)/$@.app" "$(STAGE)/Payload/$@.app" + chmod -R 0755 "$(STAGE)/Payload/$@.app" + codesign --force --sign - --timestamp=none "$(STAGE)/Payload/$@.app" + cp deps/* "$(STAGE)/Payload/$@.app/" || true rm -rf "$(STAGE)/Payload/$@.app/_CodeSignature" From cb21feaa5aae5c60d23d29f0f6da8a421c0c1f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Thu, 26 Jun 2025 15:49:24 -0700 Subject: [PATCH 4/8] Feather(Sources): Fix more issues with the source header --- .../Apps/UIKit/SourceAppsTableRepresentableView.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift index a57dfcdca..1f1700b6d 100644 --- a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift +++ b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift @@ -23,8 +23,13 @@ struct SourceAppsTableRepresentableView: UIViewRepresentable { tableView.register(UITableViewHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.allowsSelection = false - if let firstSource = sources.first, sources.count == 1 { - let header = UIHostingController(rootView: SourceNewsView(news: firstSource.news)) + if + let firstSource = sources.first, + sources.count == 1, + let news = firstSource.news, + !news.isEmpty + { + let header = UIHostingController(rootView: SourceNewsView(news: news)) header.view.translatesAutoresizingMaskIntoConstraints = true header.view.backgroundColor = .clear let fixedHeight: CGFloat = 161 From f1c0bf1ac97ac8fa392adba8e230753564a34d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Thu, 26 Jun 2025 17:21:26 -0700 Subject: [PATCH 5/8] Feather(Views): Fix bug regarding sharing and same bundle id --- Feather/Views/Library/Install/InstallPreviewView.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Feather/Views/Library/Install/InstallPreviewView.swift b/Feather/Views/Library/Install/InstallPreviewView.swift index e95bca350..c463544d0 100644 --- a/Feather/Views/Library/Install/InstallPreviewView.swift +++ b/Feather/Views/Library/Install/InstallPreviewView.swift @@ -91,9 +91,7 @@ struct InstallPreviewView: View { } private func _install() { - guard - app.identifier != Bundle.main.bundleIdentifier! || _installationMethod == 1 - else { + guard isSharing || app.identifier != Bundle.main.bundleIdentifier! || _installationMethod == 1 else { UIAlertController.showAlertWithOk( title: .localized("Install"), message: .localized("You cannot update ‘%@‘ with itself, please use an alternative tool to update it.", arguments: Bundle.main.name) From e12a971c8af08b378e05f53a844fa890fe7b9bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Sat, 28 Jun 2025 11:37:04 -0700 Subject: [PATCH 6/8] Zsign: fix unable to verify integrity for most apps --- Feather/FeatherApp.swift | 2 +- Zsign | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Feather/FeatherApp.swift b/Feather/FeatherApp.swift index c919c48e7..9f0816d70 100644 --- a/Feather/FeatherApp.swift +++ b/Feather/FeatherApp.swift @@ -50,7 +50,7 @@ struct FeatherApp: App { private func _handleURL(_ url: URL) { if url.scheme == "feather" { - /// feather://import-certificate?p12=&mobileprovision=&password= + /// feather://import-certificate?p12=&mobileprovision=&password= if url.host == "import-certificate" { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), diff --git a/Zsign b/Zsign index 6049c2bd5..557ddac20 160000 --- a/Zsign +++ b/Zsign @@ -1 +1 @@ -Subproject commit 6049c2bd5ced967c9f043020f52a8822fcee76ef +Subproject commit 557ddac205a7df326a05dc299bd19d6f449e2191 From 49630b6cb5a37f62855b535ace84a502362aae03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Sat, 28 Jun 2025 15:41:16 -0700 Subject: [PATCH 7/8] Feather(Views): Save sorting options --- .../Views/Sources/Apps/SourceAppsView.swift | 46 ++++++++------ .../SourceAppsTableRepresentableView.swift | 62 ++++++++++--------- 2 files changed, 61 insertions(+), 47 deletions(-) diff --git a/Feather/Views/Sources/Apps/SourceAppsView.swift b/Feather/Views/Sources/Apps/SourceAppsView.swift index 0fb48bfff..a4669ec09 100644 --- a/Feather/Views/Sources/Apps/SourceAppsView.swift +++ b/Feather/Views/Sources/Apps/SourceAppsView.swift @@ -10,20 +10,38 @@ import AltSourceKit import NimbleViews import UIKit +// MARK: - Extension: View (Enil) +extension SourceAppsView { + enum SortOption: String, CaseIterable { + case `default` = "default" + case name + case date + + var displayName: String { + switch self { + case .default: .localized("Default") + case .name: .localized("Name") + case .date: .localized("Date") + } + } + } +} + // MARK: - View struct SourceAppsView: View { + @AppStorage("Feather.sortOptionRawValue") private var _sortOptionRawValue: String = SortOption.default.rawValue + @AppStorage("Feather.sortAscending") private var _sortAscending: Bool = true + @State private var _sortOption: SortOption = .default + @State var isLoading = true @State var hasLoadedOnce = false - @State private var _searchText = "" - @State private var _sortOption: SortOption = .default - @State private var _sortAscending = true - + private var _navigationTitle: String { if object.count == 1 { - return object[0].name ?? .localized("Unknown") + object[0].name ?? .localized("Unknown") } else { - return .localized("%lld Sources", arguments: object.count) + .localized("%lld Sources", arguments: object.count) } } @@ -92,10 +110,14 @@ struct SourceAppsView: View { _load() hasLoadedOnce = true } + _sortOption = SortOption(rawValue: _sortOptionRawValue) ?? .default } .onChange(of: viewModel.isFinished) { _ in _load() } + .onChange(of: _sortOption) { newValue in + _sortOptionRawValue = newValue.rawValue + } } private func _load() { @@ -140,16 +162,4 @@ extension SourceAppsView { } } } - - enum SortOption: CaseIterable { - case `default`, name, date - - var displayName: String { - switch self { - case .default: return .localized("Default") - case .name: return .localized("Name") - case .date: return .localized("Date") - } - } - } } diff --git a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift index 1f1700b6d..444df3dda 100644 --- a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift +++ b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift @@ -85,23 +85,23 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl var sortOption: SourceAppsView.SortOption var sortAscending: Bool - private var groupedAppsByNameFirstLetter: [String: [(source: ASRepository, app: ASRepository.App)]] = [:] - private var groupedAppsByDate: [String: [(source: ASRepository, app: ASRepository.App)]] = [:] - private var sortedSectionTitles: [String] = [] + private var _groupedAppsByNameFirstLetter: [String: [(source: ASRepository, app: ASRepository.App)]] = [:] + private var _groupedAppsByDate: [String: [(source: ASRepository, app: ASRepository.App)]] = [:] + private var _sortedSectionTitles: [String] = [] - private var cachedSortedApps: [(source: ASRepository, app: ASRepository.App)] = [] + private var _cachedSortedApps: [(source: ASRepository, app: ASRepository.App)] = [] weak var uiTableView: UITableView? - private var allAppsWithSource: [(source: ASRepository, app: ASRepository.App)] { + private var _allAppsWithSource: [(source: ASRepository, app: ASRepository.App)] { sources.flatMap { source in source.apps.map { (source: source, app: $0) } } } private var _sortedApps: [(source: ASRepository, app: ASRepository.App)] { - if !cachedSortedApps.isEmpty { - return cachedSortedApps + if !_cachedSortedApps.isEmpty { + return _cachedSortedApps } - cachedSortedApps = calculateSortedApps() - return cachedSortedApps + _cachedSortedApps = _calculateSortedApps() + return _cachedSortedApps } init(sources: [ASRepository], searchText: String, sortOption: SourceAppsView.SortOption, sortAscending: Bool) { @@ -110,10 +110,14 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl self.sortOption = sortOption self.sortAscending = sortAscending super.init() + + if sortOption != .default { + invalidateCache() + } } - private func calculateSortedApps() -> [(source: ASRepository, app: ASRepository.App)] { - let filtered = allAppsWithSource.filter { + private func _calculateSortedApps() -> [(source: ASRepository, app: ASRepository.App)] { + let filtered = _allAppsWithSource.filter { searchText.isEmpty || ($0.app.name?.localizedCaseInsensitiveContains(searchText) ?? false) || ($0.app.description?.localizedCaseInsensitiveContains(searchText) ?? false) || @@ -123,9 +127,9 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl switch sortOption { case .default: - groupedAppsByDate = [:] - groupedAppsByNameFirstLetter = [:] - sortedSectionTitles = [] + _groupedAppsByDate = [:] + _groupedAppsByNameFirstLetter = [:] + _sortedSectionTitles = [] return sortAscending ? filtered : filtered.reversed() case .date: let sorted = filtered.sorted { @@ -143,12 +147,12 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl let sortedDates = grouped.keys.sorted(by: { sortAscending ? $0 > $1 : $0 < $1 }) - groupedAppsByDate = grouped.reduce(into: [:]) { result, pair in + _groupedAppsByDate = grouped.reduce(into: [:]) { result, pair in let key = formatter.string(from: pair.key) result[key] = pair.value } - sortedSectionTitles = sortedDates.map { formatter.string(from: $0) } + _sortedSectionTitles = sortedDates.map { formatter.string(from: $0) } return sorted case .name: let sorted = filtered.sorted { @@ -157,11 +161,11 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl let comparison = n1.localizedCaseInsensitiveCompare(n2) == .orderedAscending return sortAscending ? comparison : !comparison } - groupedAppsByNameFirstLetter = Dictionary(grouping: sorted) { + _groupedAppsByNameFirstLetter = Dictionary(grouping: sorted) { let first = $0.app.name?.trimmingCharacters(in: .whitespacesAndNewlines).first?.uppercased() ?? "#" return first.range(of: "[A-Z]", options: .regularExpression) != nil ? first : "#" } - sortedSectionTitles = groupedAppsByNameFirstLetter.keys.sorted(by: { + _sortedSectionTitles = _groupedAppsByNameFirstLetter.keys.sorted(by: { if $0 == "#" { return false } if $1 == "#" { return true } return sortAscending ? $0 < $1 : $0 > $1 @@ -171,7 +175,7 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl } func invalidateCache() { - cachedSortedApps = calculateSortedApps() + _cachedSortedApps = _calculateSortedApps() if let tableView = uiTableView { UIView.transition(with: tableView, duration: 0.3, options: [.transitionCrossDissolve], animations: { tableView.reloadData() @@ -184,15 +188,15 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl func numberOfSections(in tableView: UITableView) -> Int { switch sortOption { case .default: 1 - case .name, .date: sortedSectionTitles.count + case .name, .date: _sortedSectionTitles.count } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch sortOption { case .default: _sortedApps.count - case .name: groupedAppsByNameFirstLetter[sortedSectionTitles[section]]?.count ?? 0 - case .date: groupedAppsByDate[sortedSectionTitles[section]]?.count ?? 0 + case .name: _groupedAppsByNameFirstLetter[_sortedSectionTitles[section]]?.count ?? 0 + case .date: _groupedAppsByDate[_sortedSectionTitles[section]]?.count ?? 0 } } @@ -201,8 +205,8 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl let entry: (source: ASRepository, app: ASRepository.App) switch sortOption { case .default: entry = _sortedApps[indexPath.row] - case .name: entry = groupedAppsByNameFirstLetter[sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] - case .date: entry = groupedAppsByDate[sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] + case .name: entry = _groupedAppsByNameFirstLetter[_sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] + case .date: entry = _groupedAppsByDate[_sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] } cell.contentConfiguration = UIHostingConfiguration { SourceAppsCellView(source: entry.source, app: entry.app) @@ -216,7 +220,7 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl switch sortOption { case .default: title = .localized("%lld Apps", arguments: _sortedApps.count) - case .name, .date: title = sortedSectionTitles[section] + case .name, .date: title = _sortedSectionTitles[section] } headerView?.contentConfiguration = UIHostingConfiguration { @@ -232,19 +236,19 @@ extension SourceAppsTableRepresentableView { class Coordinator: NSObject, UITabl } func sectionIndexTitles(for tableView: UITableView) -> [String]? { - sortOption == .name ? sortedSectionTitles : nil + sortOption == .name ? _sortedSectionTitles : nil } func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { - sortedSectionTitles.firstIndex(of: title) ?? 0 + _sortedSectionTitles.firstIndex(of: title) ?? 0 } func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let entry: (source: ASRepository, app: ASRepository.App) switch sortOption { case .default: entry = _sortedApps[indexPath.row] - case .name: entry = groupedAppsByNameFirstLetter[sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] - case .date: entry = groupedAppsByDate[sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] + case .name: entry = _groupedAppsByNameFirstLetter[_sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] + case .date: entry = _groupedAppsByDate[_sortedSectionTitles[indexPath.section]]?[indexPath.row] ?? _sortedApps[indexPath.row] } return UIContextMenuConfiguration( From feb38ca25fb6a34fd43fbf44f639af3bb2967e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=91=92?= Date: Sat, 28 Jun 2025 18:33:35 -0700 Subject: [PATCH 8/8] bump to v2.2.1 --- Feather.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Feather.xcodeproj/project.pbxproj b/Feather.xcodeproj/project.pbxproj index 9e4f2f4e9..0f526dad3 100644 --- a/Feather.xcodeproj/project.pbxproj +++ b/Feather.xcodeproj/project.pbxproj @@ -482,7 +482,7 @@ "$(PROJECT_DIR)/Feather/Resources/idevice", ); MACOSX_DEPLOYMENT_TARGET = 15.2; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = thewonderofyou.Feather; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -546,7 +546,7 @@ "$(PROJECT_DIR)/Feather/Resources/idevice", ); MACOSX_DEPLOYMENT_TARGET = 15.2; - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 2.2.1; PRODUCT_BUNDLE_IDENTIFIER = thewonderofyou.Feather; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "";