diff --git a/OpenTalkie/AppShell.xaml.cs b/OpenTalkie/AppShell.xaml.cs index 1523e15..e4c10d5 100644 --- a/OpenTalkie/AppShell.xaml.cs +++ b/OpenTalkie/AppShell.xaml.cs @@ -18,5 +18,6 @@ public AppShell(IPlatformCapabilitiesService platformCapabilitiesService) Routing.RegisterRoute("PlaybackSettingsPage", typeof(PlaybackSettingsPage)); Routing.RegisterRoute("ReceiverSettingsPage", typeof(ReceiverSettingsPage)); Routing.RegisterRoute("AudioManagerSettingsPage", typeof(AudioManagerSettingsPage)); + Routing.RegisterRoute("GeneralSettingsPage", typeof(GeneralSettingsPage)); } } diff --git a/OpenTalkie/DependencyInjection/PresentationCoreServiceCollectionExtensions.cs b/OpenTalkie/DependencyInjection/PresentationCoreServiceCollectionExtensions.cs index d63ca82..49324be 100644 --- a/OpenTalkie/DependencyInjection/PresentationCoreServiceCollectionExtensions.cs +++ b/OpenTalkie/DependencyInjection/PresentationCoreServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ public static IServiceCollection AddPresentationCoreLayer(this IServiceCollectio services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/OpenTalkie/DependencyInjection/PresentationServiceCollectionExtensions.cs b/OpenTalkie/DependencyInjection/PresentationServiceCollectionExtensions.cs index 673e407..d5793c5 100644 --- a/OpenTalkie/DependencyInjection/PresentationServiceCollectionExtensions.cs +++ b/OpenTalkie/DependencyInjection/PresentationServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ public static IServiceCollection AddPresentationLayer(this IServiceCollection se services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services; } diff --git a/OpenTalkie/OpenTalkie.csproj b/OpenTalkie/OpenTalkie.csproj index f98d578..408efd7 100644 --- a/OpenTalkie/OpenTalkie.csproj +++ b/OpenTalkie/OpenTalkie.csproj @@ -87,6 +87,9 @@ + + GeneralSettingsPage.xaml + HomePage.xaml @@ -97,4 +100,9 @@ + + + MSBuild:Compile + + diff --git a/OpenTalkie/Platforms/Android/MainActivity.cs b/OpenTalkie/Platforms/Android/MainActivity.cs index b2adfd0..29f215d 100644 --- a/OpenTalkie/Platforms/Android/MainActivity.cs +++ b/OpenTalkie/Platforms/Android/MainActivity.cs @@ -2,6 +2,7 @@ using Android.Content; using Android.Content.PM; using Android.OS; +using Android.Views; namespace OpenTalkie.Platforms.Android; @@ -12,13 +13,31 @@ public class MainActivity : MauiAppCompatActivity { public static MainActivity? Instance { get; private set; } + // Instantiate swipe detector + private SwipeNavigationDetector _swipeDetector = null!; + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); Instance = this; + + _swipeDetector = new SwipeNavigationDetector(); } + // Intercept touch data at the root window level + public override bool DispatchTouchEvent(MotionEvent? ev) + { + // Hand the touch data off to our helper file. + // If it returns true, it means a swipe happened and we swallow it. + if (_swipeDetector != null && _swipeDetector.HandleTouchEvent(ev)) + { + return true; + } + + // Otherwise, let normal clicks and scrolls pass through to MAUI untouched + return base.DispatchTouchEvent(ev); + } protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) { diff --git a/OpenTalkie/Platforms/Android/SwipeNavigation.cs b/OpenTalkie/Platforms/Android/SwipeNavigation.cs new file mode 100644 index 0000000..4ddf991 --- /dev/null +++ b/OpenTalkie/Platforms/Android/SwipeNavigation.cs @@ -0,0 +1,85 @@ +using Android.Views; +using Microsoft.Maui.Storage; +using OpenTalkie.Presentation.ViewModels; + +namespace OpenTalkie.Platforms.Android; + +public class SwipeNavigationDetector +{ + private const int SwipeThreshold = 80; + private float _startX; + private float _startY; + + // Processes incoming window touches to check for swipe gestures. + public bool HandleTouchEvent(MotionEvent? ev) + { + // Check enabled state by referencing GeneralSettingsViewModel directly + bool isEnabled = Preferences.Default.Get( + GeneralSettingsViewModel.SwipeNavPreferenceKey, + GeneralSettingsViewModel.DefaultSwipeNavEnabled + ); + + if (!isEnabled) + { + return false; + } + + if (ev is null) return false; + + switch (ev.Action) + { + case MotionEventActions.Down: + _startX = ev.RawX; + _startY = ev.RawY; + break; + + case MotionEventActions.Up: + var deltaX = ev.RawX - _startX; + var deltaY = ev.RawY - _startY; + + // Ensure horizontal movement is greater than the threshold and flatter than a vertical scroll + if (Math.Abs(deltaX) > SwipeThreshold && Math.Abs(deltaX) > Math.Abs(deltaY) * 1.2f) + { + // Run on main thread to safely update UI navigation + MainThread.BeginInvokeOnMainThread(() => TriggerTabNavigation(deltaX < 0 ? 1 : -1)); + return true; + } + break; + } + + return false; + } + + + // Calculates current shell tab index dynamically and shifts navigation. + // Works perfectly on root pages and sub-pages alike. + private void TriggerTabNavigation(int direction) + { + var shell = Shell.Current; + if (shell is null) return; + + // Optional UX Guardrail: Uncomment the line below if you don't want + // swiping to switch tabs when a sub-page has a "Go Back" backstack arrow. + // if (shell.Navigation.NavigationStack.Count > 1) return; + + var tabBar = shell.CurrentItem; + if (tabBar is null) return; + + var currentTab = tabBar.CurrentItem; + if (currentTab is null) return; + + // Fetch only visible tabs to handle hidden or dynamic items properly + var visibleTabs = tabBar.Items.Where(t => t.IsVisible).ToList(); + int currentIndex = visibleTabs.IndexOf(currentTab); + + if (currentIndex < 0) return; + + int nextIndex = currentIndex + direction; + + // Execute the tab shift if within layout boundaries + if (nextIndex >= 0 && nextIndex < visibleTabs.Count) + { + tabBar.CurrentItem = visibleTabs[nextIndex]; + } + } +} \ No newline at end of file diff --git a/OpenTalkie/ViewModels/GeneralSettingsViewModel.cs b/OpenTalkie/ViewModels/GeneralSettingsViewModel.cs new file mode 100644 index 0000000..ed880af --- /dev/null +++ b/OpenTalkie/ViewModels/GeneralSettingsViewModel.cs @@ -0,0 +1,25 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Maui.Storage; + +namespace OpenTalkie.Presentation.ViewModels; + +public partial class GeneralSettingsViewModel : ObservableObject +{ + // Single Source of Truth for both files + public const string SwipeNavPreferenceKey = "SwipeNavigationEnabled"; + public const bool DefaultSwipeNavEnabled = true; + + [ObservableProperty] + private bool _isSwipingEnabled; + + public GeneralSettingsViewModel() + { + // Read using internal public constants + _isSwipingEnabled = Preferences.Default.Get(SwipeNavPreferenceKey, DefaultSwipeNavEnabled); + } + + partial void OnIsSwipingEnabledChanged(bool value) + { + Preferences.Default.Set(SwipeNavPreferenceKey, value); + } +} \ No newline at end of file diff --git a/OpenTalkie/ViewModels/SettingsViewModel.cs b/OpenTalkie/ViewModels/SettingsViewModel.cs index f43c2d7..782ce57 100644 --- a/OpenTalkie/ViewModels/SettingsViewModel.cs +++ b/OpenTalkie/ViewModels/SettingsViewModel.cs @@ -33,6 +33,7 @@ public SettingsViewModel(INavigationService navigationService, IPlatformCapabili SettingsItems.Add(new SettingsItem { Name = "Cast Settings", Route = "PlaybackSettingsPage" }); SettingsItems.Add(new SettingsItem { Name = "Audio Manager Settings", Route = "AudioManagerSettingsPage" }); + SettingsItems.Add(new SettingsItem { Name = "General Settings", Route = "GeneralSettingsPage" }); } [RelayCommand] diff --git a/OpenTalkie/Views/GeneralSettingsPage.xaml b/OpenTalkie/Views/GeneralSettingsPage.xaml new file mode 100644 index 0000000..a73ee9f --- /dev/null +++ b/OpenTalkie/Views/GeneralSettingsPage.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + diff --git a/OpenTalkie/Views/GeneralSettingsPage.xaml.cs b/OpenTalkie/Views/GeneralSettingsPage.xaml.cs new file mode 100644 index 0000000..e3273ac --- /dev/null +++ b/OpenTalkie/Views/GeneralSettingsPage.xaml.cs @@ -0,0 +1,12 @@ +using OpenTalkie.Presentation.ViewModels; + +namespace OpenTalkie.Presentation.Views; + +public partial class GeneralSettingsPage : ContentPage +{ + public GeneralSettingsPage(GeneralSettingsViewModel viewModel) + { + BindingContext = viewModel; + InitializeComponent(); + } +}