Redesign & refactor home screen

- Removed hamburger/drawer menu
- Moved search to bottom navigation bar
- Moved settings and profile to app bar
- Moved channel list from screen to widget folder
- Created search screen for viewing results
- Switched from Cupertino to Material routes due to rebuild issues
This commit is contained in:
tommyxchow 2021-11-10 21:29:15 -05:00
parent d0f17634ba
commit e3f774d8e5
11 changed files with 186 additions and 147 deletions

View file

@ -32,7 +32,10 @@ class MyApp extends StatelessWidget {
),
home: Scaffold(
body: FutureBuilder(
future: context.read<AuthStore>().init(),
future: Future.wait([
context.read<AuthStore>().init(),
context.read<SettingsStore>().init(),
]),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return Home(

View file

@ -1,14 +1,14 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/api/twitch_api.dart';
import 'package:frosty/screens/video_chat.dart';
import 'package:frosty/screens/search.dart';
import 'package:frosty/screens/settings.dart';
import 'package:frosty/stores/auth_store.dart';
import 'package:frosty/stores/channel_list_store.dart';
import 'package:frosty/stores/home_store.dart';
import 'package:frosty/widgets/drawer_menu.dart';
import 'package:frosty/stores/settings_store.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'channel_list.dart';
import '../widgets/channel_list.dart';
class Home extends StatefulWidget {
final HomeStore homeStore;
@ -29,55 +29,49 @@ class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
final titles = [if (context.read<AuthStore>().isLoggedIn) 'Followed Channels', 'Top Channels', 'Categories'];
final titles = [
if (context.read<AuthStore>().isLoggedIn) 'Followed Channels',
'Top Channels',
'Categories',
'Search',
];
debugPrint('build home');
return Scaffold(
appBar: AppBar(
title: Observer(
builder: (_) {
return widget.homeStore.search
? TextField(
controller: _textController,
autocorrect: false,
autofocus: true,
decoration: const InputDecoration(
hintText: 'Search for a channel',
),
onSubmitted: (string) async {
if (await Twitch.getUser(userLogin: string, headers: context.read<AuthStore>().headersTwitch) != null) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (_) {
return VideoChat(
userLogin: string,
);
},
),
);
} else {
const snackBar = SnackBar(content: Text('User does not exist :('));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
widget.homeStore.search = false;
_textController.clear();
},
)
: Text(titles[widget.homeStore.selectedIndex]);
return Text(
titles[widget.homeStore.selectedIndex],
style: GoogleFonts.daysOne(),
);
},
),
actions: [
IconButton(
icon: Observer(builder: (_) => widget.homeStore.search ? const Icon(Icons.cancel) : const Icon(Icons.search)),
icon: const Icon(Icons.settings),
onPressed: () {
widget.homeStore.search = !widget.homeStore.search;
_textController.clear();
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Settings',
style: GoogleFonts.daysOne(),
),
),
body: Settings(settingsStore: context.read<SettingsStore>()),
);
},
),
);
},
)
],
),
drawer: const DrawerMenu(),
body: Observer(
builder: (_) {
return IndexedStack(
@ -95,6 +89,7 @@ class _HomeState extends State<Home> {
const Center(
child: Text('Games'),
),
const Search(),
],
);
},
@ -103,6 +98,7 @@ class _HomeState extends State<Home> {
child: Observer(
builder: (_) {
return BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: [
if (context.read<AuthStore>().isLoggedIn)
const BottomNavigationBarItem(
@ -117,6 +113,10 @@ class _HomeState extends State<Home> {
icon: Icon(Icons.gamepad),
label: 'Categories',
),
const BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search',
),
],
currentIndex: widget.homeStore.selectedIndex,
onTap: widget.homeStore.handleTap,

50
lib/screens/search.dart Normal file
View file

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:frosty/api/twitch_api.dart';
import 'package:frosty/screens/video_chat.dart';
import 'package:frosty/stores/auth_store.dart';
import 'package:provider/provider.dart';
class Search extends StatefulWidget {
const Search({Key? key}) : super(key: key);
@override
_SearchState createState() => _SearchState();
}
class _SearchState extends State<Search> {
final _textController = TextEditingController();
@override
Widget build(BuildContext context) {
return TextField(
controller: _textController,
autocorrect: false,
decoration: const InputDecoration(
isDense: true,
contentPadding: EdgeInsets.all(8.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
),
hintText: 'Search',
),
onSubmitted: (string) async {
if (await Twitch.getUser(userLogin: string, headers: context.read<AuthStore>().headersTwitch) != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
return VideoChat(
userLogin: string,
);
},
),
);
} else {
const snackBar = SnackBar(content: Text('User does not exist :('));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
}
_textController.clear();
},
);
}
}

View file

@ -1,32 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/stores/auth_store.dart';
import 'package:frosty/stores/settings_store.dart';
import 'package:frosty/widgets/profile_card.dart';
import 'package:provider/provider.dart';
class Settings extends StatelessWidget {
const Settings({Key? key}) : super(key: key);
final SettingsStore settingsStore;
const Settings({Key? key, required this.settingsStore}) : super(key: key);
@override
Widget build(BuildContext context) {
final settingsStore = context.read<SettingsStore>();
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
),
body: Observer(
builder: (_) {
return ListView(
children: [
SwitchListTile(
title: const Text('Enable Video'),
value: settingsStore.videoEnabled,
onChanged: (newValue) => settingsStore.videoEnabled = newValue,
),
],
);
},
),
return Observer(
builder: (_) {
return ListView(
children: [
ProfileCard(authStore: context.read<AuthStore>()),
SwitchListTile(
title: const Text('Enable Video'),
value: settingsStore.videoEnabled,
onChanged: (newValue) => settingsStore.videoEnabled = newValue,
),
],
);
},
);
}
}

View file

@ -13,6 +13,9 @@ abstract class _HomeStoreBase with Store {
@action
void handleTap(int index) {
if (index == 3) {
search = !search;
}
if (index != selectedIndex) {
selectedIndex = index;
}

View file

@ -1,10 +1,29 @@
import 'package:flutter/foundation.dart';
import 'package:mobx/mobx.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'settings_store.g.dart';
class SettingsStore = _SettingsStoreBase with _$SettingsStore;
abstract class _SettingsStoreBase with Store {
late final SharedPreferences prefs;
late ReactionDisposer disposeSetter;
Future<void> init() async {
// debugPrint('Created settings provider!');
// prefs = await SharedPreferences.getInstance();
// // Initialize settings from stored preferences if any.
// videoEnabled = prefs.getBool('video_enabled') ?? videoEnabled;
// // Set up autorun to store setting anytime they're changed.
// disposeSetter = autorun((_) {
// debugPrint('video changed');
// prefs.setBool('video_enabled', videoEnabled);
// });
}
@observable
bool videoEnabled = true;
}

View file

@ -1,5 +1,4 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:frosty/models/channel.dart';
import 'package:frosty/screens/video_chat.dart';
@ -18,7 +17,7 @@ class ChannelCard extends StatelessWidget {
onTap: () {
Navigator.push(
context,
CupertinoPageRoute(
MaterialPageRoute(
builder: (context) {
return VideoChat(
userLogin: channelInfo.userLogin,

View file

@ -11,7 +11,7 @@ class ChannelList extends StatelessWidget {
@override
Widget build(BuildContext context) {
debugPrint('build channel list');
debugPrint('Build channel list!');
return RefreshIndicator(
child: Observer(

View file

@ -3,6 +3,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/screens/settings.dart';
import 'package:frosty/stores/auth_store.dart';
import 'package:frosty/stores/chat_store.dart';
import 'package:frosty/stores/settings_store.dart';
import 'package:frosty/widgets/chat_stats.dart';
import 'package:provider/provider.dart';
@ -50,7 +51,7 @@ class _ChatState extends State<Chat> {
padding: const EdgeInsets.symmetric(horizontal: 4.0),
width: double.infinity,
child: ElevatedButton(
onPressed: () => widget.chatStore.resumeScroll(),
onPressed: widget.chatStore.resumeScroll,
child: const Text('Resume Scroll'),
),
),
@ -62,13 +63,17 @@ class _ChatState extends State<Chat> {
),
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) {
return const Settings();
return Settings(settingsStore: context.read<SettingsStore>());
},
);
},

View file

@ -1,34 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:frosty/screens/settings.dart';
import 'package:frosty/widgets/profile_card.dart';
class DrawerMenu extends StatelessWidget {
const DrawerMenu({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer(
child: ListView(
children: [
const ProfileCard(),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Settings'),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
fullscreenDialog: true,
builder: (_) {
return const Settings();
},
),
);
},
)
],
),
);
}
}

View file

@ -2,59 +2,55 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:frosty/stores/auth_store.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class ProfileCard extends StatelessWidget {
const ProfileCard({Key? key}) : super(key: key);
final AuthStore authStore;
const ProfileCard({Key? key, required this.authStore}) : super(key: key);
@override
Widget build(BuildContext context) {
final authStore = context.read<AuthStore>();
Future<void> _showDialog() async {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Logout'),
content: const Text('Are you sure you want to log out?'),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')),
TextButton(
onPressed: () {
authStore.logout();
Navigator.of(context).pop();
},
child: const Text('Yes')),
],
);
},
);
}
return Observer(
builder: (_) {
return Center(
child: authStore.isLoggedIn
? UserAccountsDrawerHeader(
currentAccountPicture: InkWell(
child: CircleAvatar(
foregroundImage: NetworkImage(
authStore.user!.profileImageUrl,
),
),
onTap: () => _showDialog(),
),
accountName: Text(authStore.user!.displayName),
accountEmail: Text('Joined on ${DateFormat.yMMMMd('en_US').format(DateTime.parse(authStore.user!.createdAt)).toString()}'),
)
: ElevatedButton(
onPressed: () => authStore.login(),
child: const Text('Login'),
),
Future<void> _showDialog(BuildContext context) {
return showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Logout'),
content: const Text('Are you sure you want to log out?'),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')),
TextButton(
onPressed: () {
authStore.logout();
Navigator.of(context).pop();
},
child: const Text('Yes'),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Observer(
builder: (context) {
if (authStore.isLoggedIn) {
return ListTile(
leading: CircleAvatar(
foregroundImage: NetworkImage(
authStore.user!.profileImageUrl,
),
),
title: Text(authStore.user!.displayName),
onTap: () => _showDialog(context),
);
}
return ElevatedButton(
onPressed: authStore.login,
child: const Text('Login'),
);
},
),
);
}
}