Add tabbed view to homescreen
This commit is contained in:
parent
e8ea2c9797
commit
24178aaa43
10 changed files with 576 additions and 463 deletions
|
@ -68,6 +68,7 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
||||
: _apiClient = apiClient, _notificationClient = notificationClient {
|
||||
debugPrint("mClient created: $hashCode");
|
||||
Hive.openBox(_messageBoxKey).then((box) async {
|
||||
box.delete(_lastUpdateKey);
|
||||
await refreshFriendsListWithErrorHandler();
|
||||
|
@ -84,6 +85,7 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
debugPrint("mClient disposed: $hashCode");
|
||||
_autoRefresh?.cancel();
|
||||
_notifyOnlineTimer?.cancel();
|
||||
_unreadSafeguard?.cancel();
|
||||
|
|
179
lib/main.dart
179
lib/main.dart
|
@ -7,7 +7,12 @@ import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
|||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
||||
import 'package:contacts_plus_plus/models/sem_ver.dart';
|
||||
import 'package:contacts_plus_plus/widgets/friends/friends_list.dart';
|
||||
import 'package:contacts_plus_plus/widgets/friends/friends_list_app_bar.dart';
|
||||
import 'package:contacts_plus_plus/widgets/login_screen.dart';
|
||||
import 'package:contacts_plus_plus/widgets/session_list.dart';
|
||||
import 'package:contacts_plus_plus/widgets/session_list_app_bar.dart';
|
||||
import 'package:contacts_plus_plus/widgets/settings_app_bar.dart';
|
||||
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
||||
import 'package:contacts_plus_plus/widgets/update_notifier.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -22,12 +27,15 @@ import 'models/authentication_data.dart';
|
|||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
Provider.debugCheckInvalidValueType = null;
|
||||
await Hive.initFlutter();
|
||||
final dateFormat = DateFormat.Hms();
|
||||
Logger.root.onRecord.listen((event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
|
||||
Logger.root.onRecord.listen(
|
||||
(event) => log("${dateFormat.format(event.time)}: ${event.message}", name: event.loggerName, time: event.time));
|
||||
final settingsClient = SettingsClient();
|
||||
await settingsClient.loadSettings();
|
||||
final newSettings = settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault);
|
||||
final newSettings =
|
||||
settingsClient.currentSettings.copyWith(machineId: settingsClient.currentSettings.machineId.valueOrDefault);
|
||||
await settingsClient.changeSettings(newSettings); // Save generated machineId to disk
|
||||
AuthenticationData cachedAuth = AuthenticationData.unauthenticated();
|
||||
try {
|
||||
|
@ -47,15 +55,28 @@ class ContactsPlusPlus extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||
static const List<Widget> _appBars = [
|
||||
FriendsListAppBar(
|
||||
key: ValueKey("friends_list_app_bar"),
|
||||
),
|
||||
SessionListAppBar(
|
||||
key: ValueKey("session_list_app_bar"),
|
||||
),
|
||||
SettingsAppBar(
|
||||
key: ValueKey("settings_app_bar"),
|
||||
)
|
||||
];
|
||||
|
||||
final Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
||||
final PageController _pageController = PageController();
|
||||
late AuthenticationData _authData = widget.cachedAuthentication;
|
||||
|
||||
bool _checkedForUpdate = false;
|
||||
int _selectedPage = 0;
|
||||
|
||||
void showUpdateDialogOnFirstBuild(BuildContext context) {
|
||||
final navigator = Navigator.of(context);
|
||||
final settings = ClientHolder
|
||||
.of(context)
|
||||
.settingsClient;
|
||||
final settings = ClientHolder.of(context).settingsClient;
|
||||
if (_checkedForUpdate) return;
|
||||
_checkedForUpdate = true;
|
||||
GithubApi.getLatestTagName().then((remoteVer) async {
|
||||
|
@ -103,61 +124,103 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Phoenix(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return ClientHolder(
|
||||
settingsClient: widget.settingsClient,
|
||||
authenticationData: _authData,
|
||||
onLogout: () {
|
||||
setState(() {
|
||||
_authData = AuthenticationData.unauthenticated();
|
||||
});
|
||||
Phoenix.rebirth(context);
|
||||
},
|
||||
child: DynamicColorBuilder(
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Contacts++',
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.black,
|
||||
colorScheme: lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.white,
|
||||
colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
|
||||
),
|
||||
themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault],
|
||||
home: Builder( // Builder is necessary here since we need a context which has access to the ClientHolder
|
||||
child: Builder(builder: (context) {
|
||||
return ClientHolder(
|
||||
settingsClient: widget.settingsClient,
|
||||
authenticationData: _authData,
|
||||
onLogout: () {
|
||||
setState(() {
|
||||
_authData = AuthenticationData.unauthenticated();
|
||||
});
|
||||
Phoenix.rebirth(context);
|
||||
},
|
||||
child: DynamicColorBuilder(
|
||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Contacts++',
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.black,
|
||||
colorScheme:
|
||||
lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
useMaterial3: true,
|
||||
textTheme: _typography.white,
|
||||
colorScheme:
|
||||
darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
|
||||
),
|
||||
themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault],
|
||||
home: Builder(// Builder is necessary here since we need a context which has access to the ClientHolder
|
||||
builder: (context) {
|
||||
showUpdateDialogOnFirstBuild(context);
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
return _authData.isAuthenticated ?
|
||||
ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime.
|
||||
create: (context) =>
|
||||
MessagingClient(
|
||||
apiClient: clientHolder.apiClient,
|
||||
notificationClient: clientHolder.notificationClient,
|
||||
showUpdateDialogOnFirstBuild(context);
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
return _authData.isAuthenticated
|
||||
? Provider(
|
||||
create: (context) => MessagingClient(
|
||||
apiClient: clientHolder.apiClient,
|
||||
notificationClient: clientHolder.notificationClient,
|
||||
),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: _appBars[_selectedPage],
|
||||
),
|
||||
),
|
||||
child: const FriendsList(),
|
||||
) :
|
||||
LoginScreen(
|
||||
onLoginSuccessful: (AuthenticationData authData) async {
|
||||
if (authData.isAuthenticated) {
|
||||
setState(() {
|
||||
_authData = authData;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
body: PageView(
|
||||
controller: _pageController,
|
||||
children: const [
|
||||
FriendsList(),
|
||||
SessionList(),
|
||||
SettingsPage(),
|
||||
],
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
||||
currentIndex: _selectedPage,
|
||||
onTap: (index) {
|
||||
_pageController.animateToPage(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
setState(() {
|
||||
_selectedPage = index;
|
||||
});
|
||||
},
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.message),
|
||||
label: "Chat",
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.public),
|
||||
label: "Sessions",
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.settings),
|
||||
label: "Settings",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: LoginScreen(
|
||||
onLoginSuccessful: (AuthenticationData authData) async {
|
||||
if (authData.isAuthenticated) {
|
||||
setState(() {
|
||||
_authData = authData;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
})),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,10 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
||||
import 'package:contacts_plus_plus/client_holder.dart';
|
||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
||||
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
||||
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.dart';
|
||||
import 'package:contacts_plus_plus/widgets/friends/friend_list_tile.dart';
|
||||
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
|
||||
import 'package:contacts_plus_plus/widgets/session_list.dart';
|
||||
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
||||
import 'package:contacts_plus_plus/widgets/friends/user_search.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MenuItemDefinition {
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final Function() onTap;
|
||||
|
||||
const MenuItemDefinition({required this.name, required this.icon, required this.onTap});
|
||||
}
|
||||
|
||||
class FriendsList extends StatefulWidget {
|
||||
const FriendsList({super.key});
|
||||
|
@ -31,264 +13,78 @@ class FriendsList extends StatefulWidget {
|
|||
State<FriendsList> createState() => _FriendsListState();
|
||||
}
|
||||
|
||||
class _FriendsListState extends State<FriendsList> {
|
||||
Future<UserStatus>? _userStatusFuture;
|
||||
ClientHolder? _clientHolder;
|
||||
class _FriendsListState extends State<FriendsList> with AutomaticKeepAliveClientMixin {
|
||||
String _searchFilter = "";
|
||||
|
||||
@override
|
||||
void didChangeDependencies() async {
|
||||
super.didChangeDependencies();
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
if (_clientHolder != clientHolder) {
|
||||
_clientHolder = clientHolder;
|
||||
_refreshUserStatus();
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshUserStatus() {
|
||||
final apiClient = _clientHolder!.apiClient;
|
||||
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
|
||||
if (value.onlineStatus == OnlineStatus.offline) {
|
||||
final newStatus = value.copyWith(
|
||||
onlineStatus:
|
||||
OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]);
|
||||
await UserApi.setStatus(apiClient, status: newStatus);
|
||||
return newStatus;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<MessagingClient>(context, listen: false),
|
||||
child: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Consumer<MessagingClient>(builder: (context, mClient, _) {
|
||||
if (mClient.initStatus == null) {
|
||||
return const LinearProgressIndicator();
|
||||
} else if (mClient.initStatus!.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultErrorWidget(
|
||||
message: mClient.initStatus,
|
||||
onRetry: () async {
|
||||
mClient.resetInitStatus();
|
||||
mClient.refreshFriendsListWithErrorHandler();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
var friends = List.from(mClient.cachedFriends); // Explicit copy.
|
||||
if (_searchFilter.isNotEmpty) {
|
||||
friends = friends
|
||||
.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase()))
|
||||
.toList();
|
||||
friends.sort((a, b) => a.username.length.compareTo(b.username.length));
|
||||
}
|
||||
return ListView.builder(
|
||||
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
|
||||
itemCount: friends.length,
|
||||
itemBuilder: (context, index) {
|
||||
final friend = friends[index];
|
||||
final unreads = mClient.getUnreadsForFriend(friend);
|
||||
return FriendListTile(
|
||||
friend: friend,
|
||||
unreads: unreads.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ExpandingInputFab(
|
||||
onInputChanged: (String text) {
|
||||
setState(() {
|
||||
_searchFilter = text;
|
||||
});
|
||||
},
|
||||
onExpansionChanged: (expanded) {
|
||||
if (!expanded) {
|
||||
setState(() {
|
||||
_searchFilter = "";
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Contacts++"),
|
||||
actions: [
|
||||
FutureBuilder(
|
||||
future: _userStatusFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final userStatus = snapshot.data as UserStatus;
|
||||
return PopupMenuButton<OnlineStatus>(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
size: 16,
|
||||
color: userStatus.onlineStatus.color(context),
|
||||
),
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
|
||||
],
|
||||
),
|
||||
onSelected: (OnlineStatus onlineStatus) async {
|
||||
try {
|
||||
final newStatus = userStatus.copyWith(onlineStatus: onlineStatus);
|
||||
setState(() {
|
||||
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
|
||||
});
|
||||
final settingsClient = ClientHolder.of(context).settingsClient;
|
||||
await UserApi.setStatus(clientHolder.apiClient, status: newStatus);
|
||||
await settingsClient.changeSettings(
|
||||
settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
|
||||
} catch (e, s) {
|
||||
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
|
||||
setState(() {
|
||||
_userStatusFuture = Future.value(userStatus);
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => OnlineStatus.values
|
||||
.where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
|
||||
.map(
|
||||
(item) => PopupMenuItem<OnlineStatus>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 16,
|
||||
color: item.color(context),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(item.name)!),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
} else if (snapshot.hasError) {
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_userStatusFuture =
|
||||
UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient.userId);
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.warning),
|
||||
label: const Text("Retry"),
|
||||
);
|
||||
} else {
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
disabledForegroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
icon: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
label: const Text("Loading"),
|
||||
);
|
||||
}
|
||||
}),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
child: PopupMenuButton<MenuItemDefinition>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (MenuItemDefinition itemDef) async {
|
||||
await itemDef.onTap();
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
MenuItemDefinition(
|
||||
name: "Settings",
|
||||
icon: Icons.settings,
|
||||
onTap: () async {
|
||||
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "Find Users",
|
||||
icon: Icons.person_add,
|
||||
onTap: () async {
|
||||
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider<MessagingClient>.value(
|
||||
value: mClient,
|
||||
child: const UserSearch(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "My Profile",
|
||||
icon: Icons.person,
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const MyProfileDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "Sessions",
|
||||
icon: Icons.location_city,
|
||||
onTap: () async {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const SessionList()),
|
||||
);
|
||||
},
|
||||
),
|
||||
].map(
|
||||
(item) => PopupMenuItem<MenuItemDefinition>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(item.name),
|
||||
Icon(item.icon),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
Consumer<MessagingClient>(builder: (context, mClient, _) {
|
||||
if (mClient.initStatus == null) {
|
||||
return const LinearProgressIndicator();
|
||||
} else if (mClient.initStatus!.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DefaultErrorWidget(
|
||||
message: mClient.initStatus,
|
||||
onRetry: () async {
|
||||
mClient.resetInitStatus();
|
||||
mClient.refreshFriendsListWithErrorHandler();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
var friends = List.from(mClient.cachedFriends); // Explicit copy.
|
||||
if (_searchFilter.isNotEmpty) {
|
||||
friends = friends
|
||||
.where((element) => element.username.toLowerCase().contains(_searchFilter.toLowerCase()))
|
||||
.toList();
|
||||
friends.sort((a, b) => a.username.length.compareTo(b.username.length));
|
||||
}
|
||||
return ListView.builder(
|
||||
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
|
||||
itemCount: friends.length,
|
||||
itemBuilder: (context, index) {
|
||||
final friend = friends[index];
|
||||
final unreads = mClient.getUnreadsForFriend(friend);
|
||||
return FriendListTile(
|
||||
friend: friend,
|
||||
unreads: unreads.length,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ExpandingInputFab(
|
||||
onInputChanged: (String text) {
|
||||
setState(() {
|
||||
_searchFilter = text;
|
||||
});
|
||||
},
|
||||
onExpansionChanged: (expanded) {
|
||||
if (!expanded) {
|
||||
setState(() {
|
||||
_searchFilter = "";
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
223
lib/widgets/friends/friends_list_app_bar.dart
Normal file
223
lib/widgets/friends/friends_list_app_bar.dart
Normal file
|
@ -0,0 +1,223 @@
|
|||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
||||
import 'package:contacts_plus_plus/client_holder.dart';
|
||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
||||
import 'package:contacts_plus_plus/models/users/online_status.dart';
|
||||
import 'package:contacts_plus_plus/models/users/user_status.dart';
|
||||
import 'package:contacts_plus_plus/widgets/friends/user_search.dart';
|
||||
import 'package:contacts_plus_plus/widgets/my_profile_dialog.dart';
|
||||
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class FriendsListAppBar extends StatefulWidget {
|
||||
const FriendsListAppBar({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _FriendsListAppBarState();
|
||||
}
|
||||
|
||||
class _FriendsListAppBarState extends State<FriendsListAppBar> with AutomaticKeepAliveClientMixin {
|
||||
Future<UserStatus>? _userStatusFuture;
|
||||
ClientHolder? _clientHolder;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() async {
|
||||
super.didChangeDependencies();
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
if (_clientHolder != clientHolder) {
|
||||
_clientHolder = clientHolder;
|
||||
_refreshUserStatus();
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshUserStatus() {
|
||||
final apiClient = _clientHolder!.apiClient;
|
||||
_userStatusFuture ??= UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
|
||||
if (value.onlineStatus == OnlineStatus.offline) {
|
||||
final newStatus = value.copyWith(
|
||||
onlineStatus:
|
||||
OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus.valueOrDefault]);
|
||||
await UserApi.setStatus(apiClient, status: newStatus);
|
||||
return newStatus;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return ChangeNotifierProvider.value(
|
||||
value: Provider.of<MessagingClient>(context, listen: false),
|
||||
child: AppBar(
|
||||
title: const Text("Contacts++"),
|
||||
actions: [
|
||||
FutureBuilder(
|
||||
future: _userStatusFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final userStatus = snapshot.data as UserStatus;
|
||||
return PopupMenuButton<OnlineStatus>(
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Icon(
|
||||
Icons.circle,
|
||||
size: 16,
|
||||
color: userStatus.onlineStatus.color(context),
|
||||
),
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(userStatus.onlineStatus.name) ?? "Unknown"),
|
||||
],
|
||||
),
|
||||
onSelected: (OnlineStatus onlineStatus) async {
|
||||
try {
|
||||
final newStatus = userStatus.copyWith(onlineStatus: onlineStatus);
|
||||
setState(() {
|
||||
_userStatusFuture = Future.value(newStatus.copyWith(lastStatusChange: DateTime.now()));
|
||||
});
|
||||
final settingsClient = _clientHolder!.settingsClient;
|
||||
await UserApi.setStatus(_clientHolder!.apiClient, status: newStatus);
|
||||
await settingsClient.changeSettings(
|
||||
settingsClient.currentSettings.copyWith(lastOnlineStatus: onlineStatus.index));
|
||||
} catch (e, s) {
|
||||
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text("Failed to set online-status.")));
|
||||
setState(() {
|
||||
_userStatusFuture = Future.value(userStatus);
|
||||
});
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context) => OnlineStatus.values
|
||||
.where((element) => element == OnlineStatus.online || element == OnlineStatus.invisible)
|
||||
.map(
|
||||
(item) => PopupMenuItem<OnlineStatus>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 16,
|
||||
color: item.color(context),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Text(toBeginningOfSentenceCase(item.name)!),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList());
|
||||
} else if (snapshot.hasError) {
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2)),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_userStatusFuture =
|
||||
UserApi.getUserStatus(_clientHolder!.apiClient, userId: _clientHolder!.apiClient.userId);
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.warning),
|
||||
label: const Text("Retry"),
|
||||
);
|
||||
} else {
|
||||
return TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
disabledForegroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
onPressed: null,
|
||||
icon: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
label: const Text("Loading"),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
child: PopupMenuButton<MenuItemDefinition>(
|
||||
icon: const Icon(Icons.more_vert),
|
||||
onSelected: (MenuItemDefinition itemDef) async {
|
||||
await itemDef.onTap();
|
||||
},
|
||||
itemBuilder: (BuildContext context) => [
|
||||
MenuItemDefinition(
|
||||
name: "Settings",
|
||||
icon: Icons.settings,
|
||||
onTap: () async {
|
||||
await Navigator.of(context).push(MaterialPageRoute(builder: (context) => const SettingsPage()));
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "Find Users",
|
||||
icon: Icons.person_add,
|
||||
onTap: () async {
|
||||
final mClient = Provider.of<MessagingClient>(context, listen: false);
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChangeNotifierProvider<MessagingClient>.value(
|
||||
value: mClient,
|
||||
child: const UserSearch(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuItemDefinition(
|
||||
name: "My Profile",
|
||||
icon: Icons.person,
|
||||
onTap: () async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const MyProfileDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
]
|
||||
.map(
|
||||
(item) => PopupMenuItem<MenuItemDefinition>(
|
||||
value: item,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(item.name),
|
||||
Icon(item.icon),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class MenuItemDefinition {
|
||||
final String name;
|
||||
final IconData icon;
|
||||
final Function() onTap;
|
||||
|
||||
const MenuItemDefinition({required this.name, required this.icon, required this.onTap});
|
||||
}
|
20
lib/widgets/global_app_bar.dart
Normal file
20
lib/widgets/global_app_bar.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class GlobalAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
const GlobalAppBar({super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _GlobalAppBarState();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class _GlobalAppBarState extends State<GlobalAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import 'package:contacts_plus_plus/models/message.dart';
|
|||
import 'package:contacts_plus_plus/models/session.dart';
|
||||
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
|
||||
import 'package:contacts_plus_plus/widgets/messages/messages_session_header.dart';
|
||||
import 'package:contacts_plus_plus/widgets/messages/message_state_indicator.dart';
|
||||
import 'package:contacts_plus_plus/widgets/session_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:contacts_plus_plus/apis/session_api.dart';
|
||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||
import 'package:contacts_plus_plus/client_holder.dart';
|
||||
|
@ -14,146 +15,133 @@ class SessionList extends StatefulWidget {
|
|||
State<SessionList> createState() => _SessionListState();
|
||||
}
|
||||
|
||||
class _SessionListState extends State<SessionList> {
|
||||
class _SessionListState extends State<SessionList> with AutomaticKeepAliveClientMixin {
|
||||
Future<List<Session>>? _sessionsFuture;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_sessionsFuture = SessionApi.getSessions(ClientHolder.of(context).apiClient);
|
||||
_sessionsFuture ??= SessionApi.getSessions(ClientHolder.of(context).apiClient).then(
|
||||
(value) => value.sorted(
|
||||
(a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
scrolledUnderElevation: 0,
|
||||
title: const Text("Sessions"),
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 1,
|
||||
color: Colors.black,
|
||||
super.build(context);
|
||||
return FutureBuilder<List<Session>>(
|
||||
future: _sessionsFuture,
|
||||
builder: (context, snapshot) {
|
||||
final data = snapshot.data ?? [];
|
||||
return Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
|
||||
itemCount: data.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 256,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
childAspectRatio: .8,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
body: FutureBuilder<List<Session>>(
|
||||
future: _sessionsFuture,
|
||||
builder: (context, snapshot) {
|
||||
final data = snapshot.data ?? [];
|
||||
return Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
|
||||
itemCount: data.length,
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 256,
|
||||
crossAxisSpacing: 4,
|
||||
mainAxisSpacing: 4,
|
||||
childAspectRatio: .8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final session = data[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
itemBuilder: (context, index) {
|
||||
final session = data[index];
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) => SessionView(session: session)));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Hero(
|
||||
tag: session.id,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Aux.neosDbToHttp(session.thumbnail),
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) => const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
size: 64,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) => SessionView(session: session)));
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Hero(
|
||||
tag: session.id,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: Aux.neosDbToHttp(session.thumbnail),
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (context, url, error) => const Center(
|
||||
child: Icon(
|
||||
Icons.broken_image,
|
||||
size: 64,
|
||||
),
|
||||
placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FormattedText(
|
||||
session.formattedName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FormattedText(
|
||||
session.formattedName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 4,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
"${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")} Online",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator()
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator()
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
20
lib/widgets/session_list_app_bar.dart
Normal file
20
lib/widgets/session_list_app_bar.dart
Normal file
|
@ -0,0 +1,20 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class SessionListAppBar extends StatelessWidget {
|
||||
const SessionListAppBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: const Text("Sessions"),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
|
||||
},
|
||||
icon: const Icon(Icons.filter_alt_outlined),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
13
lib/widgets/settings_app_bar.dart
Normal file
13
lib/widgets/settings_app_bar.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsAppBar extends StatelessWidget {
|
||||
const SettingsAppBar({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: const Text("Settings"),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -11,17 +11,7 @@ class SettingsPage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sClient = ClientHolder.of(context).settingsClient;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
title: const Text("Settings"),
|
||||
),
|
||||
body: ListView(
|
||||
return ListView(
|
||||
children: [
|
||||
const ListSectionHeader(leadingText: "Notifications"),
|
||||
BooleanSettingsTile(
|
||||
|
@ -110,8 +100,7 @@ class SettingsPage extends StatelessWidget {
|
|||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue