diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt index ebdba5e9d..ffd470e7b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/App.kt @@ -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, ) } diff --git a/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt b/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt index d38655a54..aa0a390be 100644 --- a/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/com/nuvio/app/MainViewController.kt @@ -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) diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index feac50ee9..68fd9e2df 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -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.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 ) }