Accessibility update

This commit is contained in:
Christien Rioux 2025-05-25 23:40:52 -04:00
parent be8014c97a
commit 3b1cb53b8a
55 changed files with 1089 additions and 807 deletions

View file

@ -6,6 +6,9 @@
- Deprecated accounts no longer crash application at startup - Deprecated accounts no longer crash application at startup
- Simplify SingleContactMessagesCubit and MessageReconciliation - Simplify SingleContactMessagesCubit and MessageReconciliation
- Update flutter_chat_ui to 2.0.0 - Update flutter_chat_ui to 2.0.0
- Accessibility improvements
- Text scaling
- Keyboard shortcuts Ctrl + / Ctrl - to change font size
## v0.4.7 ## ## v0.4.7 ##
- *Community Contributions* - *Community Contributions*

View file

@ -276,6 +276,7 @@
"titlebar": "Settings", "titlebar": "Settings",
"color_theme": "Color Theme", "color_theme": "Color Theme",
"brightness_mode": "Brightness Mode", "brightness_mode": "Brightness Mode",
"display_scale": "Display Scale",
"display_beta_warning": "Display beta warning on startup", "display_beta_warning": "Display beta warning on startup",
"none": "None", "none": "None",
"in_app": "In-app", "in_app": "In-app",

View file

@ -6,54 +6,9 @@ PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_native_splash (2.4.3): - flutter_native_splash (2.4.3):
- Flutter - Flutter
- GoogleDataTransport (10.1.0): - mobile_scanner (7.0.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleMLKit/BarcodeScanning (7.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 6.0.0)
- GoogleMLKit/MLKitCore (7.0.0):
- MLKitCommon (~> 12.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (8.0.2):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.0.2):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.0.2)
- GoogleUtilities/UserDefaults (8.0.2):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMSessionFetcher/Core (3.5.0)
- MLImage (1.0.0-beta6)
- MLKitBarcodeScanning (6.0.0):
- MLKitCommon (~> 12.0)
- MLKitVision (~> 8.0)
- MLKitCommon (12.0.0):
- GoogleDataTransport (~> 10.0)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/Logger (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (8.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta6)
- MLKitCommon (~> 12.0)
- mobile_scanner (6.0.2):
- Flutter - Flutter
- GoogleMLKit/BarcodeScanning (~> 7.0.0) - FlutterMacOS
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- pasteboard (0.0.1): - pasteboard (0.0.1):
@ -63,7 +18,6 @@ PODS:
- FlutterMacOS - FlutterMacOS
- printing (1.0.0): - printing (1.0.0):
- Flutter - Flutter
- PromisesObjC (2.4.0)
- share_plus (0.0.1): - share_plus (0.0.1):
- Flutter - Flutter
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
@ -84,7 +38,7 @@ DEPENDENCIES:
- file_saver (from `.symlinks/plugins/file_saver/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
@ -96,20 +50,6 @@ DEPENDENCIES:
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- veilid (from `.symlinks/plugins/veilid/ios`) - veilid (from `.symlinks/plugins/veilid/ios`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES: EXTERNAL SOURCES:
camera_avfoundation: camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios" :path: ".symlinks/plugins/camera_avfoundation/ios"
@ -120,7 +60,7 @@ EXTERNAL SOURCES:
flutter_native_splash: flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios" :path: ".symlinks/plugins/flutter_native_splash/ios"
mobile_scanner: mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios" :path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
pasteboard: pasteboard:
@ -143,26 +83,15 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/veilid/ios" :path: ".symlinks/plugins/veilid/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07 printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0

View file

@ -139,7 +139,6 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
02C44F9283ADDE9FAAA73512 /* [CP] Embed Pods Frameworks */, 02C44F9283ADDE9FAAA73512 /* [CP] Embed Pods Frameworks */,
61BE8A90522682C17620991D /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@ -232,23 +231,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
61BE8A90522682C17620991D /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;

View file

@ -26,6 +26,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
@ -43,6 +44,7 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0" launchStyle = "0"
useCustomWorkingDirectory = "NO" useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO" ignoresPersistentStateOnLaunch = "NO"

View file

@ -73,15 +73,18 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
title: translate('edit_account_page.remove_account_confirm'), title: translate('edit_account_page.remove_account_confirm'),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(translate('edit_account_page.remove_account_confirm_message')) Text(translate('edit_account_page.remove_account_confirm_message'))
.paddingLTRB(24, 24, 24, 0), .paddingLTRB(24.scaled(context), 24.scaled(context),
Text(translate('confirmation.are_you_sure')).paddingAll(8), 24.scaled(context), 0),
Text(translate('confirmation.are_you_sure'))
.paddingAll(8.scaled(context)),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
}, },
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), Icon(Icons.cancel, size: 16.scaled(context))
.paddingLTRB(0, 0, 4.scaled(context), 0),
Text(translate('button.no')).paddingLTRB(0, 0, 4, 0) Text(translate('button.no')).paddingLTRB(0, 0, 4, 0)
])), ])),
ElevatedButton( ElevatedButton(
@ -89,10 +92,12 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), Icon(Icons.check, size: 16.scaled(context))
Text(translate('button.yes')).paddingLTRB(0, 0, 4, 0) .paddingLTRB(0, 0, 4.scaled(context), 0),
Text(translate('button.yes'))
.paddingLTRB(0, 0, 4.scaled(context), 0)
])) ]))
]).paddingAll(24) ]).paddingAll(24.scaled(context))
])); ]));
if (confirmed != null && confirmed) { if (confirmed != null && confirmed) {
try { try {
@ -141,29 +146,36 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
title: translate('edit_account_page.destroy_account_confirm'), title: translate('edit_account_page.destroy_account_confirm'),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(translate('edit_account_page.destroy_account_confirm_message')) Text(translate('edit_account_page.destroy_account_confirm_message'))
.paddingLTRB(24, 24, 24, 0), .paddingLTRB(24.scaled(context), 24.scaled(context),
24.scaled(context), 0),
Text(translate( Text(translate(
'edit_account_page.destroy_account_confirm_message_details')) 'edit_account_page.destroy_account_confirm_message_details'))
.paddingLTRB(24, 24, 24, 0), .paddingLTRB(24.scaled(context), 24.scaled(context),
Text(translate('confirmation.are_you_sure')).paddingAll(8), 24.scaled(context), 0),
Text(translate('confirmation.are_you_sure'))
.paddingAll(24.scaled(context)),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(false); Navigator.of(context).pop(false);
}, },
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0), Icon(Icons.cancel, size: 16.scaled(context))
Text(translate('button.no')).paddingLTRB(0, 0, 4, 0) .paddingLTRB(0, 0, 4.scaled(context), 0),
Text(translate('button.no'))
.paddingLTRB(0, 0, 4.scaled(context), 0)
])), ])),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(true); Navigator.of(context).pop(true);
}, },
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), Icon(Icons.check, size: 16.scaled(context))
Text(translate('button.yes')).paddingLTRB(0, 0, 4, 0) .paddingLTRB(0, 0, 4.scaled(context), 0),
Text(translate('button.yes'))
.paddingLTRB(0, 0, 4.scaled(context), 0)
])) ]))
]).paddingAll(24) ]).paddingAll(24.scaled(context))
])); ]));
if (confirmed != null && confirmed) { if (confirmed != null && confirmed) {
try { try {
@ -250,10 +262,12 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
return StyledScaffold( return StyledScaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
context: context,
title: Text(translate('edit_account_page.titlebar')), title: Text(translate('edit_account_page.titlebar')),
leading: Navigator.canPop(context) leading: Navigator.canPop(context)
? IconButton( ? IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
iconSize: 24.scaled(context),
onPressed: () { onPressed: () {
singleFuture((this, _kDoBackArrow), () async { singleFuture((this, _kDoBackArrow), () async {
if (_isModified) { if (_isModified) {
@ -277,6 +291,7 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
iconSize: 24.scaled(context),
tooltip: translate('menu.settings_tooltip'), tooltip: translate('menu.settings_tooltip'),
onPressed: () async { onPressed: () async {
await GoRouterHelper(context).push('/settings'); await GoRouterHelper(context).push('/settings');
@ -285,14 +300,14 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
body: SingleChildScrollView( body: SingleChildScrollView(
child: Column(children: [ child: Column(children: [
_editAccountForm(context).paddingLTRB(0, 0, 0, 32), _editAccountForm(context).paddingLTRB(0, 0, 0, 32),
OptionBox( StyledButtonBox(
instructions: instructions:
translate('edit_account_page.remove_account_description'), translate('edit_account_page.remove_account_description'),
buttonIcon: Icons.person_remove_alt_1, buttonIcon: Icons.person_remove_alt_1,
buttonText: translate('edit_account_page.remove_account'), buttonText: translate('edit_account_page.remove_account'),
onClick: _onRemoveAccount, onClick: _onRemoveAccount,
), ),
OptionBox( StyledButtonBox(
instructions: instructions:
translate('edit_account_page.destroy_account_description'), translate('edit_account_page.destroy_account_description'),
buttonIcon: Icons.person_off, buttonIcon: Icons.person_off,

View file

@ -53,16 +53,16 @@ class EditProfileForm extends StatefulWidget {
..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue)); ..add(DiagnosticsProperty<AccountSpec>('initialValue', initialValue));
} }
static const String formFieldName = 'name'; static const formFieldName = 'name';
static const String formFieldPronouns = 'pronouns'; static const formFieldPronouns = 'pronouns';
static const String formFieldAbout = 'about'; static const formFieldAbout = 'about';
static const String formFieldAvailability = 'availability'; static const formFieldAvailability = 'availability';
static const String formFieldFreeMessage = 'free_message'; static const formFieldFreeMessage = 'free_message';
static const String formFieldAwayMessage = 'away_message'; static const formFieldAwayMessage = 'away_message';
static const String formFieldBusyMessage = 'busy_message'; static const formFieldBusyMessage = 'busy_message';
static const String formFieldAvatar = 'avatar'; static const formFieldAvatar = 'avatar';
static const String formFieldAutoAway = 'auto_away'; static const formFieldAutoAway = 'auto_away';
static const String formFieldAutoAwayTimeout = 'auto_away_timeout'; static const formFieldAutoAwayTimeout = 'auto_away_timeout';
} }
class _EditProfileFormState extends State<EditProfileForm> { class _EditProfileFormState extends State<EditProfileForm> {
@ -98,6 +98,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
name: EditProfileForm.formFieldAvailability, name: EditProfileForm.formFieldAvailability,
initialValue: initialValue, initialValue: initialValue,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_availability'), labelText: translate('account.form_availability'),
hintText: translate('account.empty_busy_message')), hintText: translate('account.empty_busy_message')),
@ -110,7 +111,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
Text(x == proto.Availability.AVAILABILITY_OFFLINE Text(x == proto.Availability.AVAILABILITY_OFFLINE
? translate('availability.always_show_offline') ? translate('availability.always_show_offline')
: AvailabilityWidget.availabilityName(x)) : AvailabilityWidget.availabilityName(x))
.paddingLTRB(8, 0, 0, 0), .paddingLTRB(8.scaled(context), 0, 0, 0),
]))) ])))
.toList(), .toList(),
); );
@ -175,17 +176,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
BuildContext context, BuildContext context,
) { ) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
late final Color border;
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) {
border = scale.primaryScale.elementBackground;
} else {
border = scale.primaryScale.border;
}
return FormBuilder( return FormBuilder(
key: _formKey, key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
@ -197,14 +189,9 @@ class _EditProfileFormState extends State<EditProfileForm> {
children: [ children: [
Row(children: [ Row(children: [
const Spacer(), const Spacer(),
AvatarWidget( StyledAvatar(
name: _currentValueName, name: _currentValueName,
size: 128, size: 128.scaled(context),
borderColor: border,
foregroundColor: scale.primaryScale.primaryText,
backgroundColor: scale.primaryScale.primary,
scaleConfig: scaleConfig,
textStyle: theme.textTheme.titleLarge!.copyWith(fontSize: 64),
).paddingLTRB(0, 0, 0, 16), ).paddingLTRB(0, 0, 0, 16),
const Spacer() const Spacer()
]), ]),
@ -218,6 +205,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
}); });
}, },
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_name'), labelText: translate('account.form_name'),
hintText: translate('account.empty_name')), hintText: translate('account.empty_name')),
@ -233,6 +221,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
initialValue: _savedValue.pronouns, initialValue: _savedValue.pronouns,
maxLength: 64, maxLength: 64,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_pronouns'), labelText: translate('account.form_pronouns'),
hintText: translate('account.empty_pronouns')), hintText: translate('account.empty_pronouns')),
@ -245,6 +234,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
maxLines: 8, maxLines: 8,
minLines: 1, minLines: 1,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_about'), labelText: translate('account.form_about'),
hintText: translate('account.empty_about')), hintText: translate('account.empty_about')),
@ -256,6 +246,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
initialValue: _savedValue.freeMessage, initialValue: _savedValue.freeMessage,
maxLength: 128, maxLength: 128,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_free_message'), labelText: translate('account.form_free_message'),
hintText: translate('account.empty_free_message')), hintText: translate('account.empty_free_message')),
@ -266,6 +257,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
initialValue: _savedValue.awayMessage, initialValue: _savedValue.awayMessage,
maxLength: 128, maxLength: 128,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_away_message'), labelText: translate('account.form_away_message'),
hintText: translate('account.empty_away_message')), hintText: translate('account.empty_away_message')),
@ -276,6 +268,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
initialValue: _savedValue.busyMessage, initialValue: _savedValue.busyMessage,
maxLength: 128, maxLength: 128,
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
floatingLabelBehavior: FloatingLabelBehavior.always, floatingLabelBehavior: FloatingLabelBehavior.always,
labelText: translate('account.form_busy_message'), labelText: translate('account.form_busy_message'),
hintText: translate('account.empty_busy_message')), hintText: translate('account.empty_busy_message')),
@ -291,12 +284,13 @@ class _EditProfileFormState extends State<EditProfileForm> {
_currentValueAutoAway = v ?? false; _currentValueAutoAway = v ?? false;
}); });
}, },
).paddingLTRB(0, 0, 0, 16), ).paddingLTRB(0, 0, 0, 16.scaled(context)),
FormBuilderTextField( FormBuilderTextField(
name: EditProfileForm.formFieldAutoAwayTimeout, name: EditProfileForm.formFieldAutoAwayTimeout,
enabled: _currentValueAutoAway, enabled: _currentValueAutoAway,
initialValue: _savedValue.autoAwayTimeout.toString(), initialValue: _savedValue.autoAwayTimeout.toString(),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8).scaled(context),
labelText: translate('account.form_auto_away_timeout'), labelText: translate('account.form_auto_away_timeout'),
), ),
validator: FormBuilderValidators.positiveNumber(), validator: FormBuilderValidators.positiveNumber(),
@ -306,7 +300,7 @@ class _EditProfileFormState extends State<EditProfileForm> {
const Spacer(), const Spacer(),
Text(widget.instructions).toCenter().flexible(flex: 6), Text(widget.instructions).toCenter().flexible(flex: 6),
const Spacer(), const Spacer(),
]).paddingSymmetric(vertical: 16), ]).paddingSymmetric(vertical: 16.scaled(context)),
Row(children: [ Row(children: [
const Spacer(), const Spacer(),
Builder(builder: (context) { Builder(builder: (context) {
@ -320,16 +314,18 @@ class _EditProfileFormState extends State<EditProfileForm> {
return ElevatedButton( return ElevatedButton(
onPressed: (networkReady && _isModified) ? _doSubmit : null, onPressed: (networkReady && _isModified) ? _doSubmit : null,
child: Padding(
padding: EdgeInsetsGeometry.all(4.scaled(context)),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(networkReady ? Icons.check : Icons.hourglass_empty, Icon(networkReady ? Icons.check : Icons.hourglass_empty,
size: 16) size: 16.scaled(context))
.paddingLTRB(0, 0, 4, 0), .paddingLTRB(0, 0, 4.scaled(context), 0),
Text(networkReady Text(networkReady
? widget.submitText ? widget.submitText
: widget.submitDisabledText) : widget.submitDisabledText)
.paddingLTRB(0, 0, 4, 0) .paddingLTRB(0, 0, 4.scaled(context), 0)
]), ]),
); ));
}), }),
const Spacer() const Spacer()
]) ])
@ -363,5 +359,5 @@ class _EditProfileFormState extends State<EditProfileForm> {
late AccountSpec _savedValue; late AccountSpec _savedValue;
late bool _currentValueAutoAway; late bool _currentValueAutoAway;
late String _currentValueName; late String _currentValueName;
bool _isModified = false; var _isModified = false;
} }

View file

@ -94,6 +94,7 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
return StyledScaffold( return StyledScaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
context: context,
title: Text(translate('new_account_page.titlebar')), title: Text(translate('new_account_page.titlebar')),
leading: GoRouterHelper(context).canPop() leading: GoRouterHelper(context).canPop()
? IconButton( ? IconButton(
@ -111,6 +112,7 @@ class _NewAccountPageState extends WindowSetupState<NewAccountPage> {
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
IconButton( IconButton(
icon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
iconSize: 24.scaled(context),
tooltip: translate('menu.settings_tooltip'), tooltip: translate('menu.settings_tooltip'),
onPressed: () async { onPressed: () async {
await GoRouterHelper(context).push('/settings'); await GoRouterHelper(context).push('/settings');

View file

@ -164,6 +164,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState<ShowRecoveryKeyPage> {
return StyledScaffold( return StyledScaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
context: context,
title: Text(translate('show_recovery_key_page.titlebar')), title: Text(translate('show_recovery_key_page.titlebar')),
actions: [ actions: [
const SignalStrengthMeterWidget(), const SignalStrengthMeterWidget(),
@ -193,7 +194,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState<ShowRecoveryKeyPage> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
translate('show_recovery_key_page.instructions_options')) translate('show_recovery_key_page.instructions_options'))
.paddingLTRB(12, 0, 12, 24), .paddingLTRB(12, 0, 12, 24),
OptionBox( StyledButtonBox(
instructions: instructions:
translate('show_recovery_key_page.instructions_print'), translate('show_recovery_key_page.instructions_print'),
buttonIcon: Icons.print, buttonIcon: Icons.print,
@ -209,7 +210,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState<ShowRecoveryKeyPage> {
_codeHandled = true; _codeHandled = true;
}); });
}), }),
OptionBox( StyledButtonBox(
instructions: instructions:
translate('show_recovery_key_page.instructions_view'), translate('show_recovery_key_page.instructions_view'),
buttonIcon: Icons.edit_document, buttonIcon: Icons.edit_document,
@ -229,7 +230,7 @@ class _ShowRecoveryKeyPageState extends WindowSetupState<ShowRecoveryKeyPage> {
_codeHandled = true; _codeHandled = true;
}); });
}), }),
OptionBox( StyledButtonBox(
instructions: instructions:
translate('show_recovery_key_page.instructions_share'), translate('show_recovery_key_page.instructions_share'),
buttonIcon: Icons.ios_share, buttonIcon: Icons.ios_share,

View file

@ -31,7 +31,7 @@ class VeilidChatApp extends StatelessWidget {
super.key, super.key,
}); });
static const String name = 'VeilidChat'; static const name = 'VeilidChat';
final ThemeData initialThemeData; final ThemeData initialThemeData;
@ -125,7 +125,7 @@ class VeilidChatApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>( Widget build(BuildContext context) => FutureProvider<VeilidChatGlobalInit?>(
initialData: null, initialData: null,
create: (context) async => VeilidChatGlobalInit.initialize(), create: (context) => VeilidChatGlobalInit.initialize(),
builder: (context, __) { builder: (context, __) {
final globalInit = context.watch<VeilidChatGlobalInit?>(); final globalInit = context.watch<VeilidChatGlobalInit?>();
if (globalInit == null) { if (globalInit == null) {

View file

@ -355,6 +355,7 @@ class _VcComposerState extends State<VcComposerWidget> {
borderRadius: BorderRadius.all(Radius.circular( borderRadius: BorderRadius.all(Radius.circular(
8 * config.borderRadiusScale))), 8 * config.borderRadiusScale))),
hintText: widget.hintText, hintText: widget.hintText,
hintMaxLines: 1,
hintStyle: chatTheme.typography.bodyMedium.copyWith( hintStyle: chatTheme.typography.bodyMedium.copyWith(
color: widget.hintColor ?? color: widget.hintColor ??
chatTheme.colors.onSurface chatTheme.colors.onSurface

View file

@ -12,7 +12,7 @@ class VcTextMessageWidget extends StatelessWidget {
const VcTextMessageWidget({ const VcTextMessageWidget({
required this.message, required this.message,
required this.index, required this.index,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10), this.padding,
this.borderRadius, this.borderRadius,
this.onlyEmojiFontSize, this.onlyEmojiFontSize,
this.sentBackgroundColor, this.sentBackgroundColor,
@ -72,10 +72,6 @@ class VcTextMessageWidget extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaleTheme = theme.extension<ScaleTheme>()!; final scaleTheme = theme.extension<ScaleTheme>()!;
final config = scaleTheme.config;
final scheme = scaleTheme.scheme;
final scale = scaleTheme.scheme.scale(ScaleKind.primary);
final textTheme = theme.textTheme;
final scaleChatTheme = scaleTheme.chatTheme(); final scaleChatTheme = scaleTheme.chatTheme();
final chatTheme = scaleChatTheme.chatTheme; final chatTheme = scaleChatTheme.chatTheme;
@ -243,15 +239,16 @@ class TimeAndStatus extends StatelessWidget {
if (showStatus && status != null) if (showStatus && status != null)
if (status == MessageStatus.sending) if (status == MessageStatus.sending)
SizedBox( SizedBox(
width: 6, width: 6.scaled(context),
height: 6, height: 6.scaled(context),
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: textStyle?.color, color: textStyle?.color,
strokeWidth: 2, strokeWidth: 2,
), ),
) )
else else
Icon(getIconForStatus(status!), color: textStyle?.color, size: 12), Icon(getIconForStatus(status!),
color: textStyle?.color, size: 12.scaled(context)),
], ],
); );
} }

View file

@ -132,17 +132,9 @@ class _ChatComponentWidgetState extends State<ChatComponentWidget> {
final scale = scaleTheme.scheme.scale(ScaleKind.primary); final scale = scaleTheme.scheme.scale(ScaleKind.primary);
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final scaleChatTheme = scaleTheme.chatTheme(); final scaleChatTheme = scaleTheme.chatTheme();
// final errorChatTheme = chatTheme.copyWith(color:)
// ..inputTextColor = scaleScheme.errorScale.primary
// ..sendButtonIcon = Image.asset(
// 'assets/icon-send.png',
// color: scaleScheme.errorScale.primary,
// package: 'flutter_chat_ui',
// ))
// .commit();
// Get the enclosing chat component cubit that contains our state // Get the enclosing chat component cubit that contains our state
// (created by ChatComponentWidget.builder()) // (created by ChatComponentWidget.singleContact())
final chatComponentCubit = context.watch<ChatComponentCubit>(); final chatComponentCubit = context.watch<ChatComponentCubit>();
final chatComponentState = chatComponentCubit.state; final chatComponentState = chatComponentCubit.state;
@ -275,12 +267,17 @@ class _ChatComponentWidgetState extends State<ChatComponentWidget> {
VcTextMessageWidget( VcTextMessageWidget(
message: message, message: message,
index: index, index: index,
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 16)
.scaled(context)
// showTime: true, // showTime: true,
// showStatus: true, // showStatus: true,
), ),
// Composer builder // Composer builder
composerBuilder: (ctx) => VcComposerWidget( composerBuilder: (ctx) => VcComposerWidget(
autofocus: true, autofocus: true,
padding: const EdgeInsets.all(4).scaled(context),
gap: 8.scaled(context),
focusNode: _focusNode, focusNode: _focusNode,
textInputAction: isAnyMobile textInputAction: isAnyMobile
? TextInputAction.newline ? TextInputAction.newline

View file

@ -24,11 +24,9 @@ class ChatSingleContactItemWidget extends StatelessWidget {
final bool _disabled; final bool _disabled;
@override @override
// ignore: prefer_expression_function_bodies
Widget build( Widget build(
BuildContext context, BuildContext context,
) { ) {
final theme = Theme.of(context);
final scaleTheme = Theme.of(context).extension<ScaleTheme>()!; final scaleTheme = Theme.of(context).extension<ScaleTheme>()!;
final activeChatCubit = context.watch<ActiveChatCubit>(); final activeChatCubit = context.watch<ActiveChatCubit>();
@ -48,23 +46,12 @@ class ChatSingleContactItemWidget extends StatelessWidget {
selected: selected, selected: selected,
); );
final avatar = AvatarWidget( final avatar = StyledAvatar(
name: name, name: name,
size: 32, size: 32.scaled(context),
borderColor: scaleTheme.config.useVisualIndicators
? scaleTheme.scheme.primaryScale.primaryText
: scaleTheme.scheme.primaryScale.subtleBorder,
foregroundColor: _disabled
? scaleTheme.scheme.grayScale.primaryText
: scaleTheme.scheme.primaryScale.primaryText,
backgroundColor: _disabled
? scaleTheme.scheme.grayScale.primary
: scaleTheme.scheme.primaryScale.primary,
scaleConfig: scaleTheme.config,
textStyle: theme.textTheme.titleLarge!,
); );
return SliderTile( return StyledSlideTile(
key: ValueKey(_localConversationRecordKey), key: ValueKey(_localConversationRecordKey),
disabled: _disabled, disabled: _disabled,
selected: selected, selected: selected,
@ -75,14 +62,14 @@ class ChatSingleContactItemWidget extends StatelessWidget {
trailing: AvailabilityWidget( trailing: AvailabilityWidget(
availability: availability, availability: availability,
color: scaleTileTheme.textColor, color: scaleTileTheme.textColor,
).fit(fit: BoxFit.scaleDown), ).fit(fit: BoxFit.fill),
onTap: () { onTap: () {
singleFuture(activeChatCubit, () async { singleFuture(activeChatCubit, () async {
activeChatCubit.setActiveChat(_localConversationRecordKey); activeChatCubit.setActiveChat(_localConversationRecordKey);
}); });
}, },
endActions: [ endActions: [
SliderTileAction( SlideTileAction(
//icon: Icons.delete, //icon: Icons.delete,
label: translate('button.delete'), label: translate('button.delete'),
actionScale: ScaleKind.tertiary, actionScale: ScaleKind.tertiary,

View file

@ -44,7 +44,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
title = contactInvitationRecord.message; title = contactInvitationRecord.message;
} }
return SliderTile( return StyledSlideTile(
key: ObjectKey(contactInvitationRecord), key: ObjectKey(contactInvitationRecord),
disabled: tileDisabled, disabled: tileDisabled,
selected: selected, selected: selected,
@ -67,7 +67,7 @@ class ContactInvitationItemWidget extends StatelessWidget {
))); )));
}, },
endActions: [ endActions: [
SliderTileAction( SlideTileAction(
// icon: Icons.delete, // icon: Icons.delete,
label: translate('button.delete'), label: translate('button.delete'),
actionScale: ScaleKind.tertiary, actionScale: ScaleKind.tertiary,

View file

@ -1,37 +1,34 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../proto/proto.dart' as proto; import '../../proto/proto.dart' as proto;
import '../../theme/theme.dart';
class AvailabilityWidget extends StatelessWidget { class AvailabilityWidget extends StatelessWidget {
const AvailabilityWidget( const AvailabilityWidget(
{required this.availability, {required this.availability,
required this.color, required this.color,
this.vertical = true, this.vertical = true,
this.size = 32,
super.key}); super.key});
static Widget availabilityIcon(proto.Availability availability, Color color, static Widget availabilityIcon(
{double size = 24}) { proto.Availability availability,
Color color,
) {
late final Widget icon; late final Widget icon;
switch (availability) { switch (availability) {
case proto.Availability.AVAILABILITY_AWAY: case proto.Availability.AVAILABILITY_AWAY:
icon = SvgPicture.asset('assets/images/toilet.svg', icon = SvgPicture.asset('assets/images/toilet.svg',
width: size,
height: size,
colorFilter: ColorFilter.mode(color, BlendMode.srcATop)); colorFilter: ColorFilter.mode(color, BlendMode.srcATop));
case proto.Availability.AVAILABILITY_BUSY: case proto.Availability.AVAILABILITY_BUSY:
icon = Icon(Icons.event_busy, size: size); icon = const Icon(Icons.event_busy, applyTextScaling: true);
case proto.Availability.AVAILABILITY_FREE: case proto.Availability.AVAILABILITY_FREE:
icon = Icon(Icons.event_available, size: size); icon = const Icon(Icons.event_available, applyTextScaling: true);
case proto.Availability.AVAILABILITY_OFFLINE: case proto.Availability.AVAILABILITY_OFFLINE:
icon = Icon(Icons.cloud_off, size: size); icon = const Icon(Icons.cloud_off, applyTextScaling: true);
case proto.Availability.AVAILABILITY_UNSPECIFIED: case proto.Availability.AVAILABILITY_UNSPECIFIED:
icon = Icon(Icons.question_mark, size: size); icon = const Icon(Icons.question_mark, applyTextScaling: true);
} }
return icon; return icon;
} }
@ -59,23 +56,17 @@ class AvailabilityWidget extends StatelessWidget {
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final name = availabilityName(availability); final name = availabilityName(availability);
final icon = availabilityIcon(availability, color, size: size * 2 / 3); final icon = availabilityIcon(availability, color);
return vertical return vertical
? ConstrainedBox( ? Column(mainAxisSize: MainAxisSize.min, children: [
constraints: BoxConstraints.tightFor(width: size),
child: Column(mainAxisSize: MainAxisSize.min, children: [
icon, icon,
Text(name, style: textTheme.labelSmall!.copyWith(color: color)) Text(name, style: textTheme.labelSmall!.copyWith(color: color))
.fit(fit: BoxFit.scaleDown) ])
])) : Row(mainAxisSize: MainAxisSize.min, children: [
: ConstrainedBox(
constraints: BoxConstraints.tightFor(height: size),
child: Row(mainAxisSize: MainAxisSize.min, children: [
icon, icon,
Text(name, style: textTheme.labelLarge!.copyWith(color: color)) Text(' $name', style: textTheme.labelLarge!.copyWith(color: color))
.paddingLTRB(size / 4, 0, 0, 0) ]);
]));
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -83,7 +74,6 @@ class AvailabilityWidget extends StatelessWidget {
final proto.Availability availability; final proto.Availability availability;
final Color color; final Color color;
final bool vertical; final bool vertical;
final double size;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -92,7 +82,6 @@ class AvailabilityWidget extends StatelessWidget {
..add( ..add(
DiagnosticsProperty<proto.Availability>('availability', availability)) DiagnosticsProperty<proto.Availability>('availability', availability))
..add(DiagnosticsProperty<bool>('vertical', vertical)) ..add(DiagnosticsProperty<bool>('vertical', vertical))
..add(DoubleProperty('size', size))
..add(ColorProperty('color', color)); ..add(ColorProperty('color', color));
} }
} }

View file

@ -26,30 +26,16 @@ class ContactItemWidget extends StatelessWidget {
Widget build( Widget build(
BuildContext context, BuildContext context,
) { ) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
final name = _contact.nameOrNickname; final name = _contact.nameOrNickname;
final title = _contact.displayName; final title = _contact.displayName;
final subtitle = _contact.profile.status; final subtitle = _contact.profile.status;
final avatar = AvatarWidget( final avatar = StyledAvatar(
name: name, name: name,
size: 34, size: 34.scaled(context),
borderColor: _disabled
? scale.grayScale.primaryText
: scale.primaryScale.subtleBorder,
foregroundColor: _disabled
? scale.grayScale.primaryText
: scale.primaryScale.primaryText,
backgroundColor:
_disabled ? scale.grayScale.primary : scale.primaryScale.primary,
scaleConfig: scaleConfig,
textStyle: theme.textTheme.titleLarge!,
); );
return SliderTile( return StyledSlideTile(
key: ObjectKey(_contact), key: ObjectKey(_contact),
disabled: _disabled, disabled: _disabled,
selected: _selected, selected: _selected,
@ -69,7 +55,7 @@ class ContactItemWidget extends StatelessWidget {
}), }),
startActions: [ startActions: [
if (_onDoubleTap != null) if (_onDoubleTap != null)
SliderTileAction( SlideTileAction(
//icon: Icons.edit, //icon: Icons.edit,
label: translate('button.chat'), label: translate('button.chat'),
actionScale: ScaleKind.secondary, actionScale: ScaleKind.secondary,
@ -81,7 +67,7 @@ class ContactItemWidget extends StatelessWidget {
], ],
endActions: [ endActions: [
if (_onTap != null) if (_onTap != null)
SliderTileAction( SlideTileAction(
//icon: Icons.edit, //icon: Icons.edit,
label: translate('button.edit'), label: translate('button.edit'),
actionScale: ScaleKind.secondary, actionScale: ScaleKind.secondary,
@ -91,7 +77,7 @@ class ContactItemWidget extends StatelessWidget {
}), }),
), ),
if (_onDelete != null) if (_onDelete != null)
SliderTileAction( SlideTileAction(
//icon: Icons.delete, //icon: Icons.delete,
label: translate('button.delete'), label: translate('button.delete'),
actionScale: ScaleKind.tertiary, actionScale: ScaleKind.tertiary,

View file

@ -92,7 +92,7 @@ class _ContactsBrowserState extends State<ContactsBrowser>
final menuParams = StarMenuParameters( final menuParams = StarMenuParameters(
shape: MenuShape.linear, shape: MenuShape.linear,
centerOffset: const Offset(0, 64), centerOffset: Offset(0, 64.scaled(context)),
boundaryBackground: BoundaryBackground( boundaryBackground: BoundaryBackground(
color: menuBackgroundColor, color: menuBackgroundColor,
decoration: ShapeDecoration( decoration: ShapeDecoration(
@ -113,13 +113,14 @@ class _ContactsBrowserState extends State<ContactsBrowser>
onPressed: onPressed, onPressed: onPressed,
icon: Icon( icon: Icon(
iconData, iconData,
size: 32, size: 32.scaled(context),
).paddingSTEB(0, 8, 0, 8), ).paddingSTEB(0, 8.scaled(context), 0, 8.scaled(context)),
label: Text( label: Text(
text, text,
textScaler: MediaQuery.of(context).textScaler,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
).paddingSTEB(0, 8, 0, 8)); ).paddingSTEB(0, 8.scaled(context), 0, 8.scaled(context)));
final inviteMenuItems = [ final inviteMenuItems = [
makeMenuButton( makeMenuButton(
@ -135,14 +136,14 @@ class _ContactsBrowserState extends State<ContactsBrowser>
onPressed: () async { onPressed: () async {
_invitationMenuController.closeMenu!(); _invitationMenuController.closeMenu!();
await ScanInvitationDialog.show(context); await ScanInvitationDialog.show(context);
}).paddingLTRB(0, 0, 0, 8), }).paddingLTRB(0, 0, 0, 8.scaled(context)),
makeMenuButton( makeMenuButton(
iconData: Icons.contact_page, iconData: Icons.contact_page,
text: translate('add_contact_sheet.create_invite'), text: translate('add_contact_sheet.create_invite'),
onPressed: () async { onPressed: () async {
_invitationMenuController.closeMenu!(); _invitationMenuController.closeMenu!();
await CreateInvitationDialog.show(context); await CreateInvitationDialog.show(context);
}).paddingLTRB(0, 0, 0, 8), }).paddingLTRB(0, 0, 0, 8.scaled(context)),
]; ];
return StarMenu( return StarMenu(
@ -154,7 +155,7 @@ class _ContactsBrowserState extends State<ContactsBrowser>
params: menuParams, params: menuParams,
child: IconButton( child: IconButton(
onPressed: () {}, onPressed: () {},
iconSize: 24, iconSize: 24.scaled(context),
icon: Icon(Icons.person_add, color: menuIconColor), icon: Icon(Icons.person_add, color: menuIconColor),
tooltip: translate('add_contact_sheet.add_contact')), tooltip: translate('add_contact_sheet.add_contact')),
); );
@ -202,13 +203,13 @@ class _ContactsBrowserState extends State<ContactsBrowser>
onDoubleTap: _onStartChat, onDoubleTap: _onStartChat,
onTap: onContactSelected, onTap: onContactSelected,
onDelete: _onContactDeleted) onDelete: _onContactDeleted)
.paddingLTRB(0, 4, 0, 0); .paddingLTRB(0, 4.scaled(context), 0, 0);
case ContactsBrowserElementKind.invitation: case ContactsBrowserElementKind.invitation:
final invitation = element.invitation!; final invitation = element.invitation!;
return ContactInvitationItemWidget( return ContactInvitationItemWidget(
contactInvitationRecord: invitation, contactInvitationRecord: invitation,
disabled: false) disabled: false)
.paddingLTRB(0, 4, 0, 0); .paddingLTRB(0, 4.scaled(context), 0, 0);
} }
}, },
filter: (value) { filter: (value) {
@ -242,9 +243,11 @@ class _ContactsBrowserState extends State<ContactsBrowser>
} }
return filtered; return filtered;
}, },
searchFieldHeight: 40, searchFieldHeight: 40.scaled(context),
listViewPadding: const EdgeInsets.fromLTRB(4, 0, 4, 4), listViewPadding:
searchFieldPadding: const EdgeInsets.fromLTRB(4, 8, 4, 4), const EdgeInsets.fromLTRB(4, 0, 4, 4).scaled(context),
searchFieldPadding:
const EdgeInsets.fromLTRB(4, 8, 4, 4).scaled(context),
emptyWidget: contactList == null emptyWidget: contactList == null
? waitingPage( ? waitingPage(
text: translate('contact_list.loading_contacts')) text: translate('contact_list.loading_contacts'))
@ -254,8 +257,8 @@ class _ContactsBrowserState extends State<ContactsBrowser>
searchFieldEnabled: contactList != null, searchFieldEnabled: contactList != null,
inputDecoration: inputDecoration:
InputDecoration(labelText: translate('contact_list.search')), InputDecoration(labelText: translate('contact_list.search')),
secondaryWidget: secondaryWidget: buildInvitationButton(context)
buildInvitationButton(context).paddingLTRB(4, 0, 0, 0)) .paddingLTRB(4.scaled(context), 0, 0, 0))
.expanded() .expanded()
]); ]);
} }

View file

@ -40,6 +40,7 @@ class _ContactsPageState extends State<ContactsPage> {
return StyledScaffold( return StyledScaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
context: context,
title: Text( title: Text(
!enableSplit && enableRight !enableSplit && enableRight
? translate('contacts_dialog.edit_contact') ? translate('contacts_dialog.edit_contact')
@ -47,6 +48,7 @@ class _ContactsPageState extends State<ContactsPage> {
), ),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
iconSize: 24.scaled(context),
onPressed: () { onPressed: () {
singleFuture((this, _kDoBackArrow), () async { singleFuture((this, _kDoBackArrow), () async {
final confirmed = await _onContactSelected(null); final confirmed = await _onContactSelected(null);
@ -65,21 +67,21 @@ class _ContactsPageState extends State<ContactsPage> {
if (_selectedContact != null) if (_selectedContact != null)
IconButton( IconButton(
icon: const Icon(Icons.chat_bubble), icon: const Icon(Icons.chat_bubble),
iconSize: 24, iconSize: 24.scaled(context),
color: appBarTheme.iconColor, color: appBarTheme.iconColor,
tooltip: translate('contacts_dialog.new_chat'), tooltip: translate('contacts_dialog.new_chat'),
onPressed: () async { onPressed: () async {
await _onChatStarted(_selectedContact!); await _onChatStarted(_selectedContact!);
}).paddingLTRB(8, 0, 8, 0), }),
if (enableSplit && _selectedContact != null) if (enableSplit && _selectedContact != null)
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
iconSize: 24, iconSize: 24.scaled(context),
color: appBarTheme.iconColor, color: appBarTheme.iconColor,
tooltip: translate('contacts_dialog.close_contact'), tooltip: translate('contacts_dialog.close_contact'),
onPressed: () async { onPressed: () async {
await _onContactSelected(null); await _onContactSelected(null);
}).paddingLTRB(8, 0, 8, 0), }),
]), ]),
body: LayoutBuilder(builder: (context, constraint) { body: LayoutBuilder(builder: (context, constraint) {
final maxWidth = constraint.maxWidth; final maxWidth = constraint.maxWidth;

View file

@ -92,16 +92,8 @@ class _EditContactFormState extends State<EditContactForm> {
Widget _editContactForm(BuildContext context) { Widget _editContactForm(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!;
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
late final Color border;
if (scaleConfig.useVisualIndicators && !scaleConfig.preferBorders) {
border = scale.primaryScale.elementBackground;
} else {
border = scale.primaryScale.subtleBorder;
}
return FormBuilder( return FormBuilder(
key: _formKey, key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
@ -116,18 +108,12 @@ class _EditContactFormState extends State<EditContactForm> {
children: [ children: [
Row(children: [ Row(children: [
const Spacer(), const Spacer(),
AvatarWidget( StyledAvatar(
name: _currentValueNickname.isNotEmpty name: _currentValueNickname.isNotEmpty
? _currentValueNickname ? _currentValueNickname
: widget.contact.profile.name, : widget.contact.profile.name,
size: 128, size: 128)
borderColor: border, .paddingLTRB(0, 0, 0, 16),
foregroundColor: scale.primaryScale.primaryText,
backgroundColor: scale.primaryScale.primary,
scaleConfig: scaleConfig,
textStyle: theme.textTheme.titleLarge!
.copyWith(fontSize: 64),
).paddingLTRB(0, 0, 0, 16),
const Spacer() const Spacer()
]), ]),
SelectableText(widget.contact.profile.name, SelectableText(widget.contact.profile.name,
@ -211,10 +197,11 @@ class _EditContactFormState extends State<EditContactForm> {
ElevatedButton( ElevatedButton(
onPressed: _isModified ? _doSubmit : null, onPressed: _isModified ? _doSubmit : null,
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0), Icon(Icons.check, size: 24.scaled(context))
Text(widget.submitText).paddingLTRB(0, 0, 4, 0) .paddingLTRB(0, 0, 4, 0),
]), Text(widget.submitText).paddingLTRB(0, 0, 4.scaled(context), 0)
).paddingSymmetric(vertical: 4).alignAtCenter(), ]).paddingAll(4.scaled(context)),
).paddingSymmetric(vertical: 4.scaled(context)).alignAtCenter(),
], ],
), ),
); );

View file

@ -32,6 +32,14 @@ class DeveloperPageIntent extends Intent {
const DeveloperPageIntent(); const DeveloperPageIntent();
} }
class DisplayScaleUpIntent extends Intent {
const DisplayScaleUpIntent();
}
class DisplayScaleDownIntent extends Intent {
const DisplayScaleDownIntent();
}
class KeyboardShortcuts extends StatelessWidget { class KeyboardShortcuts extends StatelessWidget {
const KeyboardShortcuts({required this.child, super.key}); const KeyboardShortcuts({required this.child, super.key});
@ -57,7 +65,7 @@ class KeyboardShortcuts extends StatelessWidget {
}); });
} }
void changeBrightness(BuildContext context) { void _changeBrightness(BuildContext context) {
singleFuture(this, () async { singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value; final prefs = PreferencesRepository.instance.value;
@ -79,7 +87,7 @@ class KeyboardShortcuts extends StatelessWidget {
}); });
} }
void changeColor(BuildContext context) { void _changeColor(BuildContext context) {
singleFuture(this, () async { singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value; final prefs = PreferencesRepository.instance.value;
final oldColor = prefs.themePreference.colorPreference; final oldColor = prefs.themePreference.colorPreference;
@ -100,6 +108,54 @@ class KeyboardShortcuts extends StatelessWidget {
}); });
} }
void _displayScaleUp(BuildContext context) {
singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value;
final oldIndex = displayScaleToIndex(prefs.themePreference.displayScale);
if (oldIndex == maxDisplayScaleIndex) {
return;
}
final newIndex = oldIndex + 1;
final newDisplayScaleName = indexToDisplayScaleName(newIndex);
log.info('Changing display scale to $newDisplayScaleName');
final newPrefs = prefs.copyWith(
themePreference: prefs.themePreference
.copyWith(displayScale: indexToDisplayScale(newIndex)));
await PreferencesRepository.instance.set(newPrefs);
if (context.mounted) {
ThemeSwitcher.of(context)
.changeTheme(theme: newPrefs.themePreference.themeData());
}
});
}
void _displayScaleDown(BuildContext context) {
singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value;
final oldIndex = displayScaleToIndex(prefs.themePreference.displayScale);
if (oldIndex == 0) {
return;
}
final newIndex = oldIndex - 1;
final newDisplayScaleName = indexToDisplayScaleName(newIndex);
log.info('Changing display scale to $newDisplayScaleName');
final newPrefs = prefs.copyWith(
themePreference: prefs.themePreference
.copyWith(displayScale: indexToDisplayScale(newIndex)));
await PreferencesRepository.instance.set(newPrefs);
if (context.mounted) {
ThemeSwitcher.of(context)
.changeTheme(theme: newPrefs.themePreference.themeData());
}
});
}
void _attachDetach(BuildContext context) { void _attachDetach(BuildContext context) {
singleFuture(this, () async { singleFuture(this, () async {
if (ProcessorRepository.instance.processorConnectionState.isAttached) { if (ProcessorRepository.instance.processorConnectionState.isAttached) {
@ -125,44 +181,88 @@ class KeyboardShortcuts extends StatelessWidget {
@override @override
Widget build(BuildContext context) => ThemeSwitcher( Widget build(BuildContext context) => ThemeSwitcher(
builder: (context) => Shortcuts( builder: (context) => Shortcuts(
shortcuts: const <ShortcutActivator, Intent>{ shortcuts: <ShortcutActivator, Intent>{
SingleActivator( ////////////////////////// Reload Theme
const SingleActivator(
LogicalKeyboardKey.keyR, LogicalKeyboardKey.keyR,
control: true, control: true,
alt: true, alt: true,
): ReloadThemeIntent(), ): const ReloadThemeIntent(),
SingleActivator( ////////////////////////// Switch Brightness
const SingleActivator(
LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyB,
control: true, control: true,
alt: true, alt: true,
): ChangeBrightnessIntent(), ): const ChangeBrightnessIntent(),
SingleActivator( ////////////////////////// Change Color
const SingleActivator(
LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyC,
control: true, control: true,
alt: true, alt: true,
): ChangeColorIntent(), ): const ChangeColorIntent(),
SingleActivator( ////////////////////////// Attach/Detach
if (kIsDebugMode)
const SingleActivator(
LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyA,
control: true, control: true,
alt: true, alt: true,
): AttachDetachIntent(), ): const AttachDetachIntent(),
SingleActivator( ////////////////////////// Show Developer Page
const SingleActivator(
LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyD,
control: true, control: true,
alt: true, alt: true,
): DeveloperPageIntent(), ): const DeveloperPageIntent(),
////////////////////////// Display Scale Up
SingleActivator(
LogicalKeyboardKey.equal,
meta: isMac || isiOS,
control: !(isMac || isiOS),
): const DisplayScaleUpIntent(),
SingleActivator(
LogicalKeyboardKey.equal,
shift: true,
meta: isMac || isiOS,
control: !(isMac || isiOS),
): const DisplayScaleUpIntent(),
SingleActivator(
LogicalKeyboardKey.add,
shift: true,
meta: isMac || isiOS,
control: !(isMac || isiOS),
): const DisplayScaleUpIntent(),
SingleActivator(
LogicalKeyboardKey.numpadAdd,
meta: isMac || isiOS,
control: !(isMac || isiOS),
): const DisplayScaleUpIntent(),
////////////////////////// Display Scale Down
SingleActivator(
LogicalKeyboardKey.minus,
meta: isMac || isiOS,
control: !(isMac || isiOS),
): const DisplayScaleDownIntent(),
SingleActivator(
LogicalKeyboardKey.numpadSubtract,
meta: isMac || isiOS,
control: !(isMac || isiOS),
): const DisplayScaleDownIntent(),
}, },
child: Actions(actions: <Type, Action<Intent>>{ child: Actions(actions: <Type, Action<Intent>>{
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>( ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
onInvoke: (intent) => reloadTheme(context)), onInvoke: (intent) => reloadTheme(context)),
ChangeBrightnessIntent: CallbackAction<ChangeBrightnessIntent>( ChangeBrightnessIntent: CallbackAction<ChangeBrightnessIntent>(
onInvoke: (intent) => changeBrightness(context)), onInvoke: (intent) => _changeBrightness(context)),
ChangeColorIntent: CallbackAction<ChangeColorIntent>( ChangeColorIntent: CallbackAction<ChangeColorIntent>(
onInvoke: (intent) => changeColor(context)), onInvoke: (intent) => _changeColor(context)),
AttachDetachIntent: CallbackAction<AttachDetachIntent>( AttachDetachIntent: CallbackAction<AttachDetachIntent>(
onInvoke: (intent) => _attachDetach(context)), onInvoke: (intent) => _attachDetach(context)),
DeveloperPageIntent: CallbackAction<DeveloperPageIntent>( DeveloperPageIntent: CallbackAction<DeveloperPageIntent>(
onInvoke: (intent) => _developerPage(context)), onInvoke: (intent) => _developerPage(context)),
DisplayScaleUpIntent: CallbackAction<DisplayScaleUpIntent>(
onInvoke: (intent) => _displayScaleUp(context)),
DisplayScaleDownIntent: CallbackAction<DisplayScaleDownIntent>(
onInvoke: (intent) => _displayScaleDown(context)),
}, child: Focus(autofocus: true, child: child)))); }, child: Focus(autofocus: true, child: child))));
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////

View file

@ -2,21 +2,27 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import '../theme/theme.dart';
class DefaultAppBar extends AppBar { class DefaultAppBar extends AppBar {
DefaultAppBar( DefaultAppBar(
{super.title, {required BuildContext context,
super.title,
super.flexibleSpace, super.flexibleSpace,
super.key, super.key,
Widget? leading, Widget? leading,
super.actions}) super.actions})
: super( : super(
toolbarHeight: 40.scaled(context),
leadingWidth: 40.scaled(context),
leading: leading ?? leading: leading ??
Container( Container(
margin: const EdgeInsets.all(4), margin: const EdgeInsets.all(4).scaled(context),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withAlpha(32), color: Colors.black.withAlpha(32),
shape: BoxShape.circle), shape: BoxShape.circle),
child: child: SvgPicture.asset('assets/images/vlogo.svg',
SvgPicture.asset('assets/images/vlogo.svg', height: 24) width: 24.scaled(context),
.paddingAll(4))); height: 24.scaled(context))
.paddingAll(4.scaled(context))));
} }

View file

@ -95,19 +95,15 @@ class _DrawerMenuState extends State<DrawerMenu> {
activeBorder = scale.primary; activeBorder = scale.primary;
} }
final avatar = AvatarWidget( final avatar = StyledAvatar(
name: name, name: name,
size: 34, size: 34.scaled(context),
borderColor: border,
foregroundColor: loggedIn ? scale.primaryText : scale.subtleText,
backgroundColor: loggedIn ? scale.primary : scale.elementBackground,
scaleConfig: scaleConfig,
textStyle: theme.textTheme.titleLarge!,
); );
return AnimatedPadding( return AnimatedPadding(
padding: EdgeInsets.fromLTRB(selected ? 0 : 8, selected ? 0 : 2, padding: EdgeInsets.fromLTRB(selected ? 0 : 8, selected ? 0 : 2,
selected ? 0 : 8, selected ? 0 : 2), selected ? 0 : 8, selected ? 0 : 2)
.scaled(context),
duration: const Duration(milliseconds: 50), duration: const Duration(milliseconds: 50),
child: MenuItemWidget( child: MenuItemWidget(
title: name, title: name,
@ -144,7 +140,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
(scaleConfig.preferBorders || scaleConfig.useVisualIndicators) (scaleConfig.preferBorders || scaleConfig.useVisualIndicators)
? null ? null
: activeBorder, : activeBorder,
minHeight: 48, minHeight: 48.scaled(context),
)); ));
} }
@ -196,7 +192,8 @@ class _DrawerMenuState extends State<DrawerMenu> {
color: scaleScheme.errorScale.subtleBorder, color: scaleScheme.errorScale.subtleBorder,
borderRadius: 12 * scaleConfig.borderRadiusScale), borderRadius: 12 * scaleConfig.borderRadiusScale),
); );
loggedInAccounts.add(loggedInAccount.paddingLTRB(0, 0, 0, 8)); loggedInAccounts
.add(loggedInAccount.paddingLTRB(0, 0, 0, 8.scaled(context)));
} else { } else {
// Account is not logged in // Account is not logged in
final scale = theme.extension<ScaleScheme>()!.grayScale; final scale = theme.extension<ScaleScheme>()!.grayScale;
@ -246,8 +243,8 @@ class _DrawerMenuState extends State<DrawerMenu> {
} }
return IconButton( return IconButton(
icon: icon, icon: icon,
padding: const EdgeInsets.all(12),
color: border, color: border,
constraints: const BoxConstraints.expand(height: 48, width: 48),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((states) { backgroundColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) { if (states.contains(WidgetState.hovered)) {
@ -286,7 +283,10 @@ class _DrawerMenuState extends State<DrawerMenu> {
final scale = scaleScheme.scale(_scaleKind); final scale = scaleScheme.scale(_scaleKind);
final settingsButton = _getButton( final settingsButton = _getButton(
icon: const Icon(Icons.settings), icon: const Icon(
Icons.settings,
applyTextScaling: true,
),
tooltip: translate('menu.settings_tooltip'), tooltip: translate('menu.settings_tooltip'),
scale: scale, scale: scale,
scaleConfig: scaleConfig, scaleConfig: scaleConfig,
@ -295,7 +295,10 @@ class _DrawerMenuState extends State<DrawerMenu> {
}).paddingLTRB(0, 0, 16, 0); }).paddingLTRB(0, 0, 16, 0);
final addButton = _getButton( final addButton = _getButton(
icon: const Icon(Icons.add), icon: const Icon(
Icons.add,
applyTextScaling: true,
),
tooltip: translate('menu.add_account_tooltip'), tooltip: translate('menu.add_account_tooltip'),
scale: scale, scale: scale,
scaleConfig: scaleConfig, scaleConfig: scaleConfig,
@ -364,7 +367,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
// : null) // : null)
// .paddingLTRB(0, 0, 16, 0), // .paddingLTRB(0, 0, 16, 0),
GestureDetector( GestureDetector(
onLongPress: () async { onLongPress: () {
context context
.findAncestorWidgetOfExactType<KeyboardShortcuts>()! .findAncestorWidgetOfExactType<KeyboardShortcuts>()!
.reloadTheme(context); .reloadTheme(context);

View file

@ -2,6 +2,8 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../theme/views/preferences/preferences.dart';
class MenuItemWidget extends StatelessWidget { class MenuItemWidget extends StatelessWidget {
const MenuItemWidget({ const MenuItemWidget({
required this.title, required this.title,
@ -81,7 +83,7 @@ class MenuItemWidget extends StatelessWidget {
hoverColor: footerButtonIconHoverColor, hoverColor: footerButtonIconHoverColor,
icon: Icon( icon: Icon(
footerButtonIcon, footerButtonIcon,
size: 24, size: 24.scaled(context),
), ),
onPressed: footerCallback), onPressed: footerCallback),
], ],

View file

@ -28,12 +28,16 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
return IconButton( return AspectRatio(
icon: const Icon(Icons.menu), aspectRatio: 1,
child: IconButton(
icon: const Icon(
Icons.menu,
applyTextScaling: true,
),
color: scaleConfig.preferBorders color: scaleConfig.preferBorders
? scale.primaryScale.border ? scale.primaryScale.border
: scale.primaryScale.borderText, : scale.primaryScale.borderText,
constraints: const BoxConstraints.expand(height: 40, width: 40),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: WidgetStateProperty.all( backgroundColor: WidgetStateProperty.all(
scaleConfig.preferBorders scaleConfig.preferBorders
@ -49,26 +53,30 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
? scale.primaryScale.border ? scale.primaryScale.border
: scale.primaryScale.borderText, : scale.primaryScale.borderText,
width: 2), width: 2),
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(
Radius.circular(8 * scaleConfig.borderRadiusScale))), 8 * scaleConfig.borderRadiusScale))),
)), )),
tooltip: translate('menu.accounts_menu_tooltip'), tooltip: translate('menu.accounts_menu_tooltip'),
onPressed: () async { onPressed: () async {
final ctrl = context.read<ZoomDrawerController>(); final ctrl = context.read<ZoomDrawerController>();
await ctrl.toggle?.call(); await ctrl.toggle?.call();
}); }));
}); });
Widget buildContactsButton() => Builder(builder: (context) { Widget buildContactsButton() => Builder(builder: (context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
return IconButton( return AspectRatio(
icon: const Icon(Icons.contacts), aspectRatio: 1,
child: IconButton(
icon: const Icon(
Icons.contacts,
applyTextScaling: true,
),
color: scaleConfig.preferBorders color: scaleConfig.preferBorders
? scale.primaryScale.border ? scale.primaryScale.border
: scale.primaryScale.borderText, : scale.primaryScale.borderText,
constraints: const BoxConstraints.expand(height: 40, width: 40),
style: ButtonStyle( style: ButtonStyle(
backgroundColor: WidgetStateProperty.all( backgroundColor: WidgetStateProperty.all(
scaleConfig.preferBorders scaleConfig.preferBorders
@ -84,8 +92,8 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
? scale.primaryScale.border ? scale.primaryScale.border
: scale.primaryScale.borderText, : scale.primaryScale.borderText,
width: 2), width: 2),
borderRadius: BorderRadius.all( borderRadius: BorderRadius.all(Radius.circular(
Radius.circular(8 * scaleConfig.borderRadiusScale))), 8 * scaleConfig.borderRadiusScale))),
)), )),
tooltip: translate('menu.contacts_tooltip'), tooltip: translate('menu.contacts_tooltip'),
onPressed: () async { onPressed: () async {
@ -94,7 +102,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
builder: (_) => const ContactsPage(), builder: (_) => const ContactsPage(),
), ),
); );
}); }));
}); });
Widget buildLeftPane(BuildContext context) => Builder( Widget buildLeftPane(BuildContext context) => Builder(
@ -112,14 +120,17 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
? scale.primaryScale.subtleBackground ? scale.primaryScale.subtleBackground
: scale.primaryScale.subtleBorder, : scale.primaryScale.subtleBorder,
child: Column(children: <Widget>[ child: Column(children: <Widget>[
Row(children: [ IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildMenuButton().paddingLTRB(0, 0, 8, 0), buildMenuButton().paddingLTRB(0, 0, 8, 0),
ProfileWidget( ProfileWidget(
profile: profile, profile: profile,
showPronouns: false, showPronouns: false,
).expanded(), ).expanded(),
buildContactsButton().paddingLTRB(8, 0, 0, 0), buildContactsButton().paddingLTRB(8, 0, 0, 0),
]).paddingAll(8), ])).paddingAll(8),
const ChatListWidget().expanded() const ChatListWidget().expanded()
])); ]));
}))); })));

View file

@ -71,8 +71,9 @@ class HomeScreenState extends State<HomeScreen>
context: context, context: context,
title: translate('splash.beta_title'), title: translate('splash.beta_title'),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const Icon(Icons.warning, size: 64), Icon(Icons.warning, size: 64.scaled(context)),
RichText( RichText(
textScaler: MediaQuery.of(context).textScaler,
textAlign: TextAlign.center, textAlign: TextAlign.center,
text: TextSpan( text: TextSpan(
children: <TextSpan>[ children: <TextSpan>[
@ -206,11 +207,7 @@ class HomeScreenState extends State<HomeScreen>
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount); .indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1; final canClose = activeIndex != -1;
return DefaultTextStyle( final drawer = ZoomDrawer(
style: theme.textTheme.bodySmall!,
child: KeyboardAvoider(
curve: Curves.ease,
child: ZoomDrawer(
controller: _zoomDrawerController, controller: _zoomDrawerController,
menuScreen: Builder(builder: (context) { menuScreen: Builder(builder: (context) {
final zoomDrawer = ZoomDrawer.of(context); final zoomDrawer = ZoomDrawer.of(context);
@ -233,7 +230,13 @@ class HomeScreenState extends State<HomeScreen>
disableDragGesture: !canClose, disableDragGesture: !canClose,
mainScreenScale: .25, mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
))); );
final drawerWithAvoider =
isWeb ? drawer : KeyboardAvoider(curve: Curves.ease, child: drawer);
return DefaultTextStyle(
style: theme.textTheme.bodySmall!, child: drawerWithAvoider);
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View file

@ -1,24 +1,13 @@
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../settings/settings.dart'; import '../../settings/settings.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../notifications.dart'; import '../notifications.dart';
const String formFieldDisplayBetaWarning = 'displayBetaWarning';
const String formFieldEnableBadge = 'enableBadge';
const String formFieldEnableNotifications = 'enableNotifications';
const String formFieldMessageNotificationContent = 'messageNotificationContent';
const String formFieldInvitationAcceptMode = 'invitationAcceptMode';
const String formFieldInvitationAcceptSound = 'invitationAcceptSound';
const String formFieldMessageReceivedMode = 'messageReceivedMode';
const String formFieldMessageReceivedSound = 'messageReceivedSound';
const String formFieldMessageSentSound = 'messageSentSound';
Widget buildSettingsPageNotificationPreferences( Widget buildSettingsPageNotificationPreferences(
{required BuildContext context, required void Function() onChanged}) { {required BuildContext context}) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scaleConfig = theme.extension<ScaleConfig>()!;
@ -33,7 +22,6 @@ Widget buildSettingsPageNotificationPreferences(
final newPrefs = preferencesRepository.value final newPrefs = preferencesRepository.value
.copyWith(notificationsPreference: newNotificationsPreference); .copyWith(notificationsPreference: newNotificationsPreference);
await preferencesRepository.set(newPrefs); await preferencesRepository.set(newPrefs);
onChanged();
} }
List<DropdownMenuItem<NotificationMode>> notificationModeItems() { List<DropdownMenuItem<NotificationMode>> notificationModeItems() {
@ -54,9 +42,10 @@ Widget buildSettingsPageNotificationPreferences(
enabled: x.$2, enabled: x.$2,
child: Text( child: Text(
x.$3, x.$3,
style: textTheme.labelSmall, softWrap: false,
style: textTheme.labelMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
))); ).fit(fit: BoxFit.scaleDown)));
} }
return out; return out;
} }
@ -77,7 +66,8 @@ Widget buildSettingsPageNotificationPreferences(
enabled: x.$2, enabled: x.$2,
child: Text( child: Text(
x.$3, x.$3,
style: textTheme.labelSmall, softWrap: false,
style: textTheme.labelMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
))); )));
} }
@ -110,7 +100,8 @@ Widget buildSettingsPageNotificationPreferences(
enabled: x.$2, enabled: x.$2,
child: Text( child: Text(
x.$3, x.$3,
style: textTheme.labelSmall, softWrap: false,
style: textTheme.labelMedium,
textAlign: TextAlign.center, textAlign: TextAlign.center,
))); )));
} }
@ -127,66 +118,45 @@ Widget buildSettingsPageNotificationPreferences(
), ),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
// Display Beta Warning // Display Beta Warning
FormBuilderCheckbox( StyledCheckbox(
name: formFieldDisplayBetaWarning, label: translate('settings_page.display_beta_warning'),
title: Text(translate('settings_page.display_beta_warning'), value: notificationsPreference.displayBetaWarning,
style: textTheme.labelMedium),
initialValue: notificationsPreference.displayBetaWarning,
onChanged: (value) async { onChanged: (value) async {
if (value == null) {
return;
}
final newNotificationsPreference = final newNotificationsPreference =
notificationsPreference.copyWith(displayBetaWarning: value); notificationsPreference.copyWith(displayBetaWarning: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}), }),
// Enable Badge // Enable Badge
FormBuilderCheckbox( StyledCheckbox(
name: formFieldEnableBadge, label: translate('settings_page.enable_badge'),
title: Text(translate('settings_page.enable_badge'), value: notificationsPreference.enableBadge,
style: textTheme.labelMedium),
initialValue: notificationsPreference.enableBadge,
onChanged: (value) async { onChanged: (value) async {
if (value == null) {
return;
}
final newNotificationsPreference = final newNotificationsPreference =
notificationsPreference.copyWith(enableBadge: value); notificationsPreference.copyWith(enableBadge: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}), }),
// Enable Notifications // Enable Notifications
FormBuilderCheckbox( StyledCheckbox(
name: formFieldEnableNotifications, label: translate('settings_page.enable_notifications'),
title: Text(translate('settings_page.enable_notifications'), value: notificationsPreference.enableNotifications,
style: textTheme.labelMedium),
initialValue: notificationsPreference.enableNotifications,
onChanged: (value) async { onChanged: (value) async {
if (value == null) {
return;
}
final newNotificationsPreference = final newNotificationsPreference =
notificationsPreference.copyWith(enableNotifications: value); notificationsPreference.copyWith(enableNotifications: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}), }),
StyledDropdown<MessageNotificationContent>(
FormBuilderDropdown( items: messageNotificationContentItems(),
name: formFieldMessageNotificationContent, value: notificationsPreference.messageNotificationContent,
isDense: false, decoratorLabel: translate('settings_page.message_notification_content'),
decoration: InputDecoration( onChanged: !notificationsPreference.enableNotifications
labelText: translate('settings_page.message_notification_content')), ? null
enabled: notificationsPreference.enableNotifications, : (value) async {
initialValue: notificationsPreference.messageNotificationContent, final newNotificationsPreference = notificationsPreference
onChanged: (value) async { .copyWith(messageNotificationContent: value);
if (value == null) {
return;
}
final newNotificationsPreference = notificationsPreference.copyWith(
messageNotificationContent: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
items: messageNotificationContentItems(), ).paddingLTRB(0, 4.scaled(context), 0, 4.scaled(context)),
).paddingLTRB(0, 4, 0, 4),
// Notifications // Notifications
Table( Table(
@ -199,95 +169,85 @@ Widget buildSettingsPageNotificationPreferences(
color: scale.primaryScale.border, color: scale.primaryScale.border,
decorationColor: scale.primaryScale.border, decorationColor: scale.primaryScale.border,
decoration: TextDecoration.underline)) decoration: TextDecoration.underline))
.paddingAll(8), .paddingAll(8.scaled(context)),
Text(translate('settings_page.delivery'), Text(translate('settings_page.delivery'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.titleMedium!.copyWith( style: textTheme.titleMedium!.copyWith(
color: scale.primaryScale.border, color: scale.primaryScale.border,
decorationColor: scale.primaryScale.border, decorationColor: scale.primaryScale.border,
decoration: TextDecoration.underline)) decoration: TextDecoration.underline))
.paddingAll(8), .paddingAll(8.scaled(context)),
Text(translate('settings_page.sound'), Text(translate('settings_page.sound'),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.titleMedium!.copyWith( style: textTheme.titleMedium!.copyWith(
color: scale.primaryScale.border, color: scale.primaryScale.border,
decorationColor: scale.primaryScale.border, decorationColor: scale.primaryScale.border,
decoration: TextDecoration.underline)) decoration: TextDecoration.underline))
.paddingAll(8), .paddingAll(8.scaled(context)),
]), ]),
TableRow(children: [ TableRow(children: [
// Invitation accepted // Invitation accepted
Text( Text(
textAlign: TextAlign.right, textAlign: TextAlign.right,
translate('settings_page.invitation_accepted')) translate('settings_page.invitation_accepted'))
.paddingAll(8), .paddingAll(4.scaled(context)),
FormBuilderDropdown( StyledDropdown<NotificationMode>(
name: formFieldInvitationAcceptMode,
isDense: false,
enabled: notificationsPreference.enableNotifications,
initialValue: notificationsPreference.onInvitationAcceptedMode,
onChanged: (value) async {
if (value == null) {
return;
}
final newNotificationsPreference = notificationsPreference
.copyWith(onInvitationAcceptedMode: value);
await updatePreferences(newNotificationsPreference);
},
items: notificationModeItems(), items: notificationModeItems(),
).paddingAll(4), value: notificationsPreference.onInvitationAcceptedMode,
FormBuilderDropdown( onChanged: !notificationsPreference.enableNotifications
name: formFieldInvitationAcceptSound, ? null
isDense: false, : (value) async {
enabled: notificationsPreference.enableNotifications, final newNotificationsPreference =
initialValue: notificationsPreference.onInvitationAcceptedSound, notificationsPreference.copyWith(
onChanged: (value) async { onInvitationAcceptedMode: value);
if (value == null) {
return;
}
final newNotificationsPreference = notificationsPreference
.copyWith(onInvitationAcceptedSound: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
).paddingAll(4.scaled(context)),
StyledDropdown<SoundEffect>(
items: soundEffectItems(), items: soundEffectItems(),
).paddingLTRB(4, 4, 0, 4) value: notificationsPreference.onInvitationAcceptedSound,
onChanged: !notificationsPreference.enableNotifications
? null
: (value) async {
final newNotificationsPreference =
notificationsPreference.copyWith(
onInvitationAcceptedSound: value);
await updatePreferences(newNotificationsPreference);
},
).paddingLTRB(
4.scaled(context), 4.scaled(context), 0, 4.scaled(context))
]), ]),
// Message received // Message received
TableRow(children: [ TableRow(children: [
Text( Text(
textAlign: TextAlign.right, textAlign: TextAlign.right,
translate('settings_page.message_received')) translate('settings_page.message_received'))
.paddingAll(8), .paddingAll(4.scaled(context)),
FormBuilderDropdown( StyledDropdown<NotificationMode>(
name: formFieldMessageReceivedMode,
isDense: false,
enabled: notificationsPreference.enableNotifications,
initialValue: notificationsPreference.onMessageReceivedMode,
onChanged: (value) async {
if (value == null) {
return;
}
final newNotificationsPreference = notificationsPreference
.copyWith(onMessageReceivedMode: value);
await updatePreferences(newNotificationsPreference);
},
items: notificationModeItems(), items: notificationModeItems(),
).paddingAll(4), value: notificationsPreference.onMessageReceivedMode,
FormBuilderDropdown( onChanged: !notificationsPreference.enableNotifications
name: formFieldMessageReceivedSound, ? null
isDense: false, : (value) async {
enabled: notificationsPreference.enableNotifications, final newNotificationsPreference =
initialValue: notificationsPreference.onMessageReceivedSound, notificationsPreference.copyWith(
onChanged: (value) async { onMessageReceivedMode: value);
if (value == null) {
return;
}
final newNotificationsPreference = notificationsPreference
.copyWith(onMessageReceivedSound: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
).paddingAll(4),
StyledDropdown<SoundEffect>(
items: soundEffectItems(), items: soundEffectItems(),
).paddingLTRB(4, 4, 0, 4) value: notificationsPreference.onMessageReceivedSound,
onChanged: !notificationsPreference.enableNotifications
? null
: (value) async {
final newNotificationsPreference =
notificationsPreference.copyWith(
onMessageReceivedSound: value);
await updatePreferences(newNotificationsPreference);
},
).paddingLTRB(
4.scaled(context), 4.scaled(context), 0, 4.scaled(context))
]), ]),
// Message sent // Message sent
@ -295,25 +255,23 @@ Widget buildSettingsPageNotificationPreferences(
Text( Text(
textAlign: TextAlign.right, textAlign: TextAlign.right,
translate('settings_page.message_sent')) translate('settings_page.message_sent'))
.paddingAll(8), .paddingAll(4.scaled(context)),
const SizedBox.shrink(), const SizedBox.shrink(),
FormBuilderDropdown( StyledDropdown<SoundEffect>(
name: formFieldMessageSentSound, items: soundEffectItems(),
isDense: false, value: notificationsPreference.onMessageSentSound,
enabled: notificationsPreference.enableNotifications, onChanged: !notificationsPreference.enableNotifications
initialValue: notificationsPreference.onMessageSentSound, ? null
onChanged: (value) async { : (value) async {
if (value == null) { final newNotificationsPreference =
return; notificationsPreference.copyWith(
} onMessageSentSound: value);
final newNotificationsPreference = notificationsPreference
.copyWith(onMessageSentSound: value);
await updatePreferences(newNotificationsPreference); await updatePreferences(newNotificationsPreference);
}, },
items: soundEffectItems(), ).paddingLTRB(
).paddingLTRB(4, 4, 0, 4) 4.scaled(context), 4.scaled(context), 0, 4.scaled(context))
]), ]),
]) ])
]).paddingAll(8), ]).paddingAll(8.scaled(context)),
); );
} }

View file

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../../keyboard_shortcuts.dart'; import '../../keyboard_shortcuts.dart';
import '../../notifications/notifications.dart'; import '../../notifications/notifications.dart';
import '../../settings/settings.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
class RouterShell extends StatelessWidget { class RouterShell extends StatelessWidget {
@ -10,7 +12,13 @@ class RouterShell extends StatelessWidget {
@override @override
Widget build(BuildContext context) => PopControl( Widget build(BuildContext context) => PopControl(
dismissible: false, dismissible: false,
child: NotificationsWidget(child: KeyboardShortcuts(child: _child))); child: AsyncBlocBuilder<PreferencesCubit, Preferences>(
builder: (context, state) => MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler:
TextScaler.linear(state.themePreference.displayScale)),
child: NotificationsWidget(
child: KeyboardShortcuts(child: _child)))));
final Widget _child; final Widget _child;
} }

View file

@ -20,7 +20,7 @@ sealed class LockPreference with _$LockPreference {
factory LockPreference.fromJson(dynamic json) => factory LockPreference.fromJson(dynamic json) =>
_$LockPreferenceFromJson(json as Map<String, dynamic>); _$LockPreferenceFromJson(json as Map<String, dynamic>);
static const LockPreference defaults = LockPreference(); static const defaults = LockPreference();
} }
// Theme supports multiple translations // Theme supports multiple translations
@ -49,5 +49,5 @@ sealed class Preferences with _$Preferences {
factory Preferences.fromJson(dynamic json) => factory Preferences.fromJson(dynamic json) =>
_$PreferencesFromJson(json as Map<String, dynamic>); _$PreferencesFromJson(json as Map<String, dynamic>);
static const Preferences defaults = Preferences(); static const defaults = Preferences();
} }

View file

@ -1,7 +1,6 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -11,62 +10,53 @@ import '../theme/theme.dart';
import '../veilid_processor/veilid_processor.dart'; import '../veilid_processor/veilid_processor.dart';
import 'settings.dart'; import 'settings.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@override
SettingsPageState createState() => SettingsPageState();
}
class SettingsPageState extends State<SettingsPage> {
final _formKey = GlobalKey<FormBuilderState>();
static const String formFieldTheme = 'theme';
static const String formFieldBrightness = 'brightness';
@override
void initState() {
super.initState();
}
@override @override
Widget build(BuildContext context) => Widget build(BuildContext context) =>
AsyncBlocBuilder<PreferencesCubit, Preferences>( AsyncBlocBuilder<PreferencesCubit, Preferences>(
builder: (context, state) => ThemeSwitcher.withTheme( builder: (context, state) => ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => StyledScaffold( builder: (_, switcher, theme) => StyledScaffold(
appBar: DefaultAppBar( appBar: DefaultAppBar(
context: context,
title: Text(translate('settings_page.titlebar')), title: Text(translate('settings_page.titlebar')),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), iconSize: 24.scaled(context),
icon: Icon(Icons.arrow_back),
onPressed: () => GoRouterHelper(context).pop(), onPressed: () => GoRouterHelper(context).pop(),
), ),
actions: <Widget>[ actions: <Widget>[
const SignalStrengthMeterWidget() const SignalStrengthMeterWidget()
.paddingLTRB(16, 0, 16, 0), .paddingLTRB(16, 0, 16, 0),
]), ]),
body: ThemeSwitchingArea( body: ListView(
child: FormBuilder( padding: const EdgeInsets.all(8).scaled(context),
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(8),
children: [ children: [
buildSettingsPageColorPreferences( buildSettingsPageColorPreferences(
context: context, context: context,
switcher: switcher, switcher: switcher,
onChanged: () => setState(() {})) ),
.paddingLTRB(0, 8, 0, 0),
buildSettingsPageBrightnessPreferences( buildSettingsPageBrightnessPreferences(
context: context, context: context,
switcher: switcher, switcher: switcher,
onChanged: () => setState(() {})), ),
buildSettingsPageDisplayScalePreferences(
context: context,
switcher: switcher,
),
buildSettingsPageWallpaperPreferences( buildSettingsPageWallpaperPreferences(
context: context, context: context,
switcher: switcher, switcher: switcher,
onChanged: () => setState(() {})), ),
buildSettingsPageNotificationPreferences( buildSettingsPageNotificationPreferences(
context: context, context: context,
onChanged: () => setState(() {})),
].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(),
), ),
).paddingSymmetric(horizontal: 8, vertical: 8), ]
)))); .map((x) => x.paddingLTRB(0, 0, 0, 8.scaled(context)))
.toList(),
).paddingSymmetric(vertical: 4.scaled(context)),
).paddingSymmetric(
horizontal: 8.scaled(context), vertical: 8.scaled(context)),
));
} }

View file

@ -308,6 +308,13 @@ ThemeData contrastGenerator({
side: elementBorderWidgetStateProperty(), side: elementBorderWidgetStateProperty(),
backgroundColor: elementBackgroundWidgetStateProperty())); backgroundColor: elementBackgroundWidgetStateProperty()));
final sliderTheme = SliderThemeData.fromPrimaryColors(
primaryColor: scheme.primaryScale.borderText,
primaryColorDark: scheme.primaryScale.border,
primaryColorLight: scheme.primaryScale.border,
valueIndicatorTextStyle: textTheme.labelMedium!
.copyWith(color: scheme.primaryScale.borderText));
final themeData = baseThemeData.copyWith( final themeData = baseThemeData.copyWith(
// chipTheme: baseThemeData.chipTheme.copyWith( // chipTheme: baseThemeData.chipTheme.copyWith(
// backgroundColor: scaleScheme.primaryScale.elementBackground, // backgroundColor: scaleScheme.primaryScale.elementBackground,
@ -316,6 +323,7 @@ ThemeData contrastGenerator({
// checkmarkColor: scaleScheme.primaryScale.border, // checkmarkColor: scaleScheme.primaryScale.border,
// side: BorderSide(color: scaleScheme.primaryScale.border)), // side: BorderSide(color: scaleScheme.primaryScale.border)),
elevatedButtonTheme: elevatedButtonTheme, elevatedButtonTheme: elevatedButtonTheme,
sliderTheme: sliderTheme,
textSelectionTheme: TextSelectionThemeData( textSelectionTheme: TextSelectionThemeData(
cursorColor: scheme.primaryScale.appText, cursorColor: scheme.primaryScale.appText,
selectionColor: scheme.primaryScale.appText.withAlpha(0x7F), selectionColor: scheme.primaryScale.appText.withAlpha(0x7F),

View file

@ -132,6 +132,13 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
iconColor: elementColorWidgetStateProperty(), iconColor: elementColorWidgetStateProperty(),
)); ));
final sliderTheme = SliderThemeData.fromPrimaryColors(
primaryColor: scheme.primaryScale.hoverBorder,
primaryColorDark: scheme.primaryScale.border,
primaryColorLight: scheme.primaryScale.border,
valueIndicatorTextStyle: textTheme.labelMedium!
.copyWith(color: scheme.primaryScale.borderText));
final themeData = baseThemeData.copyWith( final themeData = baseThemeData.copyWith(
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith( scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(
thumbColor: WidgetStateProperty.resolveWith((states) { thumbColor: WidgetStateProperty.resolveWith((states) {
@ -183,6 +190,7 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
elevatedButtonTheme: elevatedButtonTheme, elevatedButtonTheme: elevatedButtonTheme,
inputDecorationTheme: inputDecorationTheme:
ScaleInputDecoratorTheme(scheme, config, textTheme), ScaleInputDecoratorTheme(scheme, config, textTheme),
sliderTheme: sliderTheme,
extensions: <ThemeExtension<dynamic>>[scheme, config, this]); extensions: <ThemeExtension<dynamic>>[scheme, config, this]);
return themeData; return themeData;

View file

@ -61,7 +61,7 @@ sealed class ThemePreferences with _$ThemePreferences {
factory ThemePreferences.fromJson(dynamic json) => factory ThemePreferences.fromJson(dynamic json) =>
_$ThemePreferencesFromJson(json as Map<String, dynamic>); _$ThemePreferencesFromJson(json as Map<String, dynamic>);
static const ThemePreferences defaults = ThemePreferences(); static const defaults = ThemePreferences();
} }
extension ThemePreferencesExt on ThemePreferences { extension ThemePreferencesExt on ThemePreferences {

View file

@ -1,74 +0,0 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../theme.dart';
class AvatarWidget extends StatelessWidget {
const AvatarWidget({
required String name,
required double size,
required Color borderColor,
required Color foregroundColor,
required Color backgroundColor,
required ScaleConfig scaleConfig,
required TextStyle textStyle,
super.key,
ImageProvider<Object>? imageProvider,
}) : _name = name,
_size = size,
_borderColor = borderColor,
_foregroundColor = foregroundColor,
_backgroundColor = backgroundColor,
_scaleConfig = scaleConfig,
_textStyle = textStyle,
_imageProvider = imageProvider;
@override
Widget build(BuildContext context) {
final abbrev = _name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join();
late final String shortname;
if (abbrev.length >= 3) {
shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1];
} else {
shortname = abbrev;
}
return Container(
height: _size,
width: _size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: _borderColor,
width: 1 * (_size ~/ 32 + 1),
strokeAlign: BorderSide.strokeAlignOutside)),
child: AvatarImage(
//size: 32,
backgroundImage: _imageProvider,
backgroundColor:
_scaleConfig.useVisualIndicators && !_scaleConfig.preferBorders
? _foregroundColor
: _backgroundColor,
child: Text(
shortname.isNotEmpty ? shortname : '?',
softWrap: false,
style: _textStyle.copyWith(
color: _scaleConfig.useVisualIndicators &&
!_scaleConfig.preferBorders
? _backgroundColor
: _foregroundColor,
),
).fit().paddingAll(_size / 16)));
}
////////////////////////////////////////////////////////////////////////////
final String _name;
final double _size;
final Color _borderColor;
final Color _foregroundColor;
final Color _backgroundColor;
final ScaleConfig _scaleConfig;
final TextStyle _textStyle;
final ImageProvider<Object>? _imageProvider;
}

View file

@ -32,7 +32,7 @@ class _EnterPasswordDialogState extends State<EnterPasswordDialog> {
final passwordController = TextEditingController(); final passwordController = TextEditingController();
final focusNode = FocusNode(); final focusNode = FocusNode();
final formKey = GlobalKey<FormState>(); final formKey = GlobalKey<FormState>();
bool _passwordVisible = false; var _passwordVisible = false;
@override @override
void initState() { void initState() {
@ -47,7 +47,6 @@ class _EnterPasswordDialogState extends State<EnterPasswordDialog> {
} }
@override @override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;

View file

@ -1,14 +1,12 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../settings/settings.dart'; import '../../../settings/settings.dart';
import '../models/models.dart'; import '../../models/models.dart';
import '../views.dart';
const String formFieldBrightness = 'brightness'; List<DropdownMenuItem<BrightnessPreference>> _getBrightnessDropdownItems() {
List<DropdownMenuItem<dynamic>> _getBrightnessDropdownItems() {
const brightnessPrefs = BrightnessPreference.values; const brightnessPrefs = BrightnessPreference.values;
final brightnessNames = { final brightnessNames = {
BrightnessPreference.system: translate('brightness.system'), BrightnessPreference.system: translate('brightness.system'),
@ -22,25 +20,21 @@ List<DropdownMenuItem<dynamic>> _getBrightnessDropdownItems() {
} }
Widget buildSettingsPageBrightnessPreferences( Widget buildSettingsPageBrightnessPreferences(
{required BuildContext context, {required BuildContext context, required ThemeSwitcherState switcher}) {
required void Function() onChanged,
required ThemeSwitcherState switcher}) {
final preferencesRepository = PreferencesRepository.instance; final preferencesRepository = PreferencesRepository.instance;
final themePreferences = preferencesRepository.value.themePreference; final themePreferences = preferencesRepository.value.themePreference;
return FormBuilderDropdown(
name: formFieldBrightness, return StyledDropdown<BrightnessPreference>(
decoration: InputDecoration(
label: Text(translate('settings_page.brightness_mode'))),
items: _getBrightnessDropdownItems(), items: _getBrightnessDropdownItems(),
initialValue: themePreferences.brightnessPreference, value: themePreferences.brightnessPreference,
decoratorLabel: translate('settings_page.brightness_mode'),
onChanged: (value) async { onChanged: (value) async {
final newThemePrefs = themePreferences.copyWith( final newThemePrefs =
brightnessPreference: value as BrightnessPreference); themePreferences.copyWith(brightnessPreference: value);
final newPrefs = preferencesRepository.value final newPrefs = preferencesRepository.value
.copyWith(themePreference: newThemePrefs); .copyWith(themePreference: newThemePrefs);
await preferencesRepository.set(newPrefs); await preferencesRepository.set(newPrefs);
switcher.changeTheme(theme: newThemePrefs.themeData()); switcher.changeTheme(theme: newThemePrefs.themeData());
onChanged();
}); });
} }

View file

@ -1,16 +1,12 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:async_tools/async_tools.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import '../../settings/settings.dart'; import '../../../settings/settings.dart';
import '../models/models.dart'; import '../../models/models.dart';
import '../views.dart';
const String formFieldTheme = 'theme'; List<DropdownMenuItem<ColorPreference>> _getThemeDropdownItems() {
const String _kSwitchTheme = 'switchTheme';
List<DropdownMenuItem<dynamic>> _getThemeDropdownItems() {
const colorPrefs = ColorPreference.values; const colorPrefs = ColorPreference.values;
final colorNames = { final colorNames = {
ColorPreference.scarlet: translate('themes.scarlet'), ColorPreference.scarlet: translate('themes.scarlet'),
@ -34,27 +30,20 @@ List<DropdownMenuItem<dynamic>> _getThemeDropdownItems() {
} }
Widget buildSettingsPageColorPreferences( Widget buildSettingsPageColorPreferences(
{required BuildContext context, {required BuildContext context, required ThemeSwitcherState switcher}) {
required void Function() onChanged,
required ThemeSwitcherState switcher}) {
final preferencesRepository = PreferencesRepository.instance; final preferencesRepository = PreferencesRepository.instance;
final themePreferences = preferencesRepository.value.themePreference; final themePreferences = preferencesRepository.value.themePreference;
return FormBuilderDropdown(
name: formFieldTheme, return StyledDropdown<ColorPreference>(
decoration:
InputDecoration(label: Text(translate('settings_page.color_theme'))),
items: _getThemeDropdownItems(), items: _getThemeDropdownItems(),
initialValue: themePreferences.colorPreference, value: themePreferences.colorPreference,
onChanged: (value) { decoratorLabel: translate('settings_page.color_theme'),
singleFuture(_kSwitchTheme, () async { onChanged: (value) async {
final newThemePrefs = themePreferences.copyWith( final newThemePrefs = themePreferences.copyWith(colorPreference: value);
colorPreference: value as ColorPreference);
final newPrefs = preferencesRepository.value final newPrefs = preferencesRepository.value
.copyWith(themePreference: newThemePrefs); .copyWith(themePreference: newThemePrefs);
await preferencesRepository.set(newPrefs); await preferencesRepository.set(newPrefs);
switcher.changeTheme(theme: newThemePrefs.themeData()); switcher.changeTheme(theme: newThemePrefs.themeData());
onChanged();
});
}); });
} }

View file

@ -0,0 +1,109 @@
import 'dart:math';
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../../settings/settings.dart';
import '../../models/models.dart';
import '../views.dart';
const _scales = [
1 / (1 + 1 / 2),
1 / (1 + 1 / 3),
1 / (1 + 1 / 4),
1,
1 + (1 / 4),
1 + (1 / 2),
1 + (1 / 1),
];
const _scaleNames = [
'-3',
'-2',
'-1',
'0',
'1',
'2',
'3',
];
const _scaleNumMult = <double>[
1 / (1 + 1 / 2),
1 / (1 + 1 / 3),
1 / (1 + 1 / 4),
1,
1 + 1 / 4,
1 + 1 / 2,
1 + 1 / 1,
];
int displayScaleToIndex(double displayScale) {
final idx = _scales.indexWhere((elem) => elem > displayScale);
final currentScaleIdx = idx == -1 ? _scales.length - 1 : max(0, idx - 1);
return currentScaleIdx;
}
double indexToDisplayScale(int scaleIdx) {
final displayScale =
_scales[max(min(scaleIdx, _scales.length - 1), 0)].toDouble();
return displayScale;
}
String indexToDisplayScaleName(int scaleIdx) =>
_scaleNames[max(min(scaleIdx, _scales.length - 1), 0)];
final maxDisplayScaleIndex = _scales.length - 1;
Widget buildSettingsPageDisplayScalePreferences(
{required BuildContext context, required ThemeSwitcherState switcher}) {
final preferencesRepository = PreferencesRepository.instance;
final themePreferences = preferencesRepository.value.themePreference;
final currentScaleIdx = displayScaleToIndex(themePreferences.displayScale);
final currentScaleName = indexToDisplayScaleName(currentScaleIdx);
return StyledSlider(
value: currentScaleIdx.toDouble(),
label: currentScaleName,
decoratorLabel: translate('settings_page.display_scale'),
max: _scales.length - 1.toDouble(),
divisions: _scales.length - 1,
leftWidget: const Icon(Icons.text_decrease),
rightWidget: const Icon(Icons.text_increase),
onChanged: (value) async {
final scaleIdx = value.toInt();
final displayScale = indexToDisplayScale(scaleIdx);
final newThemePrefs =
themePreferences.copyWith(displayScale: displayScale);
final newPrefs = preferencesRepository.value
.copyWith(themePreference: newThemePrefs);
await preferencesRepository.set(newPrefs);
switcher.changeTheme(theme: newThemePrefs.themeData());
});
}
extension DisplayScaledNum on num {
double scaled(BuildContext context) {
final prefs = context.watch<PreferencesCubit>().state.asData?.value ??
PreferencesRepository.instance.value;
final currentScaleIdx =
displayScaleToIndex(prefs.themePreference.displayScale);
return this * _scaleNumMult[currentScaleIdx];
}
}
extension DisplayScaledEdgeInsets on EdgeInsets {
EdgeInsets scaled(BuildContext context) {
final prefs = context.watch<PreferencesCubit>().state.asData?.value ??
PreferencesRepository.instance.value;
final currentScaleIdx =
displayScaleToIndex(prefs.themePreference.displayScale);
return EdgeInsets.fromLTRB(
left * _scaleNumMult[currentScaleIdx],
top * _scaleNumMult[currentScaleIdx],
right * _scaleNumMult[currentScaleIdx],
bottom * _scaleNumMult[currentScaleIdx]);
}
}

View file

@ -0,0 +1,4 @@
export 'brightness_preferences.dart';
export 'color_preferences.dart';
export 'display_scale_preferences.dart';
export 'wallpaper_preferences.dart';

View file

@ -0,0 +1,25 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../../settings/settings.dart';
import '../../models/models.dart';
import '../views.dart';
Widget buildSettingsPageWallpaperPreferences(
{required BuildContext context, required ThemeSwitcherState switcher}) {
final preferencesRepository = PreferencesRepository.instance;
final themePreferences = preferencesRepository.value.themePreference;
return StyledCheckbox(
value: themePreferences.enableWallpaper,
label: translate('settings_page.enable_wallpaper'),
onChanged: (value) async {
final newThemePrefs = themePreferences.copyWith(enableWallpaper: value);
final newPrefs = preferencesRepository.value
.copyWith(themePreference: newThemePrefs);
await preferencesRepository.set(newPrefs);
switcher.changeTheme(theme: newThemePrefs.themeData());
});
}

View file

@ -3,6 +3,10 @@ import 'package:flutter/material.dart';
final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android; final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
final isiOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; final isiOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
final isMac = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS;
final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
final isLinux = !kIsWeb && defaultTargetPlatform == TargetPlatform.linux;
final isMobile = !kIsWeb && final isMobile = !kIsWeb &&
(defaultTargetPlatform == TargetPlatform.iOS || (defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android); defaultTargetPlatform == TargetPlatform.android);

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:rflutter_alert/rflutter_alert.dart'; import 'package:rflutter_alert/rflutter_alert.dart';
import '../theme.dart'; import '../../theme.dart';
AlertStyle _alertStyle(BuildContext context) { AlertStyle _alertStyle(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -186,6 +186,7 @@ Future<void> showAlertWidgetModal(
child: Text( child: Text(
translate('button.ok'), translate('button.ok'),
style: _buttonTextStyle(context), style: _buttonTextStyle(context),
softWrap: true,
), ),
) )
], ],

View file

@ -0,0 +1,77 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../../theme.dart';
class StyledAvatar extends StatelessWidget {
const StyledAvatar({
required String name,
required double size,
bool enabled = true,
super.key,
ImageProvider<Object>? imageProvider,
}) : _name = name,
_size = size,
_imageProvider = imageProvider,
_enabled = enabled;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scaleTheme = Theme.of(context).extension<ScaleTheme>()!;
final borderColor = scaleTheme.config.useVisualIndicators
? scaleTheme.scheme.primaryScale.primaryText
: scaleTheme.scheme.primaryScale.subtleBorder;
final foregroundColor = !_enabled
? scaleTheme.scheme.grayScale.primaryText
: scaleTheme.scheme.primaryScale.calloutText;
final backgroundColor = !_enabled
? scaleTheme.scheme.grayScale.primary
: scaleTheme.scheme.primaryScale.calloutBackground;
final scaleConfig = scaleTheme.config;
final textStyle = theme.textTheme.titleLarge!.copyWith(fontSize: _size / 2);
final abbrev = _name.split(' ').map((s) => s.isEmpty ? '' : s[0]).join();
late final String shortname;
if (abbrev.length >= 3) {
shortname = abbrev[0] + abbrev[1] + abbrev[abbrev.length - 1];
} else {
shortname = abbrev;
}
return Container(
height: _size,
width: _size,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: !scaleConfig.useVisualIndicators
? null
: Border.all(
color: borderColor,
width: 1 * (_size ~/ 16 + 1),
strokeAlign: BorderSide.strokeAlignOutside)),
child: AvatarImage(
backgroundImage: _imageProvider,
backgroundColor: scaleConfig.useVisualIndicators
? foregroundColor
: backgroundColor,
child: Text(
shortname.isNotEmpty ? shortname : '?',
softWrap: false,
textScaler: MediaQuery.of(context).textScaler,
style: textStyle.copyWith(
color: scaleConfig.useVisualIndicators
? backgroundColor
: foregroundColor,
),
).paddingAll(4.scaled(context)).fit(fit: BoxFit.scaleDown)));
}
////////////////////////////////////////////////////////////////////////////
final String _name;
final double _size;
final ImageProvider<Object>? _imageProvider;
final bool _enabled;
}

View file

@ -1,10 +1,10 @@
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../../theme.dart';
class OptionBox extends StatelessWidget { class StyledButtonBox extends StatelessWidget {
const OptionBox( const StyledButtonBox(
{required String instructions, {required String instructions,
required IconData buttonIcon, required IconData buttonIcon,
required String buttonText, required String buttonText,
@ -41,12 +41,15 @@ class OptionBox extends StatelessWidget {
onPressed: _onClick, onPressed: _onClick,
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(_buttonIcon, Icon(_buttonIcon,
size: 24, color: scale.primaryScale.appText) size: 24.scaled(context),
.paddingLTRB(0, 8, 8, 8), color: scale.primaryScale.appText)
.paddingLTRB(0, 8.scaled(context),
8.scaled(context), 8.scaled(context)),
Text(textAlign: TextAlign.center, _buttonText) Text(textAlign: TextAlign.center, _buttonText)
])).paddingLTRB(0, 12, 0, 0).toCenter() ])).paddingLTRB(0, 12.scaled(context), 0, 0).toCenter()
]).paddingAll(12)) ]).paddingAll(12.scaled(context)))
.paddingLTRB(24, 0, 24, 12); .paddingLTRB(
24.scaled(context), 0, 24.scaled(context), 12.scaled(context));
} }
final String _instructions; final String _instructions;

View file

@ -0,0 +1,63 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions_flutter.dart';
import 'package:flutter/material.dart';
import '../views.dart';
const _kStyledCheckboxChanged = 'kStyledCheckboxChanged';
class StyledCheckbox extends StatelessWidget {
const StyledCheckbox(
{required bool value,
required String label,
String? decoratorLabel,
Future<void> Function(bool)? onChanged,
super.key})
: _value = value,
_onChanged = onChanged,
_label = label,
_decoratorLabel = decoratorLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
var textStyle = textTheme.labelLarge!;
if (_onChanged == null) {
textStyle = textStyle.copyWith(color: textStyle.color!.withAlpha(127));
}
Widget ctrl = Row(children: [
Transform.scale(
scale: 1.scaled(context),
child: Checkbox(
value: _value,
onChanged: _onChanged == null
? null
: (value) {
if (value == null) {
return;
}
singleFuture((this, _kStyledCheckboxChanged), () async {
await _onChanged(value);
});
})),
Text(_label, style: textStyle).paddingAll(4.scaled(context)),
]);
if (_decoratorLabel != null) {
ctrl = ctrl
.paddingLTRB(4.scaled(context), 4.scaled(context), 4.scaled(context),
4.scaled(context))
.decoratorLabel(context, _decoratorLabel);
}
return ctrl;
}
final String _label;
final String? _decoratorLabel;
final Future<void> Function(bool)? _onChanged;
final bool _value;
}

View file

@ -2,7 +2,7 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../../theme.dart';
class StyledDialog extends StatelessWidget { class StyledDialog extends StatelessWidget {
const StyledDialog({required this.title, required this.child, super.key}); const StyledDialog({required this.title, required this.child, super.key});

View file

@ -0,0 +1,59 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions_flutter.dart';
import 'package:flutter/material.dart';
import '../../models/models.dart';
import '../views.dart';
const _kStyledDropdownChanged = 'kStyledDropdownChanged';
class StyledDropdown<T> extends StatelessWidget {
const StyledDropdown(
{required List<DropdownMenuItem<T>> items,
required T value,
String? decoratorLabel,
Future<void> Function(T)? onChanged,
super.key})
: _items = items,
_onChanged = onChanged,
_decoratorLabel = decoratorLabel,
_value = value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scheme = theme.extension<ScaleScheme>()!;
Widget ctrl = DropdownButton<T>(
isExpanded: true,
padding: const EdgeInsets.fromLTRB(4, 0, 4, 0).scaled(context),
focusColor: theme.focusColor,
dropdownColor: scheme.primaryScale.elementBackground,
iconEnabledColor: scheme.primaryScale.appText,
iconDisabledColor: scheme.primaryScale.appText.withAlpha(127),
items: _items,
value: _value,
style: theme.textTheme.labelLarge,
onChanged: _onChanged == null
? null
: (value) {
if (value == null) {
return;
}
singleFuture((this, _kStyledDropdownChanged), () async {
await _onChanged(value);
});
});
if (_decoratorLabel != null) {
ctrl = ctrl
.paddingLTRB(0, 4.scaled(context), 0, 4.scaled(context))
.decoratorLabel(context, _decoratorLabel);
}
return ctrl;
}
final List<DropdownMenuItem<T>> _items;
final String? _decoratorLabel;
final Future<void> Function(T)? _onChanged;
final T _value;
}

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../theme.dart'; import '../../theme.dart';
class StyledScaffold extends StatelessWidget { class StyledScaffold extends StatelessWidget {
const StyledScaffold({required this.appBar, required this.body, super.key}); const StyledScaffold({required this.appBar, required this.body, super.key});

View file

@ -2,10 +2,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import '../theme.dart'; import '../../theme.dart';
class SliderTileAction { class SlideTileAction {
const SliderTileAction({ const SlideTileAction({
required this.actionScale, required this.actionScale,
required this.onPressed, required this.onPressed,
this.key, this.key,
@ -20,8 +20,8 @@ class SliderTileAction {
final SlidableActionCallback? onPressed; final SlidableActionCallback? onPressed;
} }
class SliderTile extends StatelessWidget { class StyledSlideTile extends StatelessWidget {
const SliderTile( const StyledSlideTile(
{required this.disabled, {required this.disabled,
required this.selected, required this.selected,
required this.tileScale, required this.tileScale,
@ -38,8 +38,8 @@ class SliderTile extends StatelessWidget {
final bool disabled; final bool disabled;
final bool selected; final bool selected;
final ScaleKind tileScale; final ScaleKind tileScale;
final List<SliderTileAction> endActions; final List<SlideTileAction> endActions;
final List<SliderTileAction> startActions; final List<SlideTileAction> startActions;
final GestureTapCallback? onTap; final GestureTapCallback? onTap;
final GestureTapCallback? onDoubleTap; final GestureTapCallback? onDoubleTap;
final Widget? leading; final Widget? leading;
@ -54,8 +54,8 @@ class SliderTile extends StatelessWidget {
..add(DiagnosticsProperty<bool>('disabled', disabled)) ..add(DiagnosticsProperty<bool>('disabled', disabled))
..add(DiagnosticsProperty<bool>('selected', selected)) ..add(DiagnosticsProperty<bool>('selected', selected))
..add(DiagnosticsProperty<ScaleKind>('tileScale', tileScale)) ..add(DiagnosticsProperty<ScaleKind>('tileScale', tileScale))
..add(IterableProperty<SliderTileAction>('endActions', endActions)) ..add(IterableProperty<SlideTileAction>('endActions', endActions))
..add(IterableProperty<SliderTileAction>('startActions', startActions)) ..add(IterableProperty<SlideTileAction>('startActions', startActions))
..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap)) ..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap))
..add(DiagnosticsProperty<Widget?>('leading', leading)) ..add(DiagnosticsProperty<Widget?>('leading', leading))
..add(StringProperty('title', title)) ..add(StringProperty('title', title))
@ -66,7 +66,6 @@ class SliderTile extends StatelessWidget {
} }
@override @override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaleTheme = theme.extension<ScaleTheme>()!; final scaleTheme = theme.extension<ScaleTheme>()!;
@ -96,7 +95,8 @@ class SliderTile extends StatelessWidget {
foregroundColor: scaleActionTheme.textColor, foregroundColor: scaleActionTheme.textColor,
icon: subtitle.isEmpty ? a.icon : null, icon: subtitle.isEmpty ? a.icon : null,
label: a.label, label: a.label,
padding: const EdgeInsets.all(2)); padding: const EdgeInsets.all(2).scaled(context),
);
}).toList()), }).toList()),
startActionPane: startActions.isEmpty startActionPane: startActions.isEmpty
? null ? null
@ -114,12 +114,13 @@ class SliderTile extends StatelessWidget {
foregroundColor: scaleActionTheme.textColor, foregroundColor: scaleActionTheme.textColor,
icon: subtitle.isEmpty ? a.icon : null, icon: subtitle.isEmpty ? a.icon : null,
label: a.label, label: a.label,
padding: const EdgeInsets.all(2)); padding: const EdgeInsets.all(2).scaled(context),
);
}).toList()), }).toList()),
child: Padding( child: Padding(
padding: scaleTheme.config.useVisualIndicators padding: scaleTheme.config.useVisualIndicators
? EdgeInsets.zero ? EdgeInsets.zero
: const EdgeInsets.fromLTRB(0, 2, 0, 2), : const EdgeInsets.fromLTRB(0, 2, 0, 2).scaled(context),
child: GestureDetector( child: GestureDetector(
onDoubleTap: onDoubleTap, onDoubleTap: onDoubleTap,
child: ListTile( child: ListTile(
@ -131,7 +132,8 @@ class SliderTile extends StatelessWidget {
softWrap: false, softWrap: false,
), ),
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
minTileHeight: 52, contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)
.scaled(context),
iconColor: scaleTileTheme.textColor, iconColor: scaleTileTheme.textColor,
textColor: scaleTileTheme.textColor, textColor: scaleTileTheme.textColor,
leading: leading:

View file

@ -0,0 +1,79 @@
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions_flutter.dart';
import 'package:flutter/material.dart';
import '../../models/models.dart';
import '../views.dart';
const _kStyledSliderChanged = 'kStyledSliderChanged';
class StyledSlider extends StatelessWidget {
const StyledSlider(
{required double value,
String? label,
String? decoratorLabel,
Future<void> Function(double)? onChanged,
Widget? leftWidget,
Widget? rightWidget,
double min = 0,
double max = 1,
int? divisions,
super.key})
: _value = value,
_onChanged = onChanged,
_leftWidget = leftWidget,
_rightWidget = rightWidget,
_min = min,
_max = max,
_divisions = divisions,
_label = label,
_decoratorLabel = decoratorLabel;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleTheme>()!;
Widget ctrl = Row(children: [
if (_leftWidget != null) _leftWidget,
Slider(
activeColor: scale.scheme.primaryScale.border,
inactiveColor: scale.scheme.primaryScale.subtleBorder,
secondaryActiveColor: scale.scheme.secondaryScale.border,
value: _value,
min: _min,
max: _max,
divisions: _divisions,
label: _label,
thumbColor: scale.scheme.primaryScale.appText,
overlayColor:
WidgetStateColor.resolveWith((ws) => theme.focusColor),
onChanged: _onChanged == null
? null
: (value) {
singleFuture((this, _kStyledSliderChanged), () async {
await _onChanged(value);
});
})
.expanded(),
if (_rightWidget != null) _rightWidget,
]);
if (_decoratorLabel != null) {
ctrl = ctrl
.paddingLTRB(4.scaled(context), 4.scaled(context), 4.scaled(context),
4.scaled(context))
.decoratorLabel(context, _decoratorLabel);
}
return ctrl;
}
final String? _label;
final String? _decoratorLabel;
final Future<void> Function(double)? _onChanged;
final double _value;
final Widget? _leftWidget;
final Widget? _rightWidget;
final double _min;
final double _max;
final int? _divisions;
}

View file

@ -0,0 +1,8 @@
export 'styled_alert.dart';
export 'styled_avatar.dart';
export 'styled_checkbox.dart';
export 'styled_dialog.dart';
export 'styled_dropdown.dart';
export 'styled_scaffold.dart';
export 'styled_slide_tile.dart';
export 'styled_slider.dart';

View file

@ -1,16 +1,10 @@
export 'avatar_widget.dart';
export 'brightness_preferences.dart';
export 'color_preferences.dart';
export 'enter_password.dart'; export 'enter_password.dart';
export 'enter_pin.dart'; export 'enter_pin.dart';
export 'option_box.dart';
export 'pop_control.dart'; export 'pop_control.dart';
export 'preferences/preferences.dart';
export 'recovery_key_widget.dart'; export 'recovery_key_widget.dart';
export 'responsive.dart'; export 'responsive.dart';
export 'scanner_error_widget.dart'; export 'scanner_error_widget.dart';
export 'slider_tile.dart'; export 'styled_widgets/styled_button_box.dart';
export 'styled_alert.dart'; export 'styled_widgets/styled_widgets.dart';
export 'styled_dialog.dart';
export 'styled_scaffold.dart';
export 'wallpaper_preferences.dart';
export 'widget_helpers.dart'; export 'widget_helpers.dart';

View file

@ -1,37 +0,0 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import '../../settings/settings.dart';
import '../models/models.dart';
const String formFieldEnableWallpaper = 'enable_wallpaper';
Widget buildSettingsPageWallpaperPreferences(
{required BuildContext context,
required void Function() onChanged,
required ThemeSwitcherState switcher}) {
final preferencesRepository = PreferencesRepository.instance;
final themePreferences = preferencesRepository.value.themePreference;
final theme = Theme.of(context);
final textTheme = theme.textTheme;
return FormBuilderCheckbox(
name: formFieldEnableWallpaper,
title: Text(translate('settings_page.enable_wallpaper'),
style: textTheme.labelMedium),
initialValue: themePreferences.enableWallpaper,
onChanged: (value) async {
if (value != null) {
final newThemePrefs =
themePreferences.copyWith(enableWallpaper: value);
final newPrefs = preferencesRepository.value
.copyWith(themePreference: newThemePrefs);
await preferencesRepository.set(newPrefs);
switcher.changeTheme(theme: newThemePrefs.themeData());
onChanged();
}
});
}

View file

@ -222,13 +222,16 @@ class _DeveloperPageState extends State<DeveloperPage> {
return Scaffold( return Scaffold(
backgroundColor: scale.primaryScale.border, backgroundColor: scale.primaryScale.border,
appBar: DefaultAppBar( appBar: DefaultAppBar(
context: context,
title: Text(translate('developer.title')), title: Text(translate('developer.title')),
leading: IconButton( leading: IconButton(
iconSize: 24.scaled(context),
icon: Icon(Icons.arrow_back, color: scale.primaryScale.borderText), icon: Icon(Icons.arrow_back, color: scale.primaryScale.borderText),
onPressed: () => GoRouterHelper(context).pop(), onPressed: () => GoRouterHelper(context).pop(),
), ),
actions: [ actions: [
IconButton( IconButton(
iconSize: 24.scaled(context),
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),
color: scale.primaryScale.borderText, color: scale.primaryScale.borderText,
disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), disabledColor: scale.primaryScale.borderText.withAlpha(0x3F),
@ -238,6 +241,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
await copySelection(context); await copySelection(context);
}), }),
IconButton( IconButton(
iconSize: 24.scaled(context),
icon: const Icon(Icons.copy_all), icon: const Icon(Icons.copy_all),
color: scale.primaryScale.borderText, color: scale.primaryScale.borderText,
disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), disabledColor: scale.primaryScale.borderText.withAlpha(0x3F),
@ -245,6 +249,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
await copyAll(context); await copyAll(context);
}), }),
IconButton( IconButton(
iconSize: 24.scaled(context),
icon: const Icon(Icons.clear_all), icon: const Icon(Icons.clear_all),
color: scale.primaryScale.borderText, color: scale.primaryScale.borderText,
disabledColor: scale.primaryScale.borderText.withAlpha(0x3F), disabledColor: scale.primaryScale.borderText.withAlpha(0x3F),
@ -259,7 +264,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
} }
}), }),
SizedBox.fromSize( SizedBox.fromSize(
size: const Size(140, 48), size: Size(140.scaled(context), 48),
child: CustomDropdown<LogLevelDropdownItem>( child: CustomDropdown<LogLevelDropdownItem>(
items: _logLevelDropdownItems, items: _logLevelDropdownItems,
initialItem: _logLevelDropdownItems initialItem: _logLevelDropdownItems
@ -300,6 +305,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
Image.asset('assets/images/ellet.png'), Image.asset('assets/images/ellet.png'),
TerminalView(globalDebugTerminal, TerminalView(globalDebugTerminal,
textStyle: kDefaultTerminalStyle, textStyle: kDefaultTerminalStyle,
textScaler: TextScaler.noScaling,
controller: _terminalController, controller: _terminalController,
keyboardType: TextInputType.none, keyboardType: TextInputType.none,
backgroundOpacity: _showEllet ? 0.75 : 1.0, backgroundOpacity: _showEllet ? 0.75 : 1.0,

View file

@ -19,7 +19,7 @@ class SignalStrengthMeterWidget extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
const iconSize = 16.0; final iconSize = 16.0.scaled(context);
return BlocBuilder<ConnectionStateCubit, return BlocBuilder<ConnectionStateCubit,
AsyncValue<ProcessorConnectionState>>(builder: (context, state) { AsyncValue<ProcessorConnectionState>>(builder: (context, state) {