reorganize pages into being under widgets
This commit is contained in:
parent
d712bb3193
commit
9f070fcdba
9 changed files with 10 additions and 10 deletions
51
lib/widgets/pages/chat.dart
Normal file
51
lib/widgets/pages/chat.dart
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/init_complete.dart";
|
||||
import "package:nexus/controllers/key.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/widgets/sidebar.dart";
|
||||
import "package:nexus/widgets/room_chat/room_chat.dart";
|
||||
import "package:nexus/widgets/loading.dart";
|
||||
|
||||
class const ChatPage({super.key}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isDesktop = constraints.maxWidth > 650;
|
||||
final showMembersByDefault = constraints.maxWidth > 1000;
|
||||
final initComplete = ref.watch(InitCompleteController.provider);
|
||||
final roomId = ref
|
||||
.watch(KeyController.provider(KeyController.roomKey))
|
||||
.requireValue;
|
||||
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
appBar: initComplete ? null : Appbar(),
|
||||
body: initComplete
|
||||
? Row(
|
||||
children: [
|
||||
if (isDesktop) Sidebar(isDesktop: isDesktop),
|
||||
Expanded(
|
||||
child: RoomChat(
|
||||
key: ValueKey(roomId),
|
||||
roomId: roomId,
|
||||
isDesktop: isDesktop,
|
||||
showMembersByDefault: showMembersByDefault,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
children: [Loading(), Text("Syncing...")],
|
||||
),
|
||||
),
|
||||
drawer: isDesktop || !initComplete
|
||||
? null
|
||||
: Sidebar(isDesktop: isDesktop),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
164
lib/widgets/pages/notifications.dart
Normal file
164
lib/widgets/pages/notifications.dart
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:m3e_buttons/m3e_buttons.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/notifications.dart";
|
||||
import "package:nexus/models/event.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/widgets/error_dialog.dart";
|
||||
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||
import "package:nexus/widgets/loading.dart";
|
||||
import "package:nexus/widgets/renderers/event.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
class const NotificationsPage({
|
||||
final String? highlightedEventId,
|
||||
final bool defaultToAllNotifications = false,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final options = <M3EToggleButtonGroupAction, UnreadType>{
|
||||
M3EToggleButtonGroupAction(
|
||||
checkedLabel: Text("Mentions"),
|
||||
icon: Icon(Icons.alternate_email),
|
||||
): .highlight,
|
||||
M3EToggleButtonGroupAction(
|
||||
checkedLabel: Text("All Notifications"),
|
||||
icon: Icon(Icons.notifications),
|
||||
): .notify,
|
||||
};
|
||||
final unreadTypeIndex = useState(defaultToAllNotifications ? 1 : 0);
|
||||
|
||||
final highlightedId = useState(highlightedEventId);
|
||||
final listController = useRef(ListController());
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final provider = NotificationsController.provider((
|
||||
options.values.toList()[unreadTypeIndex.value],
|
||||
null,
|
||||
));
|
||||
final notifications = ref.watch(provider);
|
||||
final notifier = ref.watch(provider.notifier);
|
||||
|
||||
useEffect(() {
|
||||
if (highlightedId.value == null) return null;
|
||||
Timer? timer;
|
||||
|
||||
void listener() =>
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!listController.value.isAttached) return;
|
||||
|
||||
final notifications = await ref.watch(provider.future);
|
||||
|
||||
final index = notifications.indexWhere(
|
||||
(element) => element.eventId == highlightedId.value!,
|
||||
);
|
||||
|
||||
if (index == -1) return;
|
||||
|
||||
listController.value.animateToItem(
|
||||
index: index,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => .new(milliseconds: 700),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
timer = Timer(.new(seconds: 1), () {
|
||||
highlightedId.value = null;
|
||||
});
|
||||
listController.value.removeListener(listener);
|
||||
});
|
||||
|
||||
listController.value.addListener(listener);
|
||||
return timer?.cancel;
|
||||
}, []);
|
||||
|
||||
useEffect(() {
|
||||
Future<void> listener() async {
|
||||
if (!scrollController.hasClients || notifications.isLoading) return;
|
||||
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent) {
|
||||
await notifier.loadOlder();
|
||||
}
|
||||
}
|
||||
|
||||
scrollController.addListener(listener);
|
||||
return () => scrollController.removeListener(listener);
|
||||
}, [scrollController, notifications]);
|
||||
|
||||
return Scaffold(
|
||||
appBar: Appbar(title: Text("Notifications")),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
if (notifications is AsyncLoading && notifications.value != null)
|
||||
const LinearProgressIndicator(minHeight: 2),
|
||||
Expanded(
|
||||
child: switch (notifications) {
|
||||
AsyncData(:final value) || AsyncLoading(:final value?) =>
|
||||
value.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
"No notifications yet",
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
)
|
||||
: SuperListView.builder(
|
||||
listController: listController.value,
|
||||
controller: scrollController,
|
||||
itemCount: value.length,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
reverse: true,
|
||||
itemBuilder: (context, index) {
|
||||
final event = value[index];
|
||||
final isHighlighted =
|
||||
event.eventId == highlightedId.value;
|
||||
|
||||
return Padding(
|
||||
padding: .only(top: 8),
|
||||
child: HighlightWrapper(
|
||||
InkWell(
|
||||
onTap: () {
|
||||
// TODO: Jump to event
|
||||
},
|
||||
child: EventRenderer(event),
|
||||
),
|
||||
isHighlighted: isHighlighted,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
AsyncLoading() => const Loading(),
|
||||
AsyncError(:final error, :final stackTrace) => ErrorDialog(
|
||||
error,
|
||||
stackTrace,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: .topRight,
|
||||
child: Padding(
|
||||
padding: .all(16),
|
||||
child: M3EToggleButtonGroup(
|
||||
selectedIndex: unreadTypeIndex.value,
|
||||
onSelectedIndexChanged: (index) =>
|
||||
unreadTypeIndex.value = index ?? unreadTypeIndex.value,
|
||||
actions: options.keys.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
236
lib/widgets/pages/select_server.dart
Normal file
236
lib/widgets/pages/select_server.dart
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import "package:app_links/app_links.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:flutter_svg/flutter_svg.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/auth_url.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/client_id.dart";
|
||||
import "package:nexus/helpers/launch_helper.dart";
|
||||
import "package:nexus/main.dart";
|
||||
import "package:nexus/models/homeserver.dart";
|
||||
import "package:nexus/widgets/pages/settings.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/widgets/divider_text.dart";
|
||||
|
||||
class const SelectServerPage({super.key}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final launch = ref.watch(LaunchHelper.provider).launchUrl;
|
||||
|
||||
final isLoading = useState(false);
|
||||
final homeserverUrl = useTextEditingController();
|
||||
|
||||
Future<void> setHomeserver(Uri? newHomeserver) async {
|
||||
if (newHomeserver == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Failed to parse homeserver URL. Are you sure you typed it correctly?",
|
||||
style: .new(color: theme.colorScheme.onErrorContainer),
|
||||
),
|
||||
backgroundColor: theme.colorScheme.errorContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
if (newHomeserver?.hasScheme == false) {
|
||||
newHomeserver = Uri.https(newHomeserver!.path);
|
||||
}
|
||||
|
||||
final newUrl = await ref
|
||||
.read(ClientController.provider.notifier)
|
||||
.discoverHomeserver(newHomeserver!);
|
||||
|
||||
if (context.mounted) {
|
||||
Future<void> tryLogin(Uri url) async {
|
||||
final codeResponse = await ref.watch(
|
||||
AuthUrlController.provider(url).future,
|
||||
);
|
||||
|
||||
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url);
|
||||
|
||||
AppLinks().uriLinkStream.listen((encodedUri) async {
|
||||
final state = encodedUri.queryParameters["state"];
|
||||
if (state != codeResponse.state) return;
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
final code = encodedUri.queryParameters["code"];
|
||||
|
||||
if (code != null) {
|
||||
await ref
|
||||
.watch(ClientController.provider.notifier)
|
||||
.exchangeToken(
|
||||
.new(
|
||||
homeserverUrl: url,
|
||||
codeVerifier: codeResponse.codeVerifier,
|
||||
redirectUri: .new(
|
||||
scheme: "nexus.federated.nexus",
|
||||
path: "/",
|
||||
),
|
||||
code: code,
|
||||
clientId: await ref.watch(
|
||||
ClientIdController.provider(url).future,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
showError(error, stackTrace);
|
||||
rethrow;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (newUrl == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Homeserver verification failed.",
|
||||
style: .new(color: theme.colorScheme.onErrorContainer),
|
||||
),
|
||||
action: SnackBarAction(
|
||||
onPressed: () => tryLogin(newHomeserver!).onError(showError),
|
||||
label: "Attempt log in anyways",
|
||||
textColor: theme.colorScheme.onErrorContainer,
|
||||
),
|
||||
backgroundColor: theme.colorScheme.errorContainer,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await tryLogin(newUrl);
|
||||
}
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
showError(error, stackTrace);
|
||||
rethrow;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
actions: .new([
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
showDialog(context: context, builder: (_) => SettingsPage()),
|
||||
icon: Icon(Icons.settings),
|
||||
),
|
||||
]),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: .new(maxWidth: 600),
|
||||
child: ListView(
|
||||
padding: .symmetric(vertical: 8, horizontal: 12),
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SvgPicture.asset("assets/bundled/icon.svg", width: 128),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
Text("Nexus", style: theme.textTheme.displayMedium),
|
||||
Text(
|
||||
"A Simple Matrix Client",
|
||||
style: theme.textTheme.headlineMedium,
|
||||
overflow: .ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(padding: .symmetric(vertical: 12), child: Divider()),
|
||||
DividerText("Enter a homeserver domain:"),
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
textInputAction: .done,
|
||||
autofocus: true,
|
||||
onSubmitted: (text) => setHomeserver(.tryParse(text)),
|
||||
controller: homeserverUrl,
|
||||
decoration: .new(
|
||||
labelText: "Homeserver URL",
|
||||
hintText: "matrix.org",
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton.filled(
|
||||
tooltip: "Confirm homeserver choice",
|
||||
onPressed: isLoading.value
|
||||
? null
|
||||
: () => setHomeserver(.tryParse(homeserverUrl.text)),
|
||||
icon: Icon(Icons.check),
|
||||
),
|
||||
],
|
||||
),
|
||||
DividerText("Or, choose from some popular homeservers:"),
|
||||
...(<Homeserver>[
|
||||
.new(
|
||||
name: "Matrix.org",
|
||||
description: "The Matrix.org Foundation offers the matrix.org homeserver as an easy entry point for anyone wanting to try out Matrix.",
|
||||
url: .https("matrix.org"),
|
||||
iconUrl:
|
||||
"https://raw.githubusercontent.com/element-hq/logos/refs/heads/master/matrix/matrix-favicon${Theme.brightnessOf(context) == Brightness.dark ? "-white" : ""}.png",
|
||||
),
|
||||
.new(
|
||||
name: "Federated Nexus",
|
||||
description: "Federated Nexus is a community resource hosting multiple FOSS (especially federated) services, including Matrix and Forgejo. By the same developers who made Nexus client.",
|
||||
url: .https("federated.nexus"),
|
||||
iconUrl: "https://federated.nexus/images/icon.png",
|
||||
),
|
||||
.new(
|
||||
name: "Unredacted",
|
||||
description: "Unredacted is a 501(c)(3) non-profit organization that builds Internet infrastructure and services to help people evade censorship and protect their right to privacy.",
|
||||
url: .https("unredacted.org", "services/si/matrix"),
|
||||
iconUrl: "https://unredacted.org/favicon.ico",
|
||||
),
|
||||
].map(
|
||||
(homeserver) => Card(
|
||||
child: ListTile(
|
||||
enabled: !isLoading.value,
|
||||
title: Text(homeserver.name),
|
||||
leading: Image.network(
|
||||
homeserver.iconUrl,
|
||||
errorBuilder: (_, _, _) => SizedBox.shrink(),
|
||||
height: 32,
|
||||
),
|
||||
subtitle: Text(homeserver.description),
|
||||
onTap: isLoading.value
|
||||
? null
|
||||
: () => setHomeserver(homeserver.url),
|
||||
trailing: IconButton(
|
||||
tooltip: "Launch homeserver info page",
|
||||
onPressed: () => launch(homeserver.url),
|
||||
icon: Icon(Icons.info_outline),
|
||||
),
|
||||
),
|
||||
),
|
||||
)),
|
||||
|
||||
TextButton(
|
||||
onPressed: () => launch(.https("servers.joinmatrix.org")),
|
||||
child: Text("See more homeservers..."),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
247
lib/widgets/pages/settings.dart
Normal file
247
lib/widgets/pages/settings.dart
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
import "package:collection/collection.dart";
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:m3e_card_list/m3e_card_list.dart";
|
||||
import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
||||
import "package:nexus/controllers/settings_sections.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/helpers/extensions/show_about_dialog.dart";
|
||||
import "package:nexus/widgets/pages/settings_category.dart";
|
||||
import "package:nexus/widgets/divider_text.dart";
|
||||
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
class const SettingsPage({super.key}) extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) => LayoutBuilder(
|
||||
builder: (_, constraints) => HookBuilder(
|
||||
builder: (context) {
|
||||
final categoriesArePages = constraints.maxWidth < 550;
|
||||
|
||||
final selected = useState(0);
|
||||
|
||||
final highlightedMatch = useState<(int, int)?>(null);
|
||||
final listController = useRef(ListController());
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final searchBar = SearchAnchor.bar(
|
||||
barHintText: "Search...",
|
||||
isFullScreen: categoriesArePages,
|
||||
suggestionsBuilder: (suggestionsContext, controller) async {
|
||||
final categories = await ref.watch(
|
||||
SettingsSectionsController.provider.future,
|
||||
);
|
||||
final query = controller.text.toLowerCase();
|
||||
|
||||
final matches = categories.values.flattenedToList
|
||||
.asMap()
|
||||
.entries
|
||||
.expand(
|
||||
(categoryEntry) => categoryEntry.value.settings
|
||||
.asMap()
|
||||
.entries
|
||||
.where(
|
||||
(settingEntry) =>
|
||||
settingEntry.value.title.toLowerCase().contains(
|
||||
query,
|
||||
) ||
|
||||
settingEntry.value.description
|
||||
.toLowerCase()
|
||||
.contains(query),
|
||||
)
|
||||
.map(
|
||||
(settingEntry) => (
|
||||
(categoryEntry.key, settingEntry.key),
|
||||
settingEntry.value,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toIList();
|
||||
|
||||
return matches.map(
|
||||
(match) => ListTile(
|
||||
onTap: () async {
|
||||
if (context.mounted) Navigator.of(suggestionsContext).pop();
|
||||
controller.text = "";
|
||||
|
||||
if (categoriesArePages) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => SettingsCategoryPage(
|
||||
match.$1.$1,
|
||||
initialHighlight: match.$1.$2,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
selected.value = match.$1.$1;
|
||||
listController.value.animateToItem(
|
||||
index: match.$1.$2,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => .new(milliseconds: 700),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
highlightedMatch.value = match.$1;
|
||||
await Future.delayed(.new(seconds: 1), () {
|
||||
if (highlightedMatch.value == match.$1) {
|
||||
highlightedMatch.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
leading: Icon(match.$2.icon),
|
||||
title: Text(match.$2.title),
|
||||
subtitle: Text(match.$2.description),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final settingsContent = Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Settings"),
|
||||
actionsPadding: .symmetric(horizontal: 12),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => context.showAboutDialog(ref),
|
||||
icon: Icon(Icons.info_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ref
|
||||
.watch(SettingsSectionsController.provider)
|
||||
.betterWhen(
|
||||
data: (sections) => categoriesArePages
|
||||
? CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12).copyWith(bottom: 8),
|
||||
child: searchBar,
|
||||
),
|
||||
),
|
||||
...sections
|
||||
.mapTo(
|
||||
(section, categories) => [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
).copyWith(bottom: 4),
|
||||
child: DividerText(section),
|
||||
),
|
||||
),
|
||||
SliverM3ECardList(
|
||||
padding: .symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 8,
|
||||
),
|
||||
margin: .symmetric(horizontal: 12),
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primaryContainer,
|
||||
itemCount: categories.length,
|
||||
onTap: (index) => Navigator.of(context)
|
||||
.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
SettingsCategoryPage(
|
||||
sections
|
||||
.values
|
||||
.flattenedToList
|
||||
.indexOf(
|
||||
categories[index],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, index) => ListTile(
|
||||
leading: Icon(categories[index].icon),
|
||||
title: Text(categories[index].title),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.flattened,
|
||||
],
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
MaterialUiCompatibilityBridge(
|
||||
child: NavigationRailM3E(
|
||||
type: .alwaysExpand,
|
||||
trailing: searchBar,
|
||||
scrollable: true,
|
||||
sections: sections
|
||||
.mapTo(
|
||||
(
|
||||
categoryGroup,
|
||||
categories,
|
||||
) => NavigationRailM3ESection(
|
||||
header: DividerText(categoryGroup),
|
||||
destinations: categories
|
||||
.map(
|
||||
(category) =>
|
||||
NavigationRailM3EDestination(
|
||||
icon: Icon(category.icon),
|
||||
label: category.title,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
selectedIndex: selected.value,
|
||||
onDestinationSelected: (value) =>
|
||||
selected.value = value,
|
||||
),
|
||||
),
|
||||
VerticalDivider(),
|
||||
Expanded(
|
||||
child: SuperListView(
|
||||
listController: listController.value,
|
||||
controller: scrollController,
|
||||
padding: .symmetric(vertical: 12),
|
||||
children: sections
|
||||
.values
|
||||
.flattenedToList[selected.value]
|
||||
.settings
|
||||
.mapIndexed(
|
||||
(index, setting) => Padding(
|
||||
padding: .only(bottom: 4),
|
||||
child: HighlightWrapper(
|
||||
setting.builder(
|
||||
setting.title,
|
||||
setting.description,
|
||||
setting.icon,
|
||||
),
|
||||
isHighlighted:
|
||||
highlightedMatch.value ==
|
||||
(selected.value, index),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return constraints.maxWidth < 650
|
||||
? settingsContent
|
||||
: Dialog(
|
||||
constraints: .loose(Size(900, 600)),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadiusGeometry.circular(12),
|
||||
child: settingsContent,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
79
lib/widgets/pages/settings_category.dart
Normal file
79
lib/widgets/pages/settings_category.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/settings_sections.dart";
|
||||
import "package:nexus/helpers/extensions/better_when.dart";
|
||||
import "package:nexus/widgets/highlight_wrapper.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
class const SettingsCategoryPage(
|
||||
final int index, {
|
||||
final int? initialHighlight,
|
||||
super.key,
|
||||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final highlight = useState<int?>(initialHighlight);
|
||||
final listController = useRef(ListController());
|
||||
final scrollController = useScrollController();
|
||||
|
||||
useEffect(() {
|
||||
if (initialHighlight == null) return null;
|
||||
Timer? timer;
|
||||
|
||||
void listener() => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!listController.value.isAttached) return;
|
||||
|
||||
listController.value.animateToItem(
|
||||
index: initialHighlight!,
|
||||
scrollController: scrollController,
|
||||
alignment: 0.5,
|
||||
duration: (_) => .new(milliseconds: 700),
|
||||
curve: (_) => Curves.easeInOut,
|
||||
);
|
||||
timer = Timer(.new(seconds: 1), () {
|
||||
highlight.value = null;
|
||||
});
|
||||
listController.value.removeListener(listener);
|
||||
});
|
||||
|
||||
listController.value.addListener(listener);
|
||||
return timer?.cancel;
|
||||
}, []);
|
||||
|
||||
return ref
|
||||
.watch(SettingsSectionsController.provider)
|
||||
.betterWhen(
|
||||
data: (sections) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(sections.values.flattenedToList[index].title),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: SuperListView(
|
||||
controller: scrollController,
|
||||
listController: listController.value,
|
||||
padding: .symmetric(vertical: 12, horizontal: 8),
|
||||
children: sections.values.flattenedToList[index].settings
|
||||
.mapIndexed(
|
||||
(index, setting) => Padding(
|
||||
padding: .only(bottom: 4),
|
||||
child: HighlightWrapper(
|
||||
setting.builder(
|
||||
setting.title,
|
||||
setting.description,
|
||||
setting.icon,
|
||||
),
|
||||
isHighlighted: highlight.value == index,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
82
lib/widgets/pages/verify.dart
Normal file
82
lib/widgets/pages/verify.dart
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/widgets/pages/settings.dart";
|
||||
import "package:nexus/widgets/appbar.dart";
|
||||
import "package:nexus/helpers/required_validator_helper.dart";
|
||||
|
||||
class const VerifyPage({super.key}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final passphraseController = useTextEditingController();
|
||||
final isLoading = useState(false);
|
||||
final inputError = useState<String?>(null);
|
||||
final formKey = useRef(GlobalKey<FormState>());
|
||||
|
||||
Future<void> verify() async {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
if (formKey.value.currentState?.validate() != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
inputError.value = await ref
|
||||
.watch(ClientController.provider.notifier)
|
||||
.verify(passphraseController.text);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
appBar: Appbar(
|
||||
actions: .new([
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
showDialog(context: context, builder: (_) => SettingsPage()),
|
||||
icon: Icon(Icons.settings),
|
||||
),
|
||||
]),
|
||||
),
|
||||
body: AlertDialog(
|
||||
title: Text("Verify"),
|
||||
content: Form(
|
||||
key: formKey.value,
|
||||
child: Column(
|
||||
mainAxisSize: .min,
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
Text(
|
||||
"Enter your recovery key or passphrase below to unlock encrypted events.\nYour passphrase is usually not the same as your password.",
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
TextFormField(
|
||||
autofocus: true,
|
||||
controller: passphraseController,
|
||||
textInputAction: .done,
|
||||
autovalidateMode: .onUserInteraction,
|
||||
validator: requiredValidator,
|
||||
obscureText: true,
|
||||
decoration: .new(
|
||||
label: Text("Recovery Key or Passphrase"),
|
||||
errorText: inputError.value,
|
||||
),
|
||||
onFieldSubmitted: (_) => verify(),
|
||||
// Don't defocus on submit
|
||||
onEditingComplete: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: isLoading.value ? null : verify,
|
||||
child: Text("Verify"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,8 @@ import "package:navigation_rail_m3e/navigation_rail_m3e.dart";
|
|||
import "package:nexus/controllers/key.dart";
|
||||
import "package:nexus/controllers/spaces.dart";
|
||||
import "package:nexus/models/room.dart";
|
||||
import "package:nexus/pages/notifications.dart";
|
||||
import "package:nexus/pages/settings.dart";
|
||||
import "package:nexus/widgets/pages/notifications.dart";
|
||||
import "package:nexus/widgets/pages/settings.dart";
|
||||
import "package:nexus/widgets/avatar_or_hash.dart";
|
||||
import "package:nexus/widgets/divider_widget.dart";
|
||||
import "package:nexus/widgets/join_dialog.dart";
|
||||
|
|
|
|||
Loading…
Reference in a new issue