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 = ""; 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/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) 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 f43d3c8f3..444df3dda 100644 --- a/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift +++ b/Feather/Views/Sources/Apps/UIKit/SourceAppsTableRepresentableView.swift @@ -23,24 +23,27 @@ 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 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) @@ -82,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) { @@ -107,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) || @@ -120,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 { @@ -140,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 { @@ -154,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 @@ -168,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() @@ -181,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 } } @@ -198,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) @@ -213,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 { @@ -229,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( 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" diff --git a/Zsign b/Zsign index 6049c2bd5..557ddac20 160000 --- a/Zsign +++ b/Zsign @@ -1 +1 @@ -Subproject commit 6049c2bd5ced967c9f043020f52a8822fcee76ef +Subproject commit 557ddac205a7df326a05dc299bd19d6f449e2191 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": [