commit
adf96e887f
17 changed files with 1183 additions and 540 deletions
|
@ -10,4 +10,11 @@ class SessionApi {
|
||||||
final body = jsonDecode(response.body);
|
final body = jsonDecode(response.body);
|
||||||
return Session.fromMap(body);
|
return Session.fromMap(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<Session>> getSessions(ApiClient client, {SessionFilterSettings? filterSettings}) async {
|
||||||
|
final response = await client.get("/sessions${filterSettings == null ? "" : filterSettings.buildRequestString()}");
|
||||||
|
client.checkResponse(response);
|
||||||
|
final body = jsonDecode(response.body) as List;
|
||||||
|
return body.map((e) => Session.fromMap(e)).toList();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -68,6 +68,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
|
|
||||||
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
||||||
: _apiClient = apiClient, _notificationClient = notificationClient {
|
: _apiClient = apiClient, _notificationClient = notificationClient {
|
||||||
|
debugPrint("mClient created: $hashCode");
|
||||||
Hive.openBox(_messageBoxKey).then((box) async {
|
Hive.openBox(_messageBoxKey).then((box) async {
|
||||||
box.delete(_lastUpdateKey);
|
box.delete(_lastUpdateKey);
|
||||||
await refreshFriendsListWithErrorHandler();
|
await refreshFriendsListWithErrorHandler();
|
||||||
|
@ -84,6 +85,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
debugPrint("mClient disposed: $hashCode");
|
||||||
_autoRefresh?.cancel();
|
_autoRefresh?.cancel();
|
||||||
_notifyOnlineTimer?.cancel();
|
_notifyOnlineTimer?.cancel();
|
||||||
_unreadSafeguard?.cancel();
|
_unreadSafeguard?.cancel();
|
||||||
|
|
35
lib/clients/session_client.dart
Normal file
35
lib/clients/session_client.dart
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:contacts_plus_plus/apis/session_api.dart';
|
||||||
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
class SessionClient extends ChangeNotifier {
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
Future<List<Session>>? _sessionsFuture;
|
||||||
|
|
||||||
|
SessionFilterSettings _filterSettings = SessionFilterSettings.empty();
|
||||||
|
|
||||||
|
SessionClient({required this.apiClient}) {
|
||||||
|
reloadSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
SessionFilterSettings get filterSettings => _filterSettings;
|
||||||
|
|
||||||
|
Future<List<Session>>? get sessionsFuture => _sessionsFuture;
|
||||||
|
|
||||||
|
set filterSettings(value) {
|
||||||
|
_filterSettings = value;
|
||||||
|
reloadSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reloadSessions() {
|
||||||
|
_sessionsFuture = SessionApi.getSessions(apiClient, filterSettings: _filterSettings).then(
|
||||||
|
(value) => value.sorted(
|
||||||
|
(a, b) => b.sessionUsers.length.compareTo(a.sessionUsers.length),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
195
lib/main.dart
195
lib/main.dart
|
@ -4,10 +4,16 @@ import 'package:contacts_plus_plus/apis/github_api.dart';
|
||||||
import 'package:contacts_plus_plus/client_holder.dart';
|
import 'package:contacts_plus_plus/client_holder.dart';
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
import 'package:contacts_plus_plus/clients/messaging_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/clients/session_client.dart';
|
||||||
import 'package:contacts_plus_plus/clients/settings_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/models/sem_ver.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/friends_list.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/login_screen.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/sessions/session_list.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/sessions/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:contacts_plus_plus/widgets/update_notifier.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -22,12 +28,15 @@ import 'models/authentication_data.dart';
|
||||||
void main() async {
|
void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
Provider.debugCheckInvalidValueType = null;
|
||||||
await Hive.initFlutter();
|
await Hive.initFlutter();
|
||||||
final dateFormat = DateFormat.Hms();
|
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();
|
final settingsClient = SettingsClient();
|
||||||
await settingsClient.loadSettings();
|
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
|
await settingsClient.changeSettings(newSettings); // Save generated machineId to disk
|
||||||
AuthenticationData cachedAuth = AuthenticationData.unauthenticated();
|
AuthenticationData cachedAuth = AuthenticationData.unauthenticated();
|
||||||
try {
|
try {
|
||||||
|
@ -47,15 +56,28 @@ class ContactsPlusPlus extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
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 Typography _typography = Typography.material2021(platform: TargetPlatform.android);
|
||||||
|
final PageController _pageController = PageController();
|
||||||
late AuthenticationData _authData = widget.cachedAuthentication;
|
late AuthenticationData _authData = widget.cachedAuthentication;
|
||||||
|
|
||||||
bool _checkedForUpdate = false;
|
bool _checkedForUpdate = false;
|
||||||
|
int _selectedPage = 0;
|
||||||
|
|
||||||
void showUpdateDialogOnFirstBuild(BuildContext context) {
|
void showUpdateDialogOnFirstBuild(BuildContext context) {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
final settings = ClientHolder
|
final settings = ClientHolder.of(context).settingsClient;
|
||||||
.of(context)
|
|
||||||
.settingsClient;
|
|
||||||
if (_checkedForUpdate) return;
|
if (_checkedForUpdate) return;
|
||||||
_checkedForUpdate = true;
|
_checkedForUpdate = true;
|
||||||
GithubApi.getLatestTagName().then((remoteVer) async {
|
GithubApi.getLatestTagName().then((remoteVer) async {
|
||||||
|
@ -103,61 +125,120 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Phoenix(
|
return Phoenix(
|
||||||
child: Builder(
|
child: Builder(builder: (context) {
|
||||||
builder: (context) {
|
return ClientHolder(
|
||||||
return ClientHolder(
|
settingsClient: widget.settingsClient,
|
||||||
settingsClient: widget.settingsClient,
|
authenticationData: _authData,
|
||||||
authenticationData: _authData,
|
onLogout: () {
|
||||||
onLogout: () {
|
setState(() {
|
||||||
setState(() {
|
_authData = AuthenticationData.unauthenticated();
|
||||||
_authData = AuthenticationData.unauthenticated();
|
});
|
||||||
});
|
Phoenix.rebirth(context);
|
||||||
Phoenix.rebirth(context);
|
},
|
||||||
},
|
child: DynamicColorBuilder(
|
||||||
child: DynamicColorBuilder(
|
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
||||||
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) => MaterialApp(
|
debugShowCheckedModeBanner: false,
|
||||||
debugShowCheckedModeBanner: false,
|
title: 'Contacts++',
|
||||||
title: 'Contacts++',
|
theme: ThemeData(
|
||||||
theme: ThemeData(
|
useMaterial3: true,
|
||||||
useMaterial3: true,
|
textTheme: _typography.black,
|
||||||
textTheme: _typography.black,
|
colorScheme:
|
||||||
colorScheme: lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
|
lightDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.light),
|
||||||
),
|
),
|
||||||
darkTheme: ThemeData(
|
darkTheme: ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
textTheme: _typography.white,
|
textTheme: _typography.white,
|
||||||
colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
|
colorScheme: darkDynamic ?? ColorScheme.fromSeed(seedColor: Colors.purple, brightness: Brightness.dark),
|
||||||
),
|
),
|
||||||
themeMode: ThemeMode.values[widget.settingsClient.currentSettings.themeMode.valueOrDefault],
|
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
|
home: Builder(
|
||||||
builder: (context) {
|
// Builder is necessary here since we need a context which has access to the ClientHolder
|
||||||
showUpdateDialogOnFirstBuild(context);
|
builder: (context) {
|
||||||
final clientHolder = ClientHolder.of(context);
|
showUpdateDialogOnFirstBuild(context);
|
||||||
return _authData.isAuthenticated ?
|
final clientHolder = ClientHolder.of(context);
|
||||||
ChangeNotifierProvider( // This doesn't need to be a proxy provider since the arguments should never change during it's lifetime.
|
return _authData.isAuthenticated
|
||||||
create: (context) =>
|
? MultiProvider(
|
||||||
MessagingClient(
|
providers: [
|
||||||
apiClient: clientHolder.apiClient,
|
Provider(
|
||||||
notificationClient: clientHolder.notificationClient,
|
create: (context) => MessagingClient(
|
||||||
|
apiClient: clientHolder.apiClient,
|
||||||
|
notificationClient: clientHolder.notificationClient,
|
||||||
|
),
|
||||||
|
dispose: (context, value) => value.dispose(),
|
||||||
),
|
),
|
||||||
child: const FriendsList(),
|
Provider(
|
||||||
) :
|
create: (context) => SessionClient(
|
||||||
LoginScreen(
|
apiClient: clientHolder.apiClient,
|
||||||
onLoginSuccessful: (AuthenticationData authData) async {
|
),
|
||||||
if (authData.isAuthenticated) {
|
),
|
||||||
setState(() {
|
],
|
||||||
_authData = authData;
|
child: Scaffold(
|
||||||
});
|
appBar: PreferredSize(
|
||||||
}
|
preferredSize: const Size.fromHeight(kToolbarHeight),
|
||||||
},
|
child: AnimatedSwitcher(
|
||||||
);
|
duration: const Duration(milliseconds: 200),
|
||||||
}
|
child: _appBars[_selectedPage],
|
||||||
)
|
),
|
||||||
|
),
|
||||||
|
body: PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
children: const [
|
||||||
|
FriendsList(),
|
||||||
|
SessionList(),
|
||||||
|
SettingsPage(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: const Border(top: BorderSide(width: 1, color: Colors.black)),
|
||||||
|
color: Theme.of(context).colorScheme.background,
|
||||||
|
),
|
||||||
|
child: 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,3 +1,4 @@
|
||||||
|
import 'package:contacts_plus_plus/config.dart';
|
||||||
import 'package:contacts_plus_plus/string_formatter.dart';
|
import 'package:contacts_plus_plus/string_formatter.dart';
|
||||||
|
|
||||||
class Session {
|
class Session {
|
||||||
|
@ -17,11 +18,22 @@ class Session {
|
||||||
final String hostUsername;
|
final String hostUsername;
|
||||||
final SessionAccessLevel accessLevel;
|
final SessionAccessLevel accessLevel;
|
||||||
|
|
||||||
Session({required this.id, required this.name, required this.sessionUsers, required this.thumbnail,
|
Session({
|
||||||
required this.maxUsers, required this.hasEnded, required this.isValid, required this.description,
|
required this.id,
|
||||||
required this.tags, required this.headlessHost, required this.hostUserId, required this.hostUsername,
|
required this.name,
|
||||||
|
required this.sessionUsers,
|
||||||
|
required this.thumbnail,
|
||||||
|
required this.maxUsers,
|
||||||
|
required this.hasEnded,
|
||||||
|
required this.isValid,
|
||||||
|
required this.description,
|
||||||
|
required this.tags,
|
||||||
|
required this.headlessHost,
|
||||||
|
required this.hostUserId,
|
||||||
|
required this.hostUsername,
|
||||||
required this.accessLevel,
|
required this.accessLevel,
|
||||||
}) : formattedName = FormatNode.fromText(name), formattedDescription = FormatNode.fromText(description);
|
}) : formattedName = FormatNode.fromText(name),
|
||||||
|
formattedDescription = FormatNode.fromText(description);
|
||||||
|
|
||||||
factory Session.none() {
|
factory Session.none() {
|
||||||
return Session(
|
return Session(
|
||||||
|
@ -37,8 +49,7 @@ class Session {
|
||||||
headlessHost: false,
|
headlessHost: false,
|
||||||
hostUserId: "",
|
hostUserId: "",
|
||||||
hostUsername: "",
|
hostUsername: "",
|
||||||
accessLevel: SessionAccessLevel.unknown
|
accessLevel: SessionAccessLevel.unknown);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isNone => id.isEmpty && isValid == false;
|
bool get isNone => id.isEmpty && isValid == false;
|
||||||
|
@ -62,7 +73,7 @@ class Session {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map toMap({bool shallow=false}) {
|
Map toMap({bool shallow = false}) {
|
||||||
return {
|
return {
|
||||||
"sessionId": id,
|
"sessionId": id,
|
||||||
"name": name,
|
"name": name,
|
||||||
|
@ -80,8 +91,6 @@ class Session {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool get isLive => !hasEnded && isValid;
|
bool get isLive => !hasEnded && isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +110,8 @@ enum SessionAccessLevel {
|
||||||
};
|
};
|
||||||
|
|
||||||
factory SessionAccessLevel.fromName(String? name) {
|
factory SessionAccessLevel.fromName(String? name) {
|
||||||
return SessionAccessLevel.values.firstWhere((element) => element.name.toLowerCase() == name?.toLowerCase(),
|
return SessionAccessLevel.values.firstWhere(
|
||||||
|
(element) => element.name.toLowerCase() == name?.toLowerCase(),
|
||||||
orElse: () => SessionAccessLevel.unknown,
|
orElse: () => SessionAccessLevel.unknown,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -136,4 +146,56 @@ class SessionUser {
|
||||||
"outputDevice": outputDevice,
|
"outputDevice": outputDevice,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SessionFilterSettings {
|
||||||
|
final String name;
|
||||||
|
final bool includeEnded;
|
||||||
|
final bool includeIncompatible;
|
||||||
|
final String hostName;
|
||||||
|
final int minActiveUsers;
|
||||||
|
final bool includeEmptyHeadless;
|
||||||
|
|
||||||
|
const SessionFilterSettings({
|
||||||
|
required this.name,
|
||||||
|
required this.includeEnded,
|
||||||
|
required this.includeIncompatible,
|
||||||
|
required this.hostName,
|
||||||
|
required this.minActiveUsers,
|
||||||
|
required this.includeEmptyHeadless,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SessionFilterSettings.empty() => const SessionFilterSettings(
|
||||||
|
name: "",
|
||||||
|
includeEnded: false,
|
||||||
|
includeIncompatible: false,
|
||||||
|
hostName: "",
|
||||||
|
minActiveUsers: 0,
|
||||||
|
includeEmptyHeadless: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
String buildRequestString() => "?includeEmptyHeadless=$includeEmptyHeadless"
|
||||||
|
"${"&includeEnded=$includeEnded"}"
|
||||||
|
"${name.isNotEmpty ? "&name=$name" : ""}"
|
||||||
|
"${!includeIncompatible ? "&compatibilityHash=${Uri.encodeComponent(Config.latestCompatHash)}" : ""}"
|
||||||
|
"${hostName.isNotEmpty ? "&hostName=$hostName" : ""}"
|
||||||
|
"${minActiveUsers > 0 ? "&minActiveUsers=$minActiveUsers" : ""}";
|
||||||
|
|
||||||
|
SessionFilterSettings copyWith({
|
||||||
|
String? name,
|
||||||
|
bool? includeEnded,
|
||||||
|
bool? includeIncompatible,
|
||||||
|
String? hostName,
|
||||||
|
int? minActiveUsers,
|
||||||
|
bool? includeEmptyHeadless,
|
||||||
|
}) {
|
||||||
|
return SessionFilterSettings(
|
||||||
|
name: name ?? this.name,
|
||||||
|
includeEnded: includeEnded ?? this.includeEnded,
|
||||||
|
includeIncompatible: includeIncompatible ?? this.includeIncompatible,
|
||||||
|
hostName: hostName ?? this.hostName,
|
||||||
|
minActiveUsers: minActiveUsers ?? this.minActiveUsers,
|
||||||
|
includeEmptyHeadless: includeEmptyHeadless ?? this.includeEmptyHeadless,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,29 +1,11 @@
|
||||||
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/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/default_error_widget.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/friends/expanding_input_fab.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/friends/friend_list_tile.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/my_profile_dialog.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:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:provider/provider.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 {
|
class FriendsList extends StatefulWidget {
|
||||||
const FriendsList({super.key});
|
const FriendsList({super.key});
|
||||||
|
|
||||||
|
@ -31,261 +13,78 @@ class FriendsList extends StatefulWidget {
|
||||||
State<FriendsList> createState() => _FriendsListState();
|
State<FriendsList> createState() => _FriendsListState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FriendsListState extends State<FriendsList> {
|
class _FriendsListState extends State<FriendsList> with AutomaticKeepAliveClientMixin {
|
||||||
Future<UserStatus>? _userStatusFuture;
|
|
||||||
ClientHolder? _clientHolder;
|
|
||||||
String _searchFilter = "";
|
String _searchFilter = "";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() async {
|
Widget build(BuildContext context) {
|
||||||
super.didChangeDependencies();
|
super.build(context);
|
||||||
final clientHolder = ClientHolder.of(context);
|
return ChangeNotifierProvider.value(
|
||||||
if (_clientHolder != clientHolder) {
|
value: Provider.of<MessagingClient>(context, listen: false),
|
||||||
_clientHolder = clientHolder;
|
child: Stack(
|
||||||
_refreshUserStatus();
|
alignment: Alignment.topCenter,
|
||||||
}
|
children: [
|
||||||
}
|
Consumer<MessagingClient>(builder: (context, mClient, _) {
|
||||||
|
if (mClient.initStatus == null) {
|
||||||
void _refreshUserStatus() {
|
return const LinearProgressIndicator();
|
||||||
final apiClient = _clientHolder!.apiClient;
|
} else if (mClient.initStatus!.isNotEmpty) {
|
||||||
_userStatusFuture = UserApi.getUserStatus(apiClient, userId: apiClient.userId).then((value) async {
|
return Column(
|
||||||
if (value.onlineStatus == OnlineStatus.offline) {
|
children: [
|
||||||
final newStatus = value.copyWith(
|
Expanded(
|
||||||
onlineStatus: OnlineStatus.values[_clientHolder!.settingsClient.currentSettings.lastOnlineStatus
|
child: DefaultErrorWidget(
|
||||||
.valueOrDefault]
|
message: mClient.initStatus,
|
||||||
);
|
onRetry: () async {
|
||||||
await UserApi.setStatus(apiClient, status: newStatus);
|
mClient.resetInitStatus();
|
||||||
return newStatus;
|
mClient.refreshFriendsListWithErrorHandler();
|
||||||
}
|
},
|
||||||
return value;
|
),
|
||||||
});
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
bool get wantKeepAlive => true;
|
||||||
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();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
].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 = "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
231
lib/widgets/friends/friends_list_app_bar.dart
Normal file
231
lib/widgets/friends/friends_list_app_bar.dart
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
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++"),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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});
|
||||||
|
}
|
|
@ -162,7 +162,6 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 64),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
autofocus: true,
|
|
||||||
controller: _usernameController,
|
controller: _usernameController,
|
||||||
onEditingComplete: () => _passwordFocusNode.requestFocus(),
|
onEditingComplete: () => _passwordFocusNode.requestFocus(),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|
|
@ -5,9 +5,8 @@ import 'package:contacts_plus_plus/models/message.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/formatted_text.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/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/messages/message_state_indicator.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/session_view.dart';
|
import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MessageSessionInvite extends StatelessWidget {
|
class MessageSessionInvite extends StatelessWidget {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/formatted_text.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/generic_avatar.dart';
|
||||||
import 'package:contacts_plus_plus/widgets/session_view.dart';
|
import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SessionPopup extends StatelessWidget {
|
class SessionPopup extends StatelessWidget {
|
||||||
|
|
|
@ -1,185 +0,0 @@
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
|
||||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
|
||||||
import 'package:contacts_plus_plus/models/session.dart';
|
|
||||||
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
|
||||||
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:photo_view/photo_view.dart';
|
|
||||||
|
|
||||||
class SessionView extends StatelessWidget {
|
|
||||||
const SessionView({required this.session, super.key});
|
|
||||||
|
|
||||||
final Session session;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: CustomScrollView(
|
|
||||||
physics: const BouncingScrollPhysics(decelerationRate: ScrollDecelerationRate.fast),
|
|
||||||
slivers: [
|
|
||||||
SliverAppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.arrow_back_outlined,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
pinned: true,
|
|
||||||
snap: false,
|
|
||||||
floating: false,
|
|
||||||
expandedHeight: 192,
|
|
||||||
surfaceTintColor: Theme.of(context).colorScheme.surfaceVariant,
|
|
||||||
centerTitle: true,
|
|
||||||
title: FormattedText(
|
|
||||||
session.formattedName,
|
|
||||||
maxLines: 1,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
bottom: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(1),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 1,
|
|
||||||
color: Colors.black,
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
collapseMode: CollapseMode.pin,
|
|
||||||
background: CachedNetworkImage(
|
|
||||||
imageUrl: Aux.neosDbToHttp(session.thumbnail),
|
|
||||||
imageBuilder: (context, image) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: () async {
|
|
||||||
await Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => PhotoView(
|
|
||||||
minScale: PhotoViewComputedScale.contained,
|
|
||||||
imageProvider: image,
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: session.id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Hero(
|
|
||||||
tag: session.id,
|
|
||||||
child: Image(
|
|
||||||
image: image,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
errorWidget: (context, url, error) => const Icon(
|
|
||||||
Icons.broken_image,
|
|
||||||
size: 64,
|
|
||||||
),
|
|
||||||
placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8),
|
|
||||||
child: session.formattedDescription.isEmpty
|
|
||||||
? Text("No description", style: Theme.of(context).textTheme.labelLarge)
|
|
||||||
: FormattedText(
|
|
||||||
session.formattedDescription,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const ListSectionHeader(
|
|
||||||
leadingText: "Tags:",
|
|
||||||
showLine: false,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0),
|
|
||||||
child: Text(
|
|
||||||
session.tags.isEmpty ? "None" : session.tags.join(", "),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const ListSectionHeader(
|
|
||||||
leadingText: "Details:",
|
|
||||||
showLine: false,
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Access: ",
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
session.accessLevel.toReadableString(),
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Headless: ",
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
session.headlessHost ? "Yes" : "No",
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListSectionHeader(
|
|
||||||
leadingText: "Users",
|
|
||||||
trailingText:
|
|
||||||
"${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")}",
|
|
||||||
showLine: false,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(BuildContext context, int index) {
|
|
||||||
final user = session.sessionUsers[index % session.sessionUsers.length];
|
|
||||||
return ListTile(
|
|
||||||
dense: true,
|
|
||||||
title: Text(
|
|
||||||
user.username,
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
user.isPresent ? "Active" : "Inactive",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: session.sessionUsers.length,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
190
lib/widgets/sessions/session_filter_dialog.dart
Normal file
190
lib/widgets/sessions/session_filter_dialog.dart
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:contacts_plus_plus/clients/session_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class SessionFilterDialog extends StatefulWidget {
|
||||||
|
const SessionFilterDialog({required this.lastFilter, super.key});
|
||||||
|
|
||||||
|
final SessionFilterSettings lastFilter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _SessionFilterDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SessionFilterDialogState extends State<SessionFilterDialog> {
|
||||||
|
final TextEditingController _sessionNameController = TextEditingController();
|
||||||
|
final TextEditingController _hostNameController = TextEditingController();
|
||||||
|
late SessionFilterSettings _currentFilter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant SessionFilterDialog oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_currentFilter = widget.lastFilter;
|
||||||
|
if (oldWidget.lastFilter != widget.lastFilter) {
|
||||||
|
_sessionNameController.text = widget.lastFilter.name;
|
||||||
|
_hostNameController.text = widget.lastFilter.hostName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentFilter = widget.lastFilter;
|
||||||
|
_sessionNameController.text = widget.lastFilter.name;
|
||||||
|
_hostNameController.text = widget.lastFilter.hostName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_sessionNameController.dispose();
|
||||||
|
_hostNameController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("Filter"),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _sessionNameController,
|
||||||
|
maxLines: 1,
|
||||||
|
onChanged: (value) {
|
||||||
|
_currentFilter = _currentFilter.copyWith(name: value);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
),
|
||||||
|
labelText: 'Session Name',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _hostNameController,
|
||||||
|
onChanged: (value) {
|
||||||
|
_currentFilter = _currentFilter.copyWith(hostName: value);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(32),
|
||||||
|
),
|
||||||
|
labelText: 'Host Name',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
const Text("Minimum Users"),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_currentFilter =
|
||||||
|
_currentFilter.copyWith(minActiveUsers: max(0, _currentFilter.minActiveUsers - 1));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.remove_circle_outline),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${_currentFilter.minActiveUsers}",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_currentFilter = _currentFilter.copyWith(minActiveUsers: _currentFilter.minActiveUsers + 1, includeEmptyHeadless: false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.add_circle_outline),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SessionFilterCheckbox(
|
||||||
|
label: "Include Ended",
|
||||||
|
value: _currentFilter.includeEnded,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_currentFilter = _currentFilter.copyWith(includeEnded: value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SessionFilterCheckbox(
|
||||||
|
label: "Include Empty Headless",
|
||||||
|
value: _currentFilter.includeEmptyHeadless && _currentFilter.minActiveUsers == 0,
|
||||||
|
onChanged: _currentFilter.minActiveUsers > 0 ? null : (value) {
|
||||||
|
setState(() {
|
||||||
|
_currentFilter = _currentFilter.copyWith(includeEmptyHeadless: value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SessionFilterCheckbox(
|
||||||
|
label: "Include Incompatible",
|
||||||
|
value: _currentFilter.includeIncompatible,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_currentFilter = _currentFilter.copyWith(includeIncompatible: value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Provider.of<SessionClient>(context, listen: false).filterSettings = _currentFilter;
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text("Okay"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SessionFilterCheckbox extends StatelessWidget {
|
||||||
|
const SessionFilterCheckbox({required this.label, this.onChanged, this.value, super.key});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final void Function(bool? value)? onChanged;
|
||||||
|
final bool? value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(label),
|
||||||
|
Checkbox(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
161
lib/widgets/sessions/session_list.dart
Normal file
161
lib/widgets/sessions/session_list.dart
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||||
|
import 'package:contacts_plus_plus/clients/session_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/sessions/session_view.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class SessionList extends StatefulWidget {
|
||||||
|
const SessionList({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SessionList> createState() => _SessionListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SessionListState extends State<SessionList> with AutomaticKeepAliveClientMixin {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context);
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: Provider.of<SessionClient>(context),
|
||||||
|
child: Consumer<SessionClient>(
|
||||||
|
builder: (BuildContext context, SessionClient sClient, Widget? child) {
|
||||||
|
return FutureBuilder<List<Session>>(
|
||||||
|
future: sClient.sessionsFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final data = snapshot.data ?? [];
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
sClient.reloadSessions();
|
||||||
|
try {
|
||||||
|
await sClient.sessionsFuture;
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: data.isEmpty && snapshot.connectionState == ConnectionState.done
|
||||||
|
? const DefaultErrorWidget(
|
||||||
|
title: "No Sessions Found",
|
||||||
|
message: "Try to adjust your filters",
|
||||||
|
iconOverride: Icons.public_off,
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
|
child: GridView.builder(
|
||||||
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
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()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) const LinearProgressIndicator()
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true;
|
||||||
|
}
|
48
lib/widgets/sessions/session_list_app_bar.dart
Normal file
48
lib/widgets/sessions/session_list_app_bar.dart
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:contacts_plus_plus/clients/session_client.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/sessions/session_filter_dialog.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class SessionListAppBar extends StatefulWidget {
|
||||||
|
const SessionListAppBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SessionListAppBar> createState() => _SessionListAppBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SessionListAppBarState extends State<SessionListAppBar> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppBar(
|
||||||
|
title: const Text("Sessions"),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 4.0),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final sessionClient = Provider.of<SessionClient>(context, listen: false);
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Provider.value(
|
||||||
|
value: sessionClient,
|
||||||
|
child: SessionFilterDialog(
|
||||||
|
lastFilter: sessionClient.filterSettings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.filter_alt_outlined),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
204
lib/widgets/sessions/session_view.dart
Normal file
204
lib/widgets/sessions/session_view.dart
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
import 'package:cached_network_image/cached_network_image.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';
|
||||||
|
import 'package:contacts_plus_plus/models/session.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/formatted_text.dart';
|
||||||
|
import 'package:contacts_plus_plus/widgets/settings_page.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
class SessionView extends StatefulWidget {
|
||||||
|
const SessionView({required this.session, super.key});
|
||||||
|
|
||||||
|
final Session session;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SessionView> createState() => _SessionViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SessionViewState extends State<SessionView> {
|
||||||
|
|
||||||
|
Future<Session>? _sessionFuture;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_sessionFuture = Future.value(widget.session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder(
|
||||||
|
future: _sessionFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final session = snapshot.data ?? widget.session;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_outlined,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: FormattedText(
|
||||||
|
session.formattedName,
|
||||||
|
maxLines: 1,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
setState(() {
|
||||||
|
_sessionFuture = SessionApi.getSession(ClientHolder.of(context).apiClient, sessionId: session.id);
|
||||||
|
});
|
||||||
|
await _sessionFuture;
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 192,
|
||||||
|
child: Hero(
|
||||||
|
tag: session.id,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: Aux.neosDbToHttp(session.thumbnail),
|
||||||
|
imageBuilder: (context, image) {
|
||||||
|
return Material(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
await Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => PhotoView(
|
||||||
|
minScale: PhotoViewComputedScale.contained,
|
||||||
|
imageProvider: image,
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: session.id),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Image(
|
||||||
|
image: image,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
errorWidget: (context, url, error) => const Icon(
|
||||||
|
Icons.broken_image,
|
||||||
|
size: 64,
|
||||||
|
),
|
||||||
|
placeholder: (context, uri) => const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8),
|
||||||
|
child: session.formattedDescription.isEmpty
|
||||||
|
? Text("No description", style: Theme.of(context).textTheme.labelLarge)
|
||||||
|
: FormattedText(
|
||||||
|
session.formattedDescription,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const ListSectionHeader(
|
||||||
|
leadingText: "Tags:",
|
||||||
|
showLine: false,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8.0),
|
||||||
|
child: Text(
|
||||||
|
session.tags.isEmpty ? "None" : session.tags.join(", "),
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const ListSectionHeader(
|
||||||
|
leadingText: "Details:",
|
||||||
|
showLine: false,
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Access: ",
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
session.accessLevel.toReadableString(),
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16.0, bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Headless: ",
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
session.headlessHost ? "Yes" : "No",
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListSectionHeader(
|
||||||
|
leadingText: "Users",
|
||||||
|
trailingText:
|
||||||
|
"${session.sessionUsers.length.toString().padLeft(2, "0")}/${session.maxUsers.toString().padLeft(2, "0")}",
|
||||||
|
showLine: false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
] +
|
||||||
|
session.sessionUsers
|
||||||
|
.map((user) => ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(
|
||||||
|
user.username,
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
user.isPresent ? "Active" : "Inactive",
|
||||||
|
textAlign: TextAlign.start,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
lib/widgets/settings_app_bar.dart
Normal file
21
lib/widgets/settings_app_bar.dart
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SettingsAppBar extends StatelessWidget {
|
||||||
|
const SettingsAppBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AppBar(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
title: const Text("Settings"),
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(1),
|
||||||
|
child: Container(
|
||||||
|
height: 1,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,17 +11,7 @@ class SettingsPage extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final sClient = ClientHolder.of(context).settingsClient;
|
final sClient = ClientHolder.of(context).settingsClient;
|
||||||
return Scaffold(
|
return ListView(
|
||||||
appBar: AppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.arrow_back),
|
|
||||||
),
|
|
||||||
title: const Text("Settings"),
|
|
||||||
),
|
|
||||||
body: ListView(
|
|
||||||
children: [
|
children: [
|
||||||
const ListSectionHeader(leadingText: "Notifications"),
|
const ListSectionHeader(leadingText: "Notifications"),
|
||||||
BooleanSettingsTile(
|
BooleanSettingsTile(
|
||||||
|
@ -110,8 +100,7 @@ class SettingsPage extends StatelessWidget {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue