forked from shadowfacts/Tusker
Add pinned timeline customization
This commit is contained in:
parent
795146cde4
commit
4dc108f782
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum Timeline: Equatable {
|
||||
public enum Timeline: Equatable, Hashable {
|
||||
case home
|
||||
case `public`(local: Bool)
|
||||
case tag(hashtag: String)
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
D61F759229365C6C00C0B37F /* CollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759129365C6C00C0B37F /* CollectionViewController.swift */; };
|
||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
||||
D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759829384D4D00C0B37F /* FiltersView.swift */; };
|
||||
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759829384D4D00C0B37F /* CustomizeTimelinesView.swift */; };
|
||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759A29384F9C00C0B37F /* FilterMO.swift */; };
|
||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759C2938574B00C0B37F /* FilterRow.swift */; };
|
||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */; };
|
||||
|
@ -186,6 +186,10 @@
|
|||
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC128D65274006341DA /* CustomAlertController.swift */; };
|
||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */; };
|
||||
D6895DE928D962C2006341DA /* TimelineLikeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6895DE828D962C2006341DA /* TimelineLikeController.swift */; };
|
||||
D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */; };
|
||||
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76E229524D2A001DA1B3 /* ListMO.swift */; };
|
||||
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */; };
|
||||
D68A76EA295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */; };
|
||||
D68ACE5D279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */; };
|
||||
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; };
|
||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
|
||||
|
@ -424,7 +428,7 @@
|
|||
D61F759129365C6C00C0B37F /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = "<group>"; };
|
||||
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
|
||||
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
|
||||
D61F759829384D4D00C0B37F /* FiltersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersView.swift; sourceTree = "<group>"; };
|
||||
D61F759829384D4D00C0B37F /* CustomizeTimelinesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeTimelinesView.swift; sourceTree = "<group>"; };
|
||||
D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = "<group>"; };
|
||||
D61F759C2938574B00C0B37F /* FilterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterRow.swift; sourceTree = "<group>"; };
|
||||
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = "<group>"; };
|
||||
|
@ -560,6 +564,10 @@
|
|||
D6895DC128D65274006341DA /* CustomAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlertController.swift; sourceTree = "<group>"; };
|
||||
D6895DC328D65342006341DA /* ConfirmReblogStatusPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReblogStatusPreviewView.swift; sourceTree = "<group>"; };
|
||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLikeController.swift; sourceTree = "<group>"; };
|
||||
D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountPreferences.swift; sourceTree = "<group>"; };
|
||||
D68A76E229524D2A001DA1B3 /* ListMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListMO.swift; sourceTree = "<group>"; };
|
||||
D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTimelinesView.swift; sourceTree = "<group>"; };
|
||||
D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHashtagPinnedTimelineView.swift; sourceTree = "<group>"; };
|
||||
D68ACE5C279B1ABA001CE8EB /* AssetPickerControlCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetPickerControlCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -798,14 +806,16 @@
|
|||
path = "Instance Cell";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D61F759729384D4200C0B37F /* Filters */ = {
|
||||
D61F759729384D4200C0B37F /* Customize Timelines */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D61F759829384D4D00C0B37F /* FiltersView.swift */,
|
||||
D61F759829384D4D00C0B37F /* CustomizeTimelinesView.swift */,
|
||||
D61F759C2938574B00C0B37F /* FilterRow.swift */,
|
||||
D61F75A4293ABD6F00C0B37F /* EditFilterView.swift */,
|
||||
D68A76E729527884001DA1B3 /* PinnedTimelinesView.swift */,
|
||||
D68A76E9295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift */,
|
||||
);
|
||||
path = Filters;
|
||||
path = "Customize Timelines";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D623A53B2635F4E20095BD04 /* Poll */ = {
|
||||
|
@ -895,6 +905,8 @@
|
|||
D61F759A29384F9C00C0B37F /* FilterMO.swift */,
|
||||
D61F75AA293AF11400C0B37F /* FilterKeywordMO.swift */,
|
||||
D6D706A62948D4D0000827ED /* TimlineState.swift */,
|
||||
D68A76E229524D2A001DA1B3 /* ListMO.swift */,
|
||||
D68A76D929511CA6001DA1B3 /* AccountPreferences.swift */,
|
||||
D60E2F2D244248BF005F8713 /* MastodonCachePersistentStore.swift */,
|
||||
D6B936702829F72900237D0E /* NSManagedObjectContext+Helpers.swift */,
|
||||
);
|
||||
|
@ -924,7 +936,7 @@
|
|||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||
D627943C23A5635D00D38C68 /* Explore */,
|
||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||
D61F759729384D4200C0B37F /* Filters */,
|
||||
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||
D641C788213DD86D004B4513 /* Large Image */,
|
||||
D627944B23A9A02400D38C68 /* Lists */,
|
||||
D641C782213DD7F0004B4513 /* Main */,
|
||||
|
@ -1871,6 +1883,7 @@
|
|||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||
D60D2B8223844C71001B87A3 /* BaseStatusTableViewCell.swift in Sources */,
|
||||
D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */,
|
||||
D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */,
|
||||
D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */,
|
||||
D6B9366F2828452F00237D0E /* SavedHashtag.swift in Sources */,
|
||||
|
@ -1979,6 +1992,7 @@
|
|||
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */,
|
||||
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */,
|
||||
D61F75882932DB6000C0B37F /* StatusSwipeAction.swift in Sources */,
|
||||
D68A76EA295285D0001DA1B3 /* AddHashtagPinnedTimelineView.swift in Sources */,
|
||||
D6D12B58292D5B2C00D528E1 /* StatusActionAccountListViewController.swift in Sources */,
|
||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */,
|
||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */,
|
||||
|
@ -2006,7 +2020,7 @@
|
|||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||
D61F759929384D4D00C0B37F /* FiltersView.swift in Sources */,
|
||||
D61F759929384D4D00C0B37F /* CustomizeTimelinesView.swift in Sources */,
|
||||
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||
|
@ -2040,6 +2054,7 @@
|
|||
D6620ACE2511A0ED00312CA0 /* StatusStateResolver.swift in Sources */,
|
||||
D6B9366B281EE77E00237D0E /* PollVoteButton.swift in Sources */,
|
||||
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
|
||||
D68A76DA29511CA6001DA1B3 /* AccountPreferences.swift in Sources */,
|
||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
|
||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
||||
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
|
||||
|
@ -2073,6 +2088,7 @@
|
|||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */,
|
||||
D6B053A423BD2C8100A066FA /* AssetCollectionsListViewController.swift in Sources */,
|
||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||
D6D12B5A292D684600D528E1 /* AccountListViewController.swift in Sources */,
|
||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */,
|
||||
|
|
|
@ -40,6 +40,7 @@ class MastodonController: ObservableObject {
|
|||
|
||||
let instanceURL: URL
|
||||
var accountInfo: LocalData.UserAccountInfo?
|
||||
var accountPreferences: AccountPreferences!
|
||||
|
||||
let client: Client!
|
||||
|
||||
|
@ -154,6 +155,13 @@ class MastodonController: ObservableObject {
|
|||
// are available when Filterers are constructed
|
||||
loadCachedFilters()
|
||||
|
||||
if let existing = try? persistentContainer.viewContext.fetch(AccountPreferences.fetchRequest(account: accountInfo!)).first {
|
||||
accountPreferences = existing
|
||||
} else {
|
||||
accountPreferences = AccountPreferences.default(account: accountInfo!, context: persistentContainer.viewContext)
|
||||
persistentContainer.save(context: persistentContainer.viewContext)
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
async let ownAccount = try getOwnAccount()
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// AccountPreferences.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 12/19/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import Pachyderm
|
||||
|
||||
@objc(AccountPreferences)
|
||||
public final class AccountPreferences: NSManagedObject {
|
||||
|
||||
@nonobjc class func fetchRequest(account: LocalData.UserAccountInfo) -> NSFetchRequest<AccountPreferences> {
|
||||
let req = NSFetchRequest<AccountPreferences>(entityName: "AccountPreferences")
|
||||
req.predicate = NSPredicate(format: "accountID = %@", account.id)
|
||||
return req
|
||||
}
|
||||
|
||||
@NSManaged public var accountID: String
|
||||
@NSManaged var pinnedTimelinesData: Data?
|
||||
|
||||
@LazilyDecoding(from: \AccountPreferences.pinnedTimelinesData, fallback: [])
|
||||
var pinnedTimelines: [Timeline]
|
||||
|
||||
static func `default`(account: LocalData.UserAccountInfo, context: NSManagedObjectContext) -> AccountPreferences {
|
||||
let prefs = AccountPreferences(context: context)
|
||||
prefs.accountID = account.id
|
||||
prefs.pinnedTimelines = [.home, .public(local: true), .public(local: false)]
|
||||
return prefs
|
||||
}
|
||||
|
||||
}
|
|
@ -28,6 +28,10 @@
|
|||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="AccountPreferences" representedClassName="AccountPreferences" syncable="YES">
|
||||
<attribute name="accountID" optional="YES" attributeType="String"/>
|
||||
<attribute name="pinnedTimelinesData" optional="YES" attributeType="Binary"/>
|
||||
</entity>
|
||||
<entity name="Filter" representedClassName="FilterMO" syncable="YES">
|
||||
<attribute name="action" attributeType="String" defaultValueString="warn"/>
|
||||
<attribute name="context" attributeType="String"/>
|
||||
|
@ -121,6 +125,7 @@
|
|||
<configuration name="Cloud" usedWithCloudKit="YES">
|
||||
<memberEntity name="SavedHashtag"/>
|
||||
<memberEntity name="SavedInstance"/>
|
||||
<memberEntity name="AccountPreferences"/>
|
||||
</configuration>
|
||||
<configuration name="Local">
|
||||
<memberEntity name="Account"/>
|
||||
|
|
|
@ -25,18 +25,22 @@ extension Timeline {
|
|||
}
|
||||
}
|
||||
|
||||
var tabBarImage: UIImage? {
|
||||
var image: UIImage {
|
||||
switch self {
|
||||
case .home:
|
||||
return UIImage(systemName: "house.fill")
|
||||
return UIImage(systemName: "house.fill")!
|
||||
case let .public(local):
|
||||
if local {
|
||||
return UIImage(systemName: "person.and.person.fill")
|
||||
return UIImage(systemName: "person.and.person.fill")!
|
||||
} else {
|
||||
return UIImage(systemName: "globe")
|
||||
return UIImage(systemName: "globe")!
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
case .list(id: _):
|
||||
return UIImage(systemName: "list.bullet")!
|
||||
case .tag(hashtag: _):
|
||||
return UIImage(systemName: "number")!
|
||||
case .direct:
|
||||
return UIImage(systemName: "enveloep.fill")!
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// AddHashtagPinnedTimelineView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 12/20/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
|
||||
struct AddHashtagPinnedTimelineView: View {
|
||||
@EnvironmentObject private var mastodonController: MastodonController
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Binding var pinnedTimelines: [Timeline]
|
||||
@StateObject private var viewModel = SearchViewModel()
|
||||
@State private var searchTask: Task<Void, Never>?
|
||||
@State private var isSearching = false
|
||||
@State private var searchResults: [String] = []
|
||||
|
||||
private var savedAndFollowedHashtags: [String] {
|
||||
var tags = Set<String>()
|
||||
let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||
for saved in (try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? [] {
|
||||
tags.insert(saved.name)
|
||||
}
|
||||
for followed in mastodonController.followedHashtags {
|
||||
tags.insert(followed.name)
|
||||
}
|
||||
return Array(tags).sorted(using: SemiCaseSensitiveComparator())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
list
|
||||
.navigationTitle("Search")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.searchable(text: $viewModel.searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: Text("Search for hashtags"))
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
.onReceive(viewModel.$searchQuery, perform: { newValue in
|
||||
isSearching = !newValue.isEmpty
|
||||
})
|
||||
.onReceive(viewModel.$searchQuery.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main), perform: { _ in
|
||||
searchTask?.cancel()
|
||||
searchTask = Task {
|
||||
try? await updateSearchResults()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var list: some View {
|
||||
List {
|
||||
Section {
|
||||
if viewModel.searchQuery.isEmpty {
|
||||
forEachTag(savedAndFollowedHashtags)
|
||||
} else {
|
||||
forEachTag(searchResults)
|
||||
}
|
||||
} header: {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.opacity(isSearching ? 1 : 0)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(EmptyView())
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
}
|
||||
.listStyle(.grouped)
|
||||
}
|
||||
|
||||
private func forEachTag(_ tags: [String]) -> some View {
|
||||
ForEach(tags, id: \.self) { tag in
|
||||
Button {
|
||||
pinnedTimelines.append(.tag(hashtag: tag))
|
||||
dismiss()
|
||||
} label: {
|
||||
Text("#\(tag)")
|
||||
}
|
||||
.tint(.primary)
|
||||
.disabled(pinnedTimelines.contains(.tag(hashtag: tag)))
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSearchResults() async throws {
|
||||
guard !viewModel.searchQuery.isEmpty else {
|
||||
return
|
||||
}
|
||||
isSearching = true
|
||||
let req = Client.search(query: viewModel.searchQuery, types: [.hashtags])
|
||||
let (results, _) = try await mastodonController.run(req)
|
||||
searchResults = results.hashtags.map(\.name)
|
||||
isSearching = false
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchViewModel: ObservableObject {
|
||||
@Published var searchQuery = ""
|
||||
}
|
||||
|
||||
//struct AddHashtagPinnedTimelineView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// AddHashtagPinnedTimelineView()
|
||||
// }
|
||||
//}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// FiltersView.swift
|
||||
// CustomizeTimelinesView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 11/30/22.
|
||||
|
@ -9,18 +9,18 @@
|
|||
import SwiftUI
|
||||
import Pachyderm
|
||||
|
||||
struct FiltersView: View {
|
||||
struct CustomizeTimelinesView: View {
|
||||
let mastodonController: MastodonController
|
||||
|
||||
var body: some View {
|
||||
FiltersList()
|
||||
CustomizeTimelinesList()
|
||||
.environmentObject(mastodonController)
|
||||
.environment(\.managedObjectContext, mastodonController.persistentContainer.viewContext)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct FiltersList: View {
|
||||
struct CustomizeTimelinesList: View {
|
||||
@EnvironmentObject private var mastodonController: MastodonController
|
||||
@ObservedObject private var preferences = Preferences.shared
|
||||
@FetchRequest(sortDescriptors: []) private var filters: FetchedResults<FilterMO>
|
||||
|
@ -50,6 +50,8 @@ struct FiltersList: View {
|
|||
|
||||
private var navigationBody: some View {
|
||||
List {
|
||||
PinnedTimelinesView(accountPreferences: mastodonController.accountPreferences)
|
||||
|
||||
Section {
|
||||
Toggle(isOn: $preferences.hideReblogsInTimelines) {
|
||||
Text("Hide Reblogs")
|
||||
|
@ -62,18 +64,27 @@ struct FiltersList: View {
|
|||
}
|
||||
|
||||
Section {
|
||||
filtersForEach(unexpiredFilters)
|
||||
|
||||
NavigationLink {
|
||||
EditFilterView(filter: EditedFilter(), create: true, originallyExpired: false)
|
||||
} label: {
|
||||
Label("Add Filter", systemImage: "plus")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
} header: {
|
||||
Text("Active Filters")
|
||||
}
|
||||
|
||||
filtersSection(unexpiredFilters, header: Text("Active"))
|
||||
filtersSection(expiredFilters, header: Text("Expired"))
|
||||
if !expiredFilters.isEmpty {
|
||||
Section {
|
||||
filtersForEach(expiredFilters)
|
||||
} header: {
|
||||
Text("Expired Filters")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("Filters"))
|
||||
.navigationTitle(Text("Customize Timelines"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
|
@ -95,30 +106,26 @@ struct FiltersList: View {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func filtersSection(_ filters: [FilterMO], header: some View) -> some View {
|
||||
private func filtersForEach(_ filters: [FilterMO]) -> some View {
|
||||
if !filters.isEmpty {
|
||||
Section {
|
||||
ForEach(filters, id: \.id) { filter in
|
||||
NavigationLink {
|
||||
EditFilterView(filter: EditedFilter(filter), create: false, originallyExpired: filter.expiresAt != nil && filter.expiresAt! <= Date())
|
||||
} label: {
|
||||
FilterRow(filter: filter)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
deleteFilter(filter)
|
||||
} label: {
|
||||
Label("Delete Filter", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
ForEach(filters, id: \.id) { filter in
|
||||
NavigationLink {
|
||||
EditFilterView(filter: EditedFilter(filter), create: false, originallyExpired: filter.expiresAt != nil && filter.expiresAt! <= Date())
|
||||
} label: {
|
||||
FilterRow(filter: filter)
|
||||
}
|
||||
.onDelete { indices in
|
||||
for filter in indices.map({ filters[$0] }) {
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
deleteFilter(filter)
|
||||
} label: {
|
||||
Label("Delete Filter", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
header
|
||||
}
|
||||
.onDelete { indices in
|
||||
for filter in indices.map({ filters[$0] }) {
|
||||
deleteFilter(filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -108,7 +108,6 @@ struct EditFilterView: View {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
Toggle("Expires", isOn: expires)
|
||||
|
||||
if expires.wrappedValue {
|
||||
|
@ -143,7 +142,7 @@ struct EditFilterView: View {
|
|||
Text("Contexts")
|
||||
}
|
||||
}
|
||||
.navigationTitle("Edit Filter")
|
||||
.navigationTitle(create ? "Add Filter" : "Edit Filter")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
|
@ -151,7 +150,7 @@ struct EditFilterView: View {
|
|||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
} else {
|
||||
Button(create ? "Create" : "Save") {
|
||||
Button("Save") {
|
||||
saveFilter()
|
||||
}
|
||||
.disabled(!filter.isValid(for: mastodonController) || (!edited && originallyExpired))
|
|
@ -0,0 +1,130 @@
|
|||
//
|
||||
// PinnedTimelinesView.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 12/20/22.
|
||||
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
|
||||
struct PinnedTimelinesView: View {
|
||||
@EnvironmentObject private var mastodonController: MastodonController
|
||||
@ObservedObject private var accountPreferences: AccountPreferences
|
||||
|
||||
@State private var isShowingAddHashtagSheet = false
|
||||
// store this separately from AccountPreferences in the view, b/c the @LazilyDecoding wrapper breaks animations
|
||||
@State private var pinnedTimelines: [Timeline]
|
||||
|
||||
init(accountPreferences: AccountPreferences) {
|
||||
self.accountPreferences = accountPreferences
|
||||
self.pinnedTimelines = accountPreferences.pinnedTimelines
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section {
|
||||
ForEach(pinnedTimelines, id: \.id) { timeline in
|
||||
HStack {
|
||||
Label {
|
||||
if case .list(id: let id) = timeline,
|
||||
let list = mastodonController.lists.first(where: { $0.id == id }) {
|
||||
Text(list.title)
|
||||
} else if case .tag(hashtag: let tag) = timeline {
|
||||
Text(tag)
|
||||
} else {
|
||||
Text(timeline.title)
|
||||
}
|
||||
} icon: {
|
||||
Image(uiImage: timeline.image.withRenderingMode(.alwaysTemplate))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "line.3.horizontal")
|
||||
.foregroundColor(Color(.lightGray))
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
.onMove { indices, newOffset in
|
||||
pinnedTimelines.move(fromOffsets: indices, toOffset: newOffset)
|
||||
}
|
||||
.onDelete { indices in
|
||||
pinnedTimelines.remove(atOffsets: indices)
|
||||
}
|
||||
|
||||
Menu {
|
||||
ForEach([Timeline.home, .public(local: true), .public(local: false)], id: \.id) { timeline in
|
||||
Button {
|
||||
withAnimation {
|
||||
pinnedTimelines.append(timeline)
|
||||
}
|
||||
} label: {
|
||||
Label {
|
||||
Text(timeline.title)
|
||||
} icon: {
|
||||
Image(uiImage: timeline.image)
|
||||
}
|
||||
}
|
||||
.disabled(pinnedTimelines.contains(timeline))
|
||||
}
|
||||
|
||||
Menu("List…") {
|
||||
ForEach(mastodonController.lists, id: \.id) { list in
|
||||
Button {
|
||||
withAnimation {
|
||||
pinnedTimelines.append(list.timeline)
|
||||
}
|
||||
} label: {
|
||||
Text(list.title)
|
||||
}
|
||||
.disabled(pinnedTimelines.contains(list.timeline))
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
isShowingAddHashtagSheet = true
|
||||
} label: {
|
||||
Label("Hashtag…", systemImage: "number")
|
||||
}
|
||||
} label: {
|
||||
Label("Add…", systemImage: "plus")
|
||||
.padding(.horizontal, 20)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
} header: {
|
||||
Text("Pinned Timelines")
|
||||
}
|
||||
.sheet(isPresented: $isShowingAddHashtagSheet, content: {
|
||||
AddHashtagPinnedTimelineView(pinnedTimelines: $pinnedTimelines)
|
||||
})
|
||||
.onReceive(accountPreferences.publisher(for: \.pinnedTimelinesData)) { _ in
|
||||
if pinnedTimelines != accountPreferences.pinnedTimelines {
|
||||
pinnedTimelines = accountPreferences.pinnedTimelines
|
||||
}
|
||||
}
|
||||
.onChange(of: pinnedTimelines) { newValue in
|
||||
if accountPreferences.pinnedTimelines != newValue {
|
||||
accountPreferences.pinnedTimelines = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Timeline {
|
||||
var id: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return "home"
|
||||
case .public(local: let local):
|
||||
return "public:\(local)"
|
||||
case .list(id: let id):
|
||||
return "list:\(id)"
|
||||
case .tag(hashtag: let tag):
|
||||
return "tag:\(tag)"
|
||||
case .direct:
|
||||
return "direct"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,9 +11,6 @@ import Pachyderm
|
|||
|
||||
class NotificationsPageViewController: SegmentedPageViewController<NotificationsPageViewController.Page> {
|
||||
|
||||
private let notificationsTitle = NSLocalizedString("Notifications", comment: "notifications tab title")
|
||||
private let mentionsTitle = NSLocalizedString("Mentions", comment: "mentions tab title")
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var initialMode: NotificationsMode?
|
||||
|
@ -22,20 +19,14 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
|
|||
self.initialMode = initialMode
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController)
|
||||
notifications.title = notificationsTitle
|
||||
notifications.userActivity = UserActivityManager.checkNotificationsActivity(mode: .allNotifications)
|
||||
super.init(pages: [.all, .mentions]) { page in
|
||||
let vc = NotificationsTableViewController(allowedTypes: page.allowedTypes, mastodonController: mastodonController)
|
||||
vc.title = page.title
|
||||
vc.userActivity = page.userActivity
|
||||
return vc
|
||||
}
|
||||
|
||||
let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController)
|
||||
mentions.title = mentionsTitle
|
||||
mentions.userActivity = UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly)
|
||||
|
||||
super.init(pages: [
|
||||
(.all, notificationsTitle, notifications),
|
||||
(.mentions, mentionsTitle, mentions),
|
||||
])
|
||||
|
||||
title = notificationsTitle
|
||||
title = Page.all.title
|
||||
tabBarItem.image = UIImage(systemName: "bell.fill")
|
||||
}
|
||||
|
||||
|
@ -61,9 +52,40 @@ class NotificationsPageViewController: SegmentedPageViewController<Notifications
|
|||
selectPage(page, animated: false)
|
||||
}
|
||||
|
||||
enum Page {
|
||||
enum Page: SegmentedPageViewControllerPage {
|
||||
case all
|
||||
case mentions
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return NSLocalizedString("Notifications", comment: "notifications tab title")
|
||||
case .mentions:
|
||||
return NSLocalizedString("Mentions", comment: "mentions tab title")
|
||||
}
|
||||
}
|
||||
|
||||
var segmentedControlTitle: String {
|
||||
title
|
||||
}
|
||||
|
||||
var allowedTypes: [Pachyderm.Notification.Kind] {
|
||||
switch self {
|
||||
case .all:
|
||||
return Pachyderm.Notification.Kind.allCases
|
||||
case .mentions:
|
||||
return [.mention]
|
||||
}
|
||||
}
|
||||
|
||||
var userActivity: NSUserActivity {
|
||||
switch self {
|
||||
case .all:
|
||||
return UserActivityManager.checkNotificationsActivity(mode: .allNotifications)
|
||||
case .mentions:
|
||||
return UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import Pachyderm
|
||||
|
||||
class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageViewController.Page> {
|
||||
|
||||
|
@ -17,33 +18,27 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
|||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
private var pinnedTimelinesObservation: NSKeyValueObservation?
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
self.mastodonController = mastodonController
|
||||
|
||||
let home = TimelineViewController(for: .home, mastodonController: mastodonController)
|
||||
home.title = homeTitle
|
||||
home.persistsState = true
|
||||
|
||||
let federated = TimelineViewController(for: .public(local: false), mastodonController: mastodonController)
|
||||
federated.title = federatedTitle
|
||||
federated.persistsState = true
|
||||
|
||||
let local = TimelineViewController(for: .public(local: true), mastodonController: mastodonController)
|
||||
local.title = localTitle
|
||||
local.persistsState = true
|
||||
|
||||
super.init(pages: [
|
||||
(.home, "Home", home),
|
||||
(.local, "Local", local),
|
||||
(.federated, "Federated", federated),
|
||||
])
|
||||
let pages = mastodonController.accountPreferences.pinnedTimelines.map {
|
||||
Page(mastodonController: mastodonController, timeline: $0)
|
||||
}
|
||||
super.init(pages: pages) { page in
|
||||
let vc = TimelineViewController(for: page.timeline, mastodonController: page.mastodonController)
|
||||
vc.title = page.segmentedControlTitle
|
||||
vc.persistsState = true
|
||||
return vc
|
||||
}
|
||||
|
||||
title = homeTitle
|
||||
tabBarItem.image = UIImage(systemName: "house.fill")
|
||||
|
||||
let filtersItem = UIBarButtonItem(image: UIImage(systemName: "line.3.horizontal.decrease.circle"), style: .plain, target: self, action: #selector(filtersPressed))
|
||||
filtersItem.accessibilityLabel = "Filters"
|
||||
navigationItem.leftBarButtonItem = filtersItem
|
||||
let customizeItem = UIBarButtonItem(image: UIImage(systemName: "slider.horizontal.3"), style: .plain, target: self, action: #selector(customizePressed))
|
||||
customizeItem.accessibilityLabel = "Customize Timelines"
|
||||
navigationItem.rightBarButtonItem = customizeItem
|
||||
|
||||
let jumpToPresentName = NSMutableAttributedString("Jump to Present")
|
||||
// otherwise it pronounces it as 'pɹizˈənt'
|
||||
|
@ -51,7 +46,7 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
|||
jumpToPresentName.addAttribute(.accessibilitySpeechIPANotation, value: "ˈprɛ.zənt", range: NSRange(location: "Jump to ".count, length: "Present".count))
|
||||
segmentedControl.accessibilityCustomActions = [
|
||||
UIAccessibilityCustomAction(attributedName: jumpToPresentName, actionHandler: { [unowned self] _ in
|
||||
guard let vc = pageControllers[currentIndex] as? TimelineViewController else {
|
||||
guard let vc = currentViewController as? TimelineViewController else {
|
||||
return false
|
||||
}
|
||||
Task {
|
||||
|
@ -60,42 +55,61 @@ class TimelinesPageViewController: SegmentedPageViewController<TimelinesPageView
|
|||
return true
|
||||
})
|
||||
]
|
||||
|
||||
pinnedTimelinesObservation = mastodonController.accountPreferences.observe(\.pinnedTimelinesData, changeHandler: { [unowned self] _, _ in
|
||||
let pages = self.mastodonController.accountPreferences.pinnedTimelines.map {
|
||||
Page(mastodonController: self.mastodonController, timeline: $0)
|
||||
}
|
||||
self.setPages(pages, animated: false)
|
||||
})
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func selectTimeline(_ timeline: Timeline, animated: Bool) {
|
||||
self.selectPage(Page(mastodonController: mastodonController, timeline: timeline), animated: animated)
|
||||
}
|
||||
|
||||
func stateRestorationActivity() -> NSUserActivity? {
|
||||
return (pageControllers[currentIndex] as! TimelineViewController).stateRestorationActivity()
|
||||
return (currentViewController as! TimelineViewController).stateRestorationActivity()
|
||||
}
|
||||
|
||||
func restoreActivity(_ activity: NSUserActivity) {
|
||||
guard let timeline = UserActivityManager.getTimeline(from: activity) else {
|
||||
return
|
||||
}
|
||||
let page: Page
|
||||
switch timeline {
|
||||
case .home:
|
||||
page = .home
|
||||
case .public(local: false):
|
||||
page = .federated
|
||||
case .public(local: true):
|
||||
page = .local
|
||||
default:
|
||||
return
|
||||
}
|
||||
let page = Page(mastodonController: mastodonController, timeline: timeline)
|
||||
selectPage(page, animated: false)
|
||||
}
|
||||
|
||||
@objc private func filtersPressed() {
|
||||
present(UIHostingController(rootView: FiltersView(mastodonController: mastodonController)), animated: true)
|
||||
}
|
||||
|
||||
enum Page: Hashable {
|
||||
case home
|
||||
case local
|
||||
case federated
|
||||
@objc private func customizePressed() {
|
||||
present(UIHostingController(rootView: CustomizeTimelinesView(mastodonController: mastodonController)), animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TimelinesPageViewController {
|
||||
struct Page: SegmentedPageViewControllerPage {
|
||||
let mastodonController: MastodonController
|
||||
let timeline: Timeline
|
||||
|
||||
static func ==(lhs: Page, rhs: Page) -> Bool {
|
||||
return lhs.timeline == rhs.timeline
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(timeline)
|
||||
}
|
||||
|
||||
var segmentedControlTitle: String {
|
||||
if case let .list(id) = timeline,
|
||||
let list = try? mastodonController.persistentContainer.viewContext.fetch(ListMO.fetchRequest(id: id)).first {
|
||||
return list.title
|
||||
} else {
|
||||
return timeline.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,35 +8,39 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageViewControllerDelegate, TabbedPageViewController {
|
||||
protocol SegmentedPageViewControllerPage: Hashable {
|
||||
var segmentedControlTitle: String { get }
|
||||
}
|
||||
|
||||
let pages: [Page]
|
||||
let pageControllers: [UIViewController]
|
||||
class SegmentedPageViewController<Page: SegmentedPageViewControllerPage>: UIPageViewController, UIPageViewControllerDelegate, TabbedPageViewController {
|
||||
|
||||
private(set) var pages: [Page]!
|
||||
private let pageProvider: (Page) -> UIViewController
|
||||
private var pageControllers = [Page: UIViewController]()
|
||||
|
||||
private var initialPage: Page
|
||||
private var currentPage: Page
|
||||
var currentIndex: Int {
|
||||
pages.firstIndex(of: currentPage)!
|
||||
var currentIndex: Int! {
|
||||
pages.firstIndex(of: currentPage)
|
||||
}
|
||||
var currentViewController: UIViewController {
|
||||
viewControllers!.first!
|
||||
}
|
||||
|
||||
let segmentedControl = ScrollingSegmentedControl<Page>()
|
||||
|
||||
init(pages: [(Page, String, UIViewController)]) {
|
||||
init(pages: [Page], pageProvider: @escaping (Page) -> UIViewController) {
|
||||
precondition(!pages.isEmpty)
|
||||
|
||||
self.pages = pages.map(\.0)
|
||||
self.pageControllers = pages.map(\.2)
|
||||
self.pageProvider = pageProvider
|
||||
|
||||
initialPage = self.pages.first!
|
||||
currentPage = self.pages.first!
|
||||
initialPage = pages.first!
|
||||
currentPage = pages.first!
|
||||
|
||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
|
||||
|
||||
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
|
||||
// before the view has necessarily loaded
|
||||
segmentedControl.options = pages.map {
|
||||
.init(value: $0.0, name: $0.1)
|
||||
}
|
||||
setPages(pages, animated: false)
|
||||
|
||||
segmentedControl.didSelectOption = { [unowned self] option in
|
||||
if let option {
|
||||
self.selectPage(option, animated: true)
|
||||
|
@ -54,6 +58,26 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
|
|||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setPages(_ pages: [Page], animated: Bool) {
|
||||
precondition(!pages.isEmpty)
|
||||
|
||||
self.pages = pages
|
||||
|
||||
if !pages.contains(currentPage) {
|
||||
selectPage(pages.first!, animated: animated)
|
||||
}
|
||||
|
||||
for key in pageControllers.keys where !pages.contains(key) {
|
||||
pageControllers.removeValue(forKey: key)
|
||||
}
|
||||
|
||||
// this needs to happen in init because EnhancedNavigationViewController expects to be able to look at the titleView
|
||||
// before the view has necessarily loaded
|
||||
segmentedControl.options = pages.map {
|
||||
.init(value: $0, name: $0.segmentedControlTitle)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -80,12 +104,23 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
|
|||
initialPage = page
|
||||
return
|
||||
}
|
||||
let prevIndex = currentIndex
|
||||
currentPage = page
|
||||
let index = pages.firstIndex(of: page)!
|
||||
let newController = pageControllers[index]
|
||||
let direction: UIPageViewController.NavigationDirection
|
||||
if let prevIndex = currentIndex {
|
||||
let index = pages.firstIndex(of: page)!
|
||||
direction = index - prevIndex > 0 ? .forward : .reverse
|
||||
} else {
|
||||
direction = .forward
|
||||
}
|
||||
|
||||
currentPage = page
|
||||
let newController: UIViewController
|
||||
if let existing = pageControllers[page] {
|
||||
newController = existing
|
||||
} else {
|
||||
newController = pageProvider(page)
|
||||
pageControllers[page] = newController
|
||||
}
|
||||
|
||||
let direction: UIPageViewController.NavigationDirection = index - prevIndex > 0 ? .forward : .reverse
|
||||
setViewControllers([newController], direction: direction, animated: animated)
|
||||
navigationItem.title = newController.title
|
||||
|
||||
|
@ -108,7 +143,7 @@ class SegmentedPageViewController<Page: Hashable>: UIPageViewController, UIPageV
|
|||
|
||||
extension SegmentedPageViewController: TabBarScrollableViewController {
|
||||
func tabBarScrollToTop() {
|
||||
if let scrollableVC = pageControllers[currentIndex] as? TabBarScrollableViewController {
|
||||
if let scrollableVC = currentViewController as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +151,7 @@ extension SegmentedPageViewController: TabBarScrollableViewController {
|
|||
|
||||
extension SegmentedPageViewController: BackgroundableViewController {
|
||||
func sceneDidEnterBackground() {
|
||||
if let current = pageControllers[currentIndex] as? BackgroundableViewController {
|
||||
if let current = currentViewController as? BackgroundableViewController {
|
||||
current.sceneDidEnterBackground()
|
||||
}
|
||||
}
|
||||
|
@ -124,7 +159,7 @@ extension SegmentedPageViewController: BackgroundableViewController {
|
|||
|
||||
extension SegmentedPageViewController: StatusBarTappableViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
if let current = pageControllers[currentIndex] as? StatusBarTappableViewController {
|
||||
if let current = currentViewController as? StatusBarTappableViewController {
|
||||
return current.handleStatusBarTapped(xPosition: xPosition)
|
||||
}
|
||||
return .continue
|
||||
|
|
|
@ -216,23 +216,11 @@ class UserActivityManager {
|
|||
return
|
||||
}
|
||||
|
||||
switch timeline {
|
||||
case .home, .public(true), .public(false):
|
||||
if mastodonController.accountPreferences.pinnedTimelines.contains(timeline) {
|
||||
navigationController.popToRootViewController(animated: false)
|
||||
let rootController = navigationController.viewControllers.first! as! TimelinesPageViewController
|
||||
let page: TimelinesPageViewController.Page
|
||||
switch timeline {
|
||||
case .home:
|
||||
page = .home
|
||||
case .public(local: false):
|
||||
page = .federated
|
||||
case .public(local: true):
|
||||
page = .local
|
||||
default:
|
||||
fatalError()
|
||||
}
|
||||
rootController.selectPage(page, animated: false)
|
||||
default:
|
||||
rootController.selectTimeline(timeline, animated: false)
|
||||
} else {
|
||||
let timeline = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
navigationController.pushViewController(timeline, animated: false)
|
||||
}
|
||||
|
|
|
@ -89,6 +89,9 @@ class ScrollingSegmentedControl<Value: Hashable>: UIScrollView, UIGestureRecogni
|
|||
label.accessibilityLabel = "\(option.name), \(index + 1) of \(options.count)"
|
||||
optionsStack.addArrangedSubview(label)
|
||||
}
|
||||
|
||||
updateSelectedIndicatorView()
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
func setSelectedOption(_ value: Value, animated: Bool) {
|
||||
|
|
Loading…
Reference in New Issue