Watch
1
0
Fork
You've already forked nexus
0
forked from Nexus/nexus
nexus/lib/controllers/settings_sections.dart

402 lines
17 KiB
Dart

import "dart:io";
import "package:fast_immutable_collections/fast_immutable_collections.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:material_ui/material_ui.dart";
import "package:intl/intl.dart";
import "package:m3e_buttons/m3e_buttons.dart";
import "package:nexus/controllers/account_data.dart";
import "package:nexus/controllers/client.dart";
import "package:nexus/controllers/client_state.dart";
import "package:nexus/controllers/init_complete.dart";
import "package:nexus/controllers/notification.dart";
import "package:nexus/controllers/settings.dart";
import "package:nexus/controllers/unified_push.dart";
import "package:nexus/controllers/spec_versions.dart";
import "package:nexus/controllers/unified_push_allowed.dart";
import "package:nexus/controllers/unified_push_distributors.dart";
import "package:nexus/helpers/extensions/distributor_name.dart";
import "package:nexus/models/account_data.dart";
import "package:nexus/models/room_order.dart";
import "package:nexus/models/settings_category.dart";
import "package:nexus/main.dart";
import "package:nexus/widgets/settings/dialog_list_tile.dart";
import "package:nexus/widgets/reset_encryption_sheet.dart";
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
class SettingsSectionsController
extends AsyncNotifier<IMap<String, IList<SettingsCategory>>> {
@override
Future<IMap<String, IList<SettingsCategory>>> build() async {
final settings = await ref.watch(SettingsController.provider.future);
return .new({
"General": .new([
.new(
title: "Appearance",
icon: Icons.brush,
settings: .new([
.new(
title: "Theme",
description:
"Toggle between Light Mode, Dark Mode, and System themes.",
icon: Icons.contrast,
builder: (title, description, icon) => DialogListTile<ThemeMode>(
icon: Icon(icon),
title: title,
subtitle: Text(description),
value: settings.theme,
options: ThemeMode.values,
getName: (option) => toBeginningOfSentenceCase(option.name),
onChanged: (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(theme: value))
.onError(showError),
),
),
.new(
title: "Use Dynamic Theme",
icon: Icons.palette,
description: "Toggle on or off Dynamic Theme. Only available on Android, Linux, Windows, or MacOS.",
builder: (title, description, icon) => SwitchListTile(
title: Text(title),
subtitle: Text(description),
secondary: Icon(icon),
value: settings.useDynamicTheming,
onChanged:
(Platform.isAndroid ||
Platform.isLinux ||
Platform.isMacOS ||
Platform.isWindows)
? (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(useDynamicTheming: value))
.onError(showError)
: null,
),
),
]),
),
.new(
title: "Behavior",
icon: Icons.psychology,
settings: .new([
.new(
title: "All Rooms In Home",
description: "Show all rooms in home, instead of just ones that aren't in spaces",
icon: Icons.home,
builder: (title, description, icon) => SwitchListTile(
title: Text(title),
subtitle: Text(description),
secondary: Icon(icon),
value: settings.showAllInHome,
onChanged: (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(showAllInHome: value))
.onError(showError),
),
),
.new(
title: "Room Ordering",
description: "Sort rooms by recent activity, optionally showing mentioned and unread rooms first.",
icon: Icons.sort,
builder: (title, description, icon) => DialogListTile<RoomOrder>(
icon: Icon(icon),
title: title,
subtitle: Text(description),
value: settings.roomOrder,
options: RoomOrder.values,
getName: (option) => switch (option) {
.unreadFirst => "Mentioned, then unread",
.recentActivity => "Recent activity only",
},
onChanged: (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(roomOrder: value))
.onError(showError),
),
),
.new(
title: "Swipe To Reply",
description: "Swipe a message to the right to reply to it.",
icon: Icons.swipe_right,
builder: (title, description, icon) => SwitchListTile(
title: Text(title),
subtitle: Text(description),
secondary: Icon(icon),
value: settings.swipeToReply,
onChanged: (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(swipeToReply: value))
.onError(showError),
),
),
if (!Platform.isAndroid && !Platform.isIOS)
.new(
title: "Show Minimize Button",
description:
"Enable this to add a minimize button to the appbar",
icon: Icons.minimize,
builder: (title, description, icon) => SwitchListTile(
title: Text(title),
subtitle: Text(description),
secondary: Icon(icon),
value: settings.showMinimizeButton,
onChanged: (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(showMinimizeButton: value))
.onError(showError),
),
),
.new(
title: "Linux Mobile Mode",
description: "Enables some fixes for Linux mobile, e.g. disabling dragging appbar for moving window.",
icon: Icons.construction,
builder: (title, description, icon) => SwitchListTile(
title: Text(title),
subtitle: Text(description),
secondary: Icon(icon),
value: settings.linuxMobileMode,
onChanged: Platform.isLinux
? (value) => ref
.read(SettingsController.provider.notifier)
.set(settings.copyWith(linuxMobileMode: value))
.onError(showError)
: null,
),
),
]),
),
]),
if (ref.watch(ClientStateController.provider)?.isLoggedIn == true)
"Account": .new([
.new(title: "Profile", icon: Icons.person, settings: .new([])),
.new(
title: "Notifications",
icon: Icons.notifications,
settings: .new([
.new(
title: "Push notifications",
description: "Enable push notifications using Web Push, and choose which UnifiedPush distributor to use.",
builder: (title, description, icon) => HookConsumer(
builder: (context, ref, _) {
final loading = useState(false);
final pusherRegistered = ref.watch(
UnifiedPushController.provider,
);
final unifiedPushAllowed = ref.watch(
UnifiedPushAllowedController.provider,
);
final distributors = ref.watch(
UnifiedPushDistributorsController.provider,
);
final current = pusherRegistered.value;
return DialogListTile<String?>(
title: title,
subtitle: Text(
unifiedPushAllowed.maybeWhen(
data: (data) => data,
orElse: () => null,
) ??
description,
),
icon: Icon(icon),
value: current,
options: [
null,
...?distributors.value,
if (current != null &&
distributors.value?.contains(current) != true)
current,
],
getName: (option) =>
option == null ? "Off" : option.distributorName,
onChanged: loading.value
? null
: unifiedPushAllowed.maybeWhen(
data: (data) => data == null,
orElse: () => false,
)
? pusherRegistered.maybeWhen(
data: (_) => (value) async {
if (value == current) return;
try {
loading.value = true;
if (value != null) {
final messenger = ScaffoldMessenger.of(
context,
);
if (!await ref
.read(
NotificationController
.provider
.notifier,
)
.requestPermissions()) {
messenger.showSnackBar(
.new(
content: Text(
"Notifications are disabled. Enable them for Nexus in your system settings.",
),
),
);
return;
}
await ref
.read(
UnifiedPushController
.provider
.notifier,
)
.register(value);
} else {
await ref
.read(
UnifiedPushController
.provider
.notifier,
)
.deregister();
}
} catch (error, stackTrace) {
showError(error, stackTrace);
} finally {
loading.value = false;
}
},
orElse: () => null,
)
: null,
);
},
),
icon: Icons.notification_add,
),
]),
),
.new(
title: "Safety",
icon: Icons.gpp_good,
settings: .new([
.new(
title: "Invite Blocking",
description: "Block invites, either completely, or block only invites from users without shared private rooms (depends on server support).",
builder: (title, description, icon) => Consumer(
builder: (context, ref, _) {
final specVersionsResponse = ref.watch(
SpecVersionsController.provider,
);
return DialogListTile<DefaultInviteAction>(
icon: Icon(icon),
title: title,
subtitle: Text(description),
value: ref
.watch(AccountDataController.provider)
.invitePermissionConfig
.defaultAction,
options:
specVersionsResponse.maybeWhen(
data: (data) => data.unstableFeatures.msc4494,
orElse: () => true,
)
? DefaultInviteAction.values
: IList(DefaultInviteAction.values)
.remove(.denyPublic)
.toList(),
getName: (option) => switch (option) {
.allow => "Allow",
.deny => "Deny",
.denyPublic => "Deny public",
},
onChanged: specVersionsResponse.maybeWhen(
data: (_) =>
(value) => ref
.read(ClientController.provider.notifier)
.setAccountData(
.new(
type: AccountData.invitePermissionConfigKey,
content: InvitePermissionConfig(
defaultAction: value,
),
),
)
.onError(showError),
orElse: () => null,
),
);
},
),
icon: Icons.person_off,
),
]),
),
.new(
title: "Other",
icon: Icons.key,
settings: .new([
.new(
title: "Reset Encryption Identity",
description: "Replace your encryption identity and recovery key with new ones, if you've lost your recovery key or your encryption is broken.",
builder: (title, description, icon) => Builder(
builder: (context) => M3EButton.icon(
onPressed: () => context.showAdaptiveSheet(
(_) => const ResetEncryptionSheet(),
),
label: Text(title),
icon: Icon(icon),
tooltip: description,
),
),
icon: Icons.key_off,
),
.new(
title: "Log Out",
description:
"Log out of your account, returning you to the login page.",
builder: (title, description, icon) => Builder(
builder: (context) {
final colorScheme = Theme.of(context).colorScheme;
return M3EButton.icon(
onPressed: () async {
final container = ProviderScope.containerOf(context);
final route = ModalRoute.of(context)!;
Navigator.of(context).popUntil((r) => r.isFirst);
await route.completed;
await container
.read(UnifiedPushController.provider.notifier)
.deregister()
.onError(showError);
container.invalidate(InitCompleteController.provider);
await container
.read(ClientController.provider.notifier)
.logout()
.onError(showError);
},
label: Text(title),
icon: Icon(icon),
tooltip: description,
decoration: .styleFrom(
backgroundColor: colorScheme.errorContainer,
foregroundColor: colorScheme.onErrorContainer,
),
);
},
),
icon: Icons.logout,
),
]),
),
]),
});
}
static final provider =
AsyncNotifierProvider.autoDispose<
SettingsSectionsController,
IMap<String, IList<SettingsCategory>>
>(SettingsSectionsController.new);
}