mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 14:32:28 +00:00
Merge branch 'pr/ios-profile-hold-menu-fixes' into cmp-rewrite
This commit is contained in:
commit
623844ed84
3 changed files with 82 additions and 38 deletions
|
|
@ -424,7 +424,7 @@ fun App(
|
|||
onReplace: ((AppRoute) -> Unit)? = null,
|
||||
onActivate: ((AppScreenTab) -> Unit)? = null,
|
||||
onAppReady: ((Boolean) -> Unit)? = null,
|
||||
onTabTitles: ((home: String, search: String, library: String, profile: String) -> Unit)? = null,
|
||||
onTabTitles: ((home: String, search: String, library: String, profile: String, switchProfile: String, addProfile: String) -> Unit)? = null,
|
||||
nativeProfileSwitcherController: NativeProfileSwitcherController? = null,
|
||||
) {
|
||||
setSingletonImageLoaderFactory { context ->
|
||||
|
|
@ -753,7 +753,7 @@ private fun MainAppContent(
|
|||
onGoBack: (() -> Unit)? = null,
|
||||
onReplace: ((AppRoute) -> Unit)? = null,
|
||||
onActivate: ((AppScreenTab) -> Unit)? = null,
|
||||
onTabTitles: ((home: String, search: String, library: String, profile: String) -> Unit)? = null,
|
||||
onTabTitles: ((home: String, search: String, library: String, profile: String, switchProfile: String, addProfile: String) -> Unit)? = null,
|
||||
nativeProfileSwitcherController: NativeProfileSwitcherController? = null,
|
||||
onRootContentReady: ((Boolean) -> Unit)? = null,
|
||||
onSwitchProfile: () -> Unit = {},
|
||||
|
|
@ -863,6 +863,8 @@ private fun MainAppContent(
|
|||
val nativeTabSearchTitle = stringResource(Res.string.compose_nav_search)
|
||||
val nativeTabLibraryTitle = stringResource(Res.string.compose_nav_library)
|
||||
val nativeTabProfileTitle = stringResource(Res.string.compose_nav_profile)
|
||||
val nativeSwitchProfileTitle = stringResource(Res.string.compose_settings_root_switch_profile_title)
|
||||
val nativeAddProfileTitle = stringResource(Res.string.compose_profile_add_profile)
|
||||
val homescreenSettingsTitle = stringResource(Res.string.compose_settings_page_homescreen)
|
||||
val metaScreenSettingsTitle = stringResource(Res.string.compose_settings_page_meta_screen)
|
||||
val continueWatchingSettingsTitle = stringResource(Res.string.compose_settings_page_continue_watching)
|
||||
|
|
@ -940,6 +942,8 @@ private fun MainAppContent(
|
|||
nativeTabSearchTitle,
|
||||
nativeTabLibraryTitle,
|
||||
nativeTabProfileTitle,
|
||||
nativeSwitchProfileTitle,
|
||||
nativeAddProfileTitle,
|
||||
onTabTitles,
|
||||
) {
|
||||
NativeTabBridge.publishTabTitles(
|
||||
|
|
@ -953,6 +957,8 @@ private fun MainAppContent(
|
|||
nativeTabSearchTitle,
|
||||
nativeTabLibraryTitle,
|
||||
nativeTabProfileTitle,
|
||||
nativeSwitchProfileTitle,
|
||||
nativeAddProfileTitle,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ fun MainViewController(
|
|||
onReplace: (AppRoute) -> Unit,
|
||||
onActivate: (String) -> Unit,
|
||||
onAppReady: (Boolean) -> Unit,
|
||||
onTabTitles: (String, String, String, String) -> Unit,
|
||||
onTabTitles: (String, String, String, String, String, String) -> Unit,
|
||||
nativeProfileSwitcherController: NativeProfileSwitcherController,
|
||||
): UIViewController {
|
||||
val initialTab = AppScreenTab.fromName(initialTabName)
|
||||
|
|
|
|||
|
|
@ -18,14 +18,14 @@ private enum NuvioComposeHost {
|
|||
static func wrap(
|
||||
_ contentController: UIViewController,
|
||||
disablesInteractiveContentPopGesture: Bool = false,
|
||||
onTabBarAvailable: ((UITabBar) -> Void)? = nil
|
||||
onTabBarControllerAvailable: ((UITabBarController) -> Void)? = nil
|
||||
) -> RootComposeViewController {
|
||||
_ = registerPlayerBridge
|
||||
contentController.view.backgroundColor = nuvioBackgroundColor
|
||||
return RootComposeViewController(
|
||||
contentController: contentController,
|
||||
disablesInteractiveContentPopGesture: disablesInteractiveContentPopGesture,
|
||||
onTabBarAvailable: onTabBarAvailable
|
||||
onTabBarControllerAvailable: onTabBarControllerAvailable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -36,16 +36,16 @@ private enum NuvioComposeHost {
|
|||
final class RootComposeViewController: UIViewController {
|
||||
private let contentController: UIViewController
|
||||
private let disablesInteractiveContentPopGesture: Bool
|
||||
private let onTabBarAvailable: ((UITabBar) -> Void)?
|
||||
private let onTabBarControllerAvailable: ((UITabBarController) -> Void)?
|
||||
|
||||
init(
|
||||
contentController: UIViewController,
|
||||
disablesInteractiveContentPopGesture: Bool,
|
||||
onTabBarAvailable: ((UITabBar) -> Void)?
|
||||
onTabBarControllerAvailable: ((UITabBarController) -> Void)?
|
||||
) {
|
||||
self.contentController = contentController
|
||||
self.disablesInteractiveContentPopGesture = disablesInteractiveContentPopGesture
|
||||
self.onTabBarAvailable = onTabBarAvailable
|
||||
self.onTabBarControllerAvailable = onTabBarControllerAvailable
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
|
|
@ -108,8 +108,8 @@ final class RootComposeViewController: UIViewController {
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
configureBackGestures(isVisible: true)
|
||||
if let tabBar = tabBarController?.tabBar {
|
||||
onTabBarAvailable?(tabBar)
|
||||
if let tabBarController {
|
||||
onTabBarControllerAvailable?(tabBarController)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -455,8 +455,9 @@ final class NativeProfileTabInteractionCoordinator: NSObject, UIGestureRecognize
|
|||
var onLongPress: (() -> Void)?
|
||||
private(set) var isHandlingLongPress = false
|
||||
private(set) var suppressesProfileSelection = false
|
||||
private weak var tabBar: UITabBar?
|
||||
private var resetWorkItem: DispatchWorkItem?
|
||||
private weak var tabBarController: UITabBarController?
|
||||
private var selectedIndexBeforeLongPress: Int?
|
||||
private let competingRecognizers = NSHashTable<UIGestureRecognizer>.weakObjects()
|
||||
private lazy var recognizer: UILongPressGestureRecognizer = {
|
||||
let recognizer = UILongPressGestureRecognizer(
|
||||
target: self,
|
||||
|
|
@ -468,11 +469,11 @@ final class NativeProfileTabInteractionCoordinator: NSObject, UIGestureRecognize
|
|||
return recognizer
|
||||
}()
|
||||
|
||||
func attach(to tabBar: UITabBar) {
|
||||
guard self.tabBar !== tabBar else { return }
|
||||
self.tabBar?.removeGestureRecognizer(recognizer)
|
||||
tabBar.addGestureRecognizer(recognizer)
|
||||
self.tabBar = tabBar
|
||||
func attach(to tabBarController: UITabBarController) {
|
||||
guard self.tabBarController !== tabBarController else { return }
|
||||
self.tabBarController?.tabBar.removeGestureRecognizer(recognizer)
|
||||
tabBarController.tabBar.addGestureRecognizer(recognizer)
|
||||
self.tabBarController = tabBarController
|
||||
}
|
||||
|
||||
func gestureRecognizer(
|
||||
|
|
@ -480,10 +481,11 @@ final class NativeProfileTabInteractionCoordinator: NSObject, UIGestureRecognize
|
|||
shouldReceive touch: UITouch
|
||||
) -> Bool {
|
||||
guard gestureRecognizer === recognizer,
|
||||
let tabBar,
|
||||
let tabBar = tabBarController?.tabBar,
|
||||
let profileItem = tabBar.items?.last else {
|
||||
return false
|
||||
}
|
||||
competingRecognizers.removeAllObjects()
|
||||
guard #available(iOS 17.0, *),
|
||||
let profileFrame = profileItem.frame(in: tabBar) else { return false }
|
||||
return profileFrame.contains(touch.location(in: tabBar))
|
||||
|
|
@ -493,24 +495,41 @@ final class NativeProfileTabInteractionCoordinator: NSObject, UIGestureRecognize
|
|||
_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
|
||||
) -> Bool {
|
||||
gestureRecognizer === recognizer || otherGestureRecognizer === recognizer
|
||||
guard gestureRecognizer === recognizer || otherGestureRecognizer === recognizer else {
|
||||
return false
|
||||
}
|
||||
competingRecognizers.add(
|
||||
gestureRecognizer === recognizer ? otherGestureRecognizer : gestureRecognizer
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
@objc private func handleLongPress(_ recognizer: UILongPressGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
resetWorkItem?.cancel()
|
||||
selectedIndexBeforeLongPress = tabBarController?.selectedIndex
|
||||
isHandlingLongPress = true
|
||||
suppressesProfileSelection = true
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
onLongPress?()
|
||||
case .ended, .cancelled, .failed:
|
||||
let workItem = DispatchWorkItem { [weak self] in
|
||||
self?.isHandlingLongPress = false
|
||||
self?.suppressesProfileSelection = false
|
||||
competingRecognizers.allObjects.forEach { competingRecognizer in
|
||||
competingRecognizer.isEnabled = false
|
||||
competingRecognizer.isEnabled = true
|
||||
}
|
||||
if let selectedIndexBeforeLongPress {
|
||||
tabBarController?.selectedIndex = selectedIndexBeforeLongPress
|
||||
}
|
||||
case .ended, .cancelled, .failed:
|
||||
let selectedIndex = selectedIndexBeforeLongPress
|
||||
isHandlingLongPress = false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
if let selectedIndex {
|
||||
self.tabBarController?.selectedIndex = selectedIndex
|
||||
}
|
||||
self.selectedIndexBeforeLongPress = nil
|
||||
self.suppressesProfileSelection = false
|
||||
}
|
||||
resetWorkItem = workItem
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75, execute: workItem)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
@ -523,6 +542,8 @@ final class AppNavigationCoordinator: ObservableObject {
|
|||
@Published var selectedTab: NuvioAppTab = .home
|
||||
@Published private(set) var isAppReady = false
|
||||
@Published private var localizedTabTitles: [NuvioAppTab: String] = [:]
|
||||
@Published private(set) var localizedSwitchProfileTitle = ""
|
||||
@Published private(set) var localizedAddProfileTitle = ""
|
||||
@Published var isProfileSwitcherPresented = false
|
||||
|
||||
let homeCoordinator = TabNavigationCoordinator()
|
||||
|
|
@ -563,13 +584,22 @@ final class AppNavigationCoordinator: ObservableObject {
|
|||
localizedTabTitles[tab] ?? tab.fallbackTitle
|
||||
}
|
||||
|
||||
func updateTabTitles(home: String, search: String, library: String, profile: String) {
|
||||
func updateTabTitles(
|
||||
home: String,
|
||||
search: String,
|
||||
library: String,
|
||||
profile: String,
|
||||
switchProfile: String,
|
||||
addProfile: String
|
||||
) {
|
||||
localizedTabTitles = [
|
||||
.home: home,
|
||||
.search: search,
|
||||
.library: library,
|
||||
.settings: profile,
|
||||
]
|
||||
localizedSwitchProfileTitle = switchProfile
|
||||
localizedAddProfileTitle = addProfile
|
||||
}
|
||||
|
||||
func updateAppReady(_ ready: Bool) {
|
||||
|
|
@ -652,20 +682,22 @@ struct NativeNavComposeView: UIViewControllerRepresentable {
|
|||
onAppReady: { ready in
|
||||
appCoordinator.updateAppReady(ready.boolValue)
|
||||
},
|
||||
onTabTitles: { home, search, library, profile in
|
||||
onTabTitles: { home, search, library, profile, switchProfile, addProfile in
|
||||
appCoordinator.updateTabTitles(
|
||||
home: home,
|
||||
search: search,
|
||||
library: library,
|
||||
profile: profile
|
||||
profile: profile,
|
||||
switchProfile: switchProfile,
|
||||
addProfile: addProfile
|
||||
)
|
||||
},
|
||||
nativeProfileSwitcherController: appCoordinator.profileSwitcherController
|
||||
)
|
||||
return NuvioComposeHost.wrap(
|
||||
controller,
|
||||
onTabBarAvailable: { tabBar in
|
||||
appCoordinator.profileTabInteraction.attach(to: tabBar)
|
||||
onTabBarControllerAvailable: { tabBarController in
|
||||
appCoordinator.profileTabInteraction.attach(to: tabBarController)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -987,7 +1019,7 @@ private struct NativeProfileAvatarView: View {
|
|||
}
|
||||
.clipShape(Circle())
|
||||
.overlay {
|
||||
Circle().stroke(
|
||||
Circle().strokeBorder(
|
||||
Color(uiColor: profile.avatarColor).opacity(profile.active ? 1 : 0.45),
|
||||
lineWidth: profile.active ? 2.5 : 1.5
|
||||
)
|
||||
|
|
@ -1005,21 +1037,27 @@ private struct NativeProfileAvatarView: View {
|
|||
private struct NativeProfileSwitcherView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@StateObject private var model: NativeProfileSwitcherViewModel
|
||||
let title: String
|
||||
let addProfileTitle: String
|
||||
let onManageProfiles: () -> Void
|
||||
|
||||
init(
|
||||
controller: NativeProfileSwitcherController,
|
||||
title: String,
|
||||
addProfileTitle: String,
|
||||
onManageProfiles: @escaping () -> Void
|
||||
) {
|
||||
_model = StateObject(
|
||||
wrappedValue: NativeProfileSwitcherViewModel(controller: controller)
|
||||
)
|
||||
self.title = title
|
||||
self.addProfileTitle = addProfileTitle
|
||||
self.onManageProfiles = onManageProfiles
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
Text("Switch Profile")
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
|
||||
if model.isLoaded {
|
||||
|
|
@ -1061,8 +1099,9 @@ private struct NativeProfileSwitcherView: View {
|
|||
.font(.system(size: 19, weight: .semibold))
|
||||
.frame(width: 52, height: 52)
|
||||
.background(.secondary.opacity(0.12), in: Circle())
|
||||
Text("Add")
|
||||
Text(addProfileTitle)
|
||||
.font(.caption)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(width: 64)
|
||||
}
|
||||
}
|
||||
|
|
@ -1142,10 +1181,7 @@ struct NativeNavContentView: View {
|
|||
get: { appCoordinator.selectedTab },
|
||||
set: { newTab in
|
||||
if newTab == .settings &&
|
||||
(
|
||||
appCoordinator.profileTabInteraction.suppressesProfileSelection ||
|
||||
appCoordinator.isProfileSwitcherPresented
|
||||
) {
|
||||
appCoordinator.profileTabInteraction.suppressesProfileSelection {
|
||||
return
|
||||
}
|
||||
if newTab == appCoordinator.selectedTab {
|
||||
|
|
@ -1227,6 +1263,8 @@ struct NativeNavContentView: View {
|
|||
) {
|
||||
NativeProfileSwitcherView(
|
||||
controller: appCoordinator.profileSwitcherController,
|
||||
title: appCoordinator.localizedSwitchProfileTitle,
|
||||
addProfileTitle: appCoordinator.localizedAddProfileTitle,
|
||||
onManageProfiles: appCoordinator.openProfileManagement
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue