From fb5f1b01959bcd68bcd1efd341b45d0f1e52c2d0 Mon Sep 17 00:00:00 2001 From: Doozy <98877504+DoozyDoz@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:08:01 +0300 Subject: [PATCH 1/2] buttom nav bar changed to left for tvs (remains the same for small screens) --- lib/bottom_nav.dart | 51 ++++++++ lib/home.dart | 184 +++++++++++++++------------- lib/settings_view.dart | 272 +++++++++++++++++++++-------------------- 3 files changed, 291 insertions(+), 216 deletions(-) diff --git a/lib/bottom_nav.dart b/lib/bottom_nav.dart index 883c704..46e0944 100644 --- a/lib/bottom_nav.dart +++ b/lib/bottom_nav.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:open_tv/models/view_type.dart'; import 'package:open_tv/settings_view.dart'; @@ -6,11 +7,13 @@ class BottomNav extends StatefulWidget { final Function(ViewType) updateViewMode; final ViewType startingView; final bool blockSettings; + final bool useRail; const BottomNav({ super.key, required this.updateViewMode, this.startingView = ViewType.all, this.blockSettings = false, + this.useRail = false, }); @override @@ -56,6 +59,42 @@ class _BottomNavState extends State { @override Widget build(BuildContext context) { + if (widget.useRail) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceBright, + border: Border( + right: BorderSide( + color: Theme.of(context).colorScheme.surfaceBright, + width: 1))), + child: NavigationRail( + selectedIndex: _selectedIndex, + onDestinationSelected: onBarTapped, + labelType: NavigationRailLabelType.all, + destinations: const [ + NavigationRailDestination( + icon: Icon(Icons.list), + label: Text('All'), + ), + NavigationRailDestination( + icon: Icon(Icons.dashboard), + label: Text('Categories'), + ), + NavigationRailDestination( + icon: Icon(Icons.star), + label: Text('Favorites'), + ), + NavigationRailDestination( + icon: Icon(Icons.history), + label: Text('History'), + ), + NavigationRailDestination( + icon: Icon(Icons.settings), + label: Text('Settings'), + ), + ], + )); + } return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceBright, @@ -91,3 +130,15 @@ class _BottomNavState extends State { )); } } + +bool shouldUseSideNav(BuildContext context) { + final media = MediaQuery.of(context); + final navigationMode = + MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; + final isLargeLandscape = + media.size.width >= 900 && media.size.width > media.size.height; + final isAndroidLike = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + return isLargeLandscape && + (navigationMode == NavigationMode.directional || isAndroidLike); +} diff --git a/lib/home.dart b/lib/home.dart index 1a25bb9..21b4ade 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -194,6 +194,87 @@ class _HomeState extends State { @override Widget build(BuildContext context) { + final useSideNav = shouldUseSideNav(context); + final content = Column(children: [ + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + child: searchMode + ? Container( + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + color: Theme.of(context) + .colorScheme + .surfaceContainer, // Background color + child: Row( + children: [ + Expanded( + child: TextField( + controller: searchController, + focusNode: _focusNode, + onChanged: (query) { + _debounce?.cancel(); + _debounce = + Timer(const Duration(milliseconds: 500), () { + widget.home.filters.query = query; + load(false); + }); + }, + decoration: InputDecoration( + hintText: "Search...", + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: BorderSide.none, + ), + suffixIcon: IconButton( + onPressed: () { + widget.home.filters.useKeywords = + !widget.home.filters.useKeywords; + load(false); + }, + icon: Icon(widget.home.filters.useKeywords + ? Icons.label + : Icons.label_outline)), + filled: true, // Light background for contrast + contentPadding: + const EdgeInsets.symmetric(vertical: 0), + ), + )), + const SizedBox(width: 10), + SizedBox( + width: 40, + child: IconButton( + onPressed: toggleSearch, + icon: const Icon( + Icons.close, + ))) + ], + ), + ) + : const SizedBox.shrink()), + Expanded( + child: GridView.builder( + shrinkWrap: true, + controller: _scrollController, + padding: const EdgeInsets.fromLTRB(16, 15, 16, 5), + itemCount: channels.length, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 315, + mainAxisExtent: 120, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + ), + itemBuilder: (context, index) { + final channel = channels[index]; + return ChannelTile( + channel: channel, + parentContext: context, + setNode: setNode, + ); + }, + )), + ]); return PopScope( canPop: canPop(), onPopInvokedWithResult: (didPop, result) { @@ -211,91 +292,24 @@ class _HomeState extends State { : null, body: Loading( child: SafeArea( - child: Column(children: [ - AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: searchMode - ? Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 8), - color: Theme.of(context) - .colorScheme - .surfaceContainer, // Background color - child: Row( - children: [ - Expanded( - child: TextField( - controller: searchController, - focusNode: _focusNode, - onChanged: (query) { - _debounce?.cancel(); - _debounce = Timer( - const Duration(milliseconds: 500), () { - widget.home.filters.query = query; - load(false); - }); - }, - decoration: InputDecoration( - hintText: "Search...", - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), - suffixIcon: IconButton( - onPressed: () { - widget.home.filters.useKeywords = - !widget.home.filters.useKeywords; - load(false); - }, - icon: Icon(widget.home.filters.useKeywords - ? Icons.label - : Icons.label_outline)), - filled: true, // Light background for contrast - contentPadding: - const EdgeInsets.symmetric(vertical: 0), - ), - )), - const SizedBox(width: 10), - SizedBox( - width: 40, - child: IconButton( - onPressed: toggleSearch, - icon: const Icon( - Icons.close, - ))) - ], - ), - ) - : const SizedBox.shrink()), - Expanded( - child: GridView.builder( - shrinkWrap: true, - controller: _scrollController, - padding: const EdgeInsets.fromLTRB(16, 15, 16, 5), - itemCount: channels.length, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 315, - mainAxisExtent: 120, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - ), - itemBuilder: (context, index) { - final channel = channels[index]; - return ChannelTile( - channel: channel, - parentContext: context, - setNode: setNode, - ); - }, - )), - ]))), - bottomNavigationBar: BottomNav( - startingView: getStartingView(), - blockSettings: blockSettings, - updateViewMode: updateViewMode, - ), + child: useSideNav + ? Row(children: [ + BottomNav( + startingView: getStartingView(), + blockSettings: blockSettings, + updateViewMode: updateViewMode, + useRail: true, + ), + Expanded(child: content), + ]) + : content)), + bottomNavigationBar: useSideNav + ? null + : BottomNav( + startingView: getStartingView(), + blockSettings: blockSettings, + updateViewMode: updateViewMode, + ), floatingActionButton: Visibility( visible: !searchMode, child: FloatingActionButton( diff --git a/lib/settings_view.dart b/lib/settings_view.dart index 4d8f479..578a11e 100644 --- a/lib/settings_view.dart +++ b/lib/settings_view.dart @@ -179,143 +179,153 @@ class _SettingsState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: Visibility( - visible: !loading, - child: Loading( - child: SafeArea( - child: Padding( - padding: - const EdgeInsetsDirectional.symmetric(vertical: 10), - child: ListView( - children: [ - const SizedBox(height: 10), - const Padding( - padding: EdgeInsets.only(left: 10), - child: Text('Settings', - style: TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold))), - const SizedBox(height: 10), - ListTile( - title: const Text("Donate"), - subtitle: const Text( - "Fred TV needs your help! Consider donating ❤️"), - onTap: () async => await launchUrl( - Uri.parse( - "https://github.com/Fredolx/fred-tv-mobile/discussions/1", - ), - mode: LaunchMode.externalApplication)), - ListTile( - title: const Text("Default view"), - subtitle: - Text(viewTypeToString(settings.defaultView)), - onTap: () async => - await _showDefaultViewDialog(context)), - ListTile( - title: const Text("Refresh sources on start"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Switch( - value: settings.refreshOnStart, - onChanged: (bool value) { - setState(() { - settings.refreshOnStart = value; - }); - updateSettings(); - }, - ), - ], + final useSideNav = shouldUseSideNav(context); + final content = Visibility( + visible: !loading, + child: Loading( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 10), + child: ListView( + children: [ + const SizedBox(height: 10), + const Padding( + padding: EdgeInsets.only(left: 10), + child: Text('Settings', + style: TextStyle( + fontSize: 30, fontWeight: FontWeight.bold))), + const SizedBox(height: 10), + ListTile( + title: const Text("Donate"), + subtitle: const Text( + "Fred TV needs your help! Consider donating."), + onTap: () async => await launchUrl( + Uri.parse( + "https://github.com/Fredolx/fred-tv-mobile/discussions/1", ), + mode: LaunchMode.externalApplication)), + ListTile( + title: const Text("Default view"), + subtitle: Text(viewTypeToString(settings.defaultView)), + onTap: () async => + await _showDefaultViewDialog(context)), + ListTile( + title: const Text("Refresh sources on start"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: settings.refreshOnStart, + onChanged: (bool value) { + setState(() { + settings.refreshOnStart = value; + }); + updateSettings(); + }, ), - ListTile( - title: const Text("Show livestreams"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Switch( - value: settings.showLivestreams, - onChanged: (bool value) { - setState(() { - settings.showLivestreams = value; - }); - updateSettings(); - }, - ), - ], - ), + ], + ), + ), + ListTile( + title: const Text("Show livestreams"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: settings.showLivestreams, + onChanged: (bool value) { + setState(() { + settings.showLivestreams = value; + }); + updateSettings(); + }, ), - ListTile( - title: const Text("Show movies"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Switch( - value: settings.showMovies, - onChanged: (bool value) { - setState(() { - settings.showMovies = value; - }); - updateSettings(); - }, - ), - ], - ), + ], + ), + ), + ListTile( + title: const Text("Show movies"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: settings.showMovies, + onChanged: (bool value) { + setState(() { + settings.showMovies = value; + }); + updateSettings(); + }, ), - ListTile( - title: const Text("Show series"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Switch( - value: settings.showSeries, - onChanged: (bool value) { - setState(() { - settings.showSeries = value; - }); - updateSettings(); - }, - ), - ], - ), + ], + ), + ), + ListTile( + title: const Text("Show series"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Switch( + value: settings.showSeries, + onChanged: (bool value) { + setState(() { + settings.showSeries = value; + }); + updateSettings(); + }, ), - const Divider(), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Padding( - padding: EdgeInsets.only(left: 10), - child: Text('Sources', - style: TextStyle( - fontSize: 30, - fontWeight: FontWeight.bold))), - Row(children: [ - IconButton( - onPressed: () async => await Error.tryAsync( - () async => - await Utils.refreshAllSources(), - context, - "Successfully refreshed all sources"), - icon: const Icon(Icons.refresh)), - IconButton( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const Setup( - showAppBar: true, - ))), - icon: const Icon(Icons.add)) - ]) - ]), - const SizedBox(height: 10), - ...sources.map(getSource) ], - ))))), - bottomNavigationBar: BottomNav( - updateViewMode: updateView, - startingView: ViewType.settings, - ), + ), + ), + const Divider(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Padding( + padding: EdgeInsets.only(left: 10), + child: Text('Sources', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold))), + Row(children: [ + IconButton( + onPressed: () async => await Error.tryAsync( + () async => + await Utils.refreshAllSources(), + context, + "Successfully refreshed all sources"), + icon: const Icon(Icons.refresh)), + IconButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const Setup( + showAppBar: true, + ))), + icon: const Icon(Icons.add)) + ]) + ]), + const SizedBox(height: 10), + ...sources.map(getSource) + ], + )))); + return Scaffold( + body: SafeArea( + child: useSideNav + ? Row(children: [ + BottomNav( + updateViewMode: updateView, + startingView: ViewType.settings, + useRail: true, + ), + Expanded(child: content), + ]) + : content), + bottomNavigationBar: useSideNav + ? null + : BottomNav( + updateViewMode: updateView, + startingView: ViewType.settings, + ), ); } } From 1f418570541b407b5a9663efa83746886b450dc6 Mon Sep 17 00:00:00 2001 From: Doozy <98877504+DoozyDoz@users.noreply.github.com> Date: Wed, 31 Dec 2025 00:16:31 +0300 Subject: [PATCH 2/2] moved the search button to the left navbar for accessibility --- lib/bottom_nav.dart | 35 +++++++++++++++++++++++++++-------- lib/home.dart | 20 ++++++++++++-------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/lib/bottom_nav.dart b/lib/bottom_nav.dart index 46e0944..97a3280 100644 --- a/lib/bottom_nav.dart +++ b/lib/bottom_nav.dart @@ -8,12 +8,16 @@ class BottomNav extends StatefulWidget { final ViewType startingView; final bool blockSettings; final bool useRail; + final bool showSearch; + final VoidCallback? onSearch; const BottomNav({ super.key, required this.updateViewMode, this.startingView = ViewType.all, this.blockSettings = false, this.useRail = false, + this.showSearch = false, + this.onSearch, }); @override @@ -60,6 +64,9 @@ class _BottomNavState extends State { @override Widget build(BuildContext context) { if (widget.useRail) { + final includeSearch = widget.showSearch && widget.onSearch != null; + final selectedIndex = + includeSearch ? _selectedIndex + 1 : _selectedIndex; return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceBright, @@ -68,27 +75,39 @@ class _BottomNavState extends State { color: Theme.of(context).colorScheme.surfaceBright, width: 1))), child: NavigationRail( - selectedIndex: _selectedIndex, - onDestinationSelected: onBarTapped, + selectedIndex: selectedIndex, + onDestinationSelected: (index) { + if (includeSearch && index == 0) { + widget.onSearch!(); + return; + } + final adjustedIndex = includeSearch ? index - 1 : index; + onBarTapped(adjustedIndex); + }, labelType: NavigationRailLabelType.all, - destinations: const [ - NavigationRailDestination( + destinations: [ + if (includeSearch) + const NavigationRailDestination( + icon: Icon(Icons.search), + label: Text('Search'), + ), + const NavigationRailDestination( icon: Icon(Icons.list), label: Text('All'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.dashboard), label: Text('Categories'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.star), label: Text('Favorites'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.history), label: Text('History'), ), - NavigationRailDestination( + const NavigationRailDestination( icon: Icon(Icons.settings), label: Text('Settings'), ), diff --git a/lib/home.dart b/lib/home.dart index 21b4ade..8c298cc 100644 --- a/lib/home.dart +++ b/lib/home.dart @@ -299,6 +299,8 @@ class _HomeState extends State { blockSettings: blockSettings, updateViewMode: updateViewMode, useRail: true, + showSearch: true, + onSearch: toggleSearch, ), Expanded(child: content), ]) @@ -310,13 +312,15 @@ class _HomeState extends State { blockSettings: blockSettings, updateViewMode: updateViewMode, ), - floatingActionButton: Visibility( - visible: !searchMode, - child: FloatingActionButton( - onPressed: toggleSearch, - tooltip: 'Search', - child: const Icon(Icons.search), - ), - ))); + floatingActionButton: useSideNav + ? null + : Visibility( + visible: !searchMode, + child: FloatingActionButton( + onPressed: toggleSearch, + tooltip: 'Search', + child: const Icon(Icons.search), + ), + ))); } }