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
- Simplify SingleContactMessagesCubit and MessageReconciliation
- Update flutter_chat_ui to 2.0.0
- Accessibility improvements
- Text scaling
- Keyboard shortcuts Ctrl + / Ctrl - to change font size
## v0.4.7 ##
- *Community Contributions*

View file

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

View file

@ -6,54 +6,9 @@ PODS:
- Flutter (1.0.0)
- flutter_native_splash (2.4.3):
- Flutter
- GoogleDataTransport (10.1.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):
- mobile_scanner (7.0.0):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- FlutterMacOS
- package_info_plus (0.4.5):
- Flutter
- pasteboard (0.0.1):
@ -63,7 +18,6 @@ PODS:
- FlutterMacOS
- printing (1.0.0):
- Flutter
- PromisesObjC (2.4.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@ -84,7 +38,7 @@ DEPENDENCIES:
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- Flutter (from `Flutter`)
- 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`)
- pasteboard (from `.symlinks/plugins/pasteboard/ios`)
- 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`)
- veilid (from `.symlinks/plugins/veilid/ios`)
SPEC REPOS:
trunk:
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- PromisesObjC
EXTERNAL SOURCES:
camera_avfoundation:
:path: ".symlinks/plugins/camera_avfoundation/ios"
@ -120,7 +60,7 @@ EXTERNAL SOURCES:
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
:path: ".symlinks/plugins/mobile_scanner/darwin"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
pasteboard:
@ -143,26 +83,15 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/veilid/ios"
SPEC CHECKSUMS:
camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
printing: 54ff03f28fe9ba3aa93358afb80a8595a071dd07
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0

View file

@ -139,7 +139,6 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
02C44F9283ADDE9FAAA73512 /* [CP] Embed Pods Frameworks */,
61BE8A90522682C17620991D /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -232,23 +231,6 @@
shellPath = /bin/sh;
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 */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@ class VcTextMessageWidget extends StatelessWidget {
const VcTextMessageWidget({
required this.message,
required this.index,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
this.padding,
this.borderRadius,
this.onlyEmojiFontSize,
this.sentBackgroundColor,
@ -72,10 +72,6 @@ class VcTextMessageWidget extends StatelessWidget {
Widget build(BuildContext context) {
final theme = Theme.of(context);
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 chatTheme = scaleChatTheme.chatTheme;
@ -243,15 +239,16 @@ class TimeAndStatus extends StatelessWidget {
if (showStatus && status != null)
if (status == MessageStatus.sending)
SizedBox(
width: 6,
height: 6,
width: 6.scaled(context),
height: 6.scaled(context),
child: CircularProgressIndicator(
color: textStyle?.color,
strokeWidth: 2,
),
)
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 textTheme = theme.textTheme;
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
// (created by ChatComponentWidget.builder())
// (created by ChatComponentWidget.singleContact())
final chatComponentCubit = context.watch<ChatComponentCubit>();
final chatComponentState = chatComponentCubit.state;
@ -273,14 +265,19 @@ class _ChatComponentWidgetState extends State<ChatComponentWidget> {
// Text message builder
textMessageBuilder: (context, message, index) =>
VcTextMessageWidget(
message: message,
index: index,
// showTime: true,
// showStatus: true,
),
message: message,
index: index,
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 16)
.scaled(context)
// showTime: true,
// showStatus: true,
),
// Composer builder
composerBuilder: (ctx) => VcComposerWidget(
autofocus: true,
padding: const EdgeInsets.all(4).scaled(context),
gap: 8.scaled(context),
focusNode: _focusNode,
textInputAction: isAnyMobile
? TextInputAction.newline

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,6 +32,14 @@ class DeveloperPageIntent extends Intent {
const DeveloperPageIntent();
}
class DisplayScaleUpIntent extends Intent {
const DisplayScaleUpIntent();
}
class DisplayScaleDownIntent extends Intent {
const DisplayScaleDownIntent();
}
class KeyboardShortcuts extends StatelessWidget {
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 {
final prefs = PreferencesRepository.instance.value;
@ -79,7 +87,7 @@ class KeyboardShortcuts extends StatelessWidget {
});
}
void changeColor(BuildContext context) {
void _changeColor(BuildContext context) {
singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value;
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) {
singleFuture(this, () async {
if (ProcessorRepository.instance.processorConnectionState.isAttached) {
@ -125,44 +181,88 @@ class KeyboardShortcuts extends StatelessWidget {
@override
Widget build(BuildContext context) => ThemeSwitcher(
builder: (context) => Shortcuts(
shortcuts: const <ShortcutActivator, Intent>{
SingleActivator(
shortcuts: <ShortcutActivator, Intent>{
////////////////////////// Reload Theme
const SingleActivator(
LogicalKeyboardKey.keyR,
control: true,
alt: true,
): ReloadThemeIntent(),
SingleActivator(
): const ReloadThemeIntent(),
////////////////////////// Switch Brightness
const SingleActivator(
LogicalKeyboardKey.keyB,
control: true,
alt: true,
): ChangeBrightnessIntent(),
SingleActivator(
): const ChangeBrightnessIntent(),
////////////////////////// Change Color
const SingleActivator(
LogicalKeyboardKey.keyC,
control: true,
alt: true,
): ChangeColorIntent(),
SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
alt: true,
): AttachDetachIntent(),
SingleActivator(
): const ChangeColorIntent(),
////////////////////////// Attach/Detach
if (kIsDebugMode)
const SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
alt: true,
): const AttachDetachIntent(),
////////////////////////// Show Developer Page
const SingleActivator(
LogicalKeyboardKey.keyD,
control: 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>>{
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
onInvoke: (intent) => reloadTheme(context)),
ChangeBrightnessIntent: CallbackAction<ChangeBrightnessIntent>(
onInvoke: (intent) => changeBrightness(context)),
onInvoke: (intent) => _changeBrightness(context)),
ChangeColorIntent: CallbackAction<ChangeColorIntent>(
onInvoke: (intent) => changeColor(context)),
onInvoke: (intent) => _changeColor(context)),
AttachDetachIntent: CallbackAction<AttachDetachIntent>(
onInvoke: (intent) => _attachDetach(context)),
DeveloperPageIntent: CallbackAction<DeveloperPageIntent>(
onInvoke: (intent) => _developerPage(context)),
DisplayScaleUpIntent: CallbackAction<DisplayScaleUpIntent>(
onInvoke: (intent) => _displayScaleUp(context)),
DisplayScaleDownIntent: CallbackAction<DisplayScaleDownIntent>(
onInvoke: (intent) => _displayScaleDown(context)),
}, 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_svg/flutter_svg.dart';
import '../theme/theme.dart';
class DefaultAppBar extends AppBar {
DefaultAppBar(
{super.title,
{required BuildContext context,
super.title,
super.flexibleSpace,
super.key,
Widget? leading,
super.actions})
: super(
toolbarHeight: 40.scaled(context),
leadingWidth: 40.scaled(context),
leading: leading ??
Container(
margin: const EdgeInsets.all(4),
margin: const EdgeInsets.all(4).scaled(context),
decoration: BoxDecoration(
color: Colors.black.withAlpha(32),
shape: BoxShape.circle),
child:
SvgPicture.asset('assets/images/vlogo.svg', height: 24)
.paddingAll(4)));
child: SvgPicture.asset('assets/images/vlogo.svg',
width: 24.scaled(context),
height: 24.scaled(context))
.paddingAll(4.scaled(context))));
}

View file

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

View file

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

View file

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

View file

@ -71,8 +71,9 @@ class HomeScreenState extends State<HomeScreen>
context: context,
title: translate('splash.beta_title'),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const Icon(Icons.warning, size: 64),
Icon(Icons.warning, size: 64.scaled(context)),
RichText(
textScaler: MediaQuery.of(context).textScaler,
textAlign: TextAlign.center,
text: TextSpan(
children: <TextSpan>[
@ -206,34 +207,36 @@ class HomeScreenState extends State<HomeScreen>
.indexWhere((x) => x.superIdentity.recordKey == activeLocalAccount);
final canClose = activeIndex != -1;
final drawer = ZoomDrawer(
controller: _zoomDrawerController,
menuScreen: Builder(builder: (context) {
final zoomDrawer = ZoomDrawer.of(context);
zoomDrawer!.stateNotifier.addListener(() {
if (zoomDrawer.isOpen()) {
FocusManager.instance.primaryFocus?.unfocus();
}
});
return const DrawerMenu();
}),
mainScreen: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildAccountPageView)),
borderRadius: 0,
angle: 0,
openCurve: Curves.fastEaseInToSlowEaseOut,
closeCurve: Curves.fastEaseInToSlowEaseOut,
menuScreenTapClose: canClose,
mainScreenTapClose: canClose,
disableDragGesture: !canClose,
mainScreenScale: .25,
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: KeyboardAvoider(
curve: Curves.ease,
child: ZoomDrawer(
controller: _zoomDrawerController,
menuScreen: Builder(builder: (context) {
final zoomDrawer = ZoomDrawer.of(context);
zoomDrawer!.stateNotifier.addListener(() {
if (zoomDrawer.isOpen()) {
FocusManager.instance.primaryFocus?.unfocus();
}
});
return const DrawerMenu();
}),
mainScreen: Provider<ZoomDrawerController>.value(
value: _zoomDrawerController,
child: Builder(builder: _buildAccountPageView)),
borderRadius: 0,
angle: 0,
openCurve: Curves.fastEaseInToSlowEaseOut,
closeCurve: Curves.fastEaseInToSlowEaseOut,
menuScreenTapClose: canClose,
mainScreenTapClose: canClose,
disableDragGesture: !canClose,
mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
)));
style: theme.textTheme.bodySmall!, child: drawerWithAvoider);
}
////////////////////////////////////////////////////////////////////////////

View file

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

View file

@ -20,7 +20,7 @@ sealed class LockPreference with _$LockPreference {
factory LockPreference.fromJson(dynamic json) =>
_$LockPreferenceFromJson(json as Map<String, dynamic>);
static const LockPreference defaults = LockPreference();
static const defaults = LockPreference();
}
// Theme supports multiple translations
@ -49,5 +49,5 @@ sealed class Preferences with _$Preferences {
factory Preferences.fromJson(dynamic json) =>
_$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:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:go_router/go_router.dart';
@ -11,62 +10,53 @@ import '../theme/theme.dart';
import '../veilid_processor/veilid_processor.dart';
import 'settings.dart';
class SettingsPage extends StatefulWidget {
class SettingsPage extends StatelessWidget {
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
Widget build(BuildContext context) =>
AsyncBlocBuilder<PreferencesCubit, Preferences>(
builder: (context, state) => ThemeSwitcher.withTheme(
builder: (_, switcher, theme) => StyledScaffold(
builder: (_, switcher, theme) => StyledScaffold(
appBar: DefaultAppBar(
context: context,
title: Text(translate('settings_page.titlebar')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
iconSize: 24.scaled(context),
icon: Icon(Icons.arrow_back),
onPressed: () => GoRouterHelper(context).pop(),
),
actions: <Widget>[
const SignalStrengthMeterWidget()
.paddingLTRB(16, 0, 16, 0),
]),
body: ThemeSwitchingArea(
child: FormBuilder(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(8),
children: [
buildSettingsPageColorPreferences(
context: context,
switcher: switcher,
onChanged: () => setState(() {}))
.paddingLTRB(0, 8, 0, 0),
buildSettingsPageBrightnessPreferences(
context: context,
switcher: switcher,
onChanged: () => setState(() {})),
buildSettingsPageWallpaperPreferences(
context: context,
switcher: switcher,
onChanged: () => setState(() {})),
buildSettingsPageNotificationPreferences(
context: context,
onChanged: () => setState(() {})),
].map((x) => x.paddingLTRB(0, 0, 0, 8)).toList(),
body: ListView(
padding: const EdgeInsets.all(8).scaled(context),
children: [
buildSettingsPageColorPreferences(
context: context,
switcher: switcher,
),
).paddingSymmetric(horizontal: 8, vertical: 8),
))));
buildSettingsPageBrightnessPreferences(
context: context,
switcher: switcher,
),
buildSettingsPageDisplayScalePreferences(
context: context,
switcher: switcher,
),
buildSettingsPageWallpaperPreferences(
context: context,
switcher: switcher,
),
buildSettingsPageNotificationPreferences(
context: context,
),
]
.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(),
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(
// chipTheme: baseThemeData.chipTheme.copyWith(
// backgroundColor: scaleScheme.primaryScale.elementBackground,
@ -316,6 +323,7 @@ ThemeData contrastGenerator({
// checkmarkColor: scaleScheme.primaryScale.border,
// side: BorderSide(color: scaleScheme.primaryScale.border)),
elevatedButtonTheme: elevatedButtonTheme,
sliderTheme: sliderTheme,
textSelectionTheme: TextSelectionThemeData(
cursorColor: scheme.primaryScale.appText,
selectionColor: scheme.primaryScale.appText.withAlpha(0x7F),

View file

@ -132,6 +132,13 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
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(
scrollbarTheme: baseThemeData.scrollbarTheme.copyWith(
thumbColor: WidgetStateProperty.resolveWith((states) {
@ -183,6 +190,7 @@ class ScaleTheme extends ThemeExtension<ScaleTheme> {
elevatedButtonTheme: elevatedButtonTheme,
inputDecorationTheme:
ScaleInputDecoratorTheme(scheme, config, textTheme),
sliderTheme: sliderTheme,
extensions: <ThemeExtension<dynamic>>[scheme, config, this]);
return themeData;

View file

@ -61,7 +61,7 @@ sealed class ThemePreferences with _$ThemePreferences {
factory ThemePreferences.fromJson(dynamic json) =>
_$ThemePreferencesFromJson(json as Map<String, dynamic>);
static const ThemePreferences defaults = ThemePreferences();
static const defaults = 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 focusNode = FocusNode();
final formKey = GlobalKey<FormState>();
bool _passwordVisible = false;
var _passwordVisible = false;
@override
void initState() {
@ -47,7 +47,6 @@ class _EnterPasswordDialogState extends State<EnterPasswordDialog> {
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;

View file

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

View file

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

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 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 &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android);

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:rflutter_alert/rflutter_alert.dart';
import '../theme.dart';
import '../../theme.dart';
AlertStyle _alertStyle(BuildContext context) {
final theme = Theme.of(context);
@ -186,6 +186,7 @@ Future<void> showAlertWidgetModal(
child: Text(
translate('button.ok'),
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:flutter/material.dart';
import '../theme.dart';
import '../../theme.dart';
class OptionBox extends StatelessWidget {
const OptionBox(
class StyledButtonBox extends StatelessWidget {
const StyledButtonBox(
{required String instructions,
required IconData buttonIcon,
required String buttonText,
@ -41,12 +41,15 @@ class OptionBox extends StatelessWidget {
onPressed: _onClick,
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(_buttonIcon,
size: 24, color: scale.primaryScale.appText)
.paddingLTRB(0, 8, 8, 8),
size: 24.scaled(context),
color: scale.primaryScale.appText)
.paddingLTRB(0, 8.scaled(context),
8.scaled(context), 8.scaled(context)),
Text(textAlign: TextAlign.center, _buttonText)
])).paddingLTRB(0, 12, 0, 0).toCenter()
]).paddingAll(12))
.paddingLTRB(24, 0, 24, 12);
])).paddingLTRB(0, 12.scaled(context), 0, 0).toCenter()
]).paddingAll(12.scaled(context)))
.paddingLTRB(
24.scaled(context), 0, 24.scaled(context), 12.scaled(context));
}
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/material.dart';
import '../theme.dart';
import '../../theme.dart';
class StyledDialog extends StatelessWidget {
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 '../theme.dart';
import '../../theme.dart';
class StyledScaffold extends StatelessWidget {
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_slidable/flutter_slidable.dart';
import '../theme.dart';
import '../../theme.dart';
class SliderTileAction {
const SliderTileAction({
class SlideTileAction {
const SlideTileAction({
required this.actionScale,
required this.onPressed,
this.key,
@ -20,8 +20,8 @@ class SliderTileAction {
final SlidableActionCallback? onPressed;
}
class SliderTile extends StatelessWidget {
const SliderTile(
class StyledSlideTile extends StatelessWidget {
const StyledSlideTile(
{required this.disabled,
required this.selected,
required this.tileScale,
@ -38,8 +38,8 @@ class SliderTile extends StatelessWidget {
final bool disabled;
final bool selected;
final ScaleKind tileScale;
final List<SliderTileAction> endActions;
final List<SliderTileAction> startActions;
final List<SlideTileAction> endActions;
final List<SlideTileAction> startActions;
final GestureTapCallback? onTap;
final GestureTapCallback? onDoubleTap;
final Widget? leading;
@ -54,8 +54,8 @@ class SliderTile extends StatelessWidget {
..add(DiagnosticsProperty<bool>('disabled', disabled))
..add(DiagnosticsProperty<bool>('selected', selected))
..add(DiagnosticsProperty<ScaleKind>('tileScale', tileScale))
..add(IterableProperty<SliderTileAction>('endActions', endActions))
..add(IterableProperty<SliderTileAction>('startActions', startActions))
..add(IterableProperty<SlideTileAction>('endActions', endActions))
..add(IterableProperty<SlideTileAction>('startActions', startActions))
..add(ObjectFlagProperty<GestureTapCallback?>.has('onTap', onTap))
..add(DiagnosticsProperty<Widget?>('leading', leading))
..add(StringProperty('title', title))
@ -66,7 +66,6 @@ class SliderTile extends StatelessWidget {
}
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scaleTheme = theme.extension<ScaleTheme>()!;
@ -91,12 +90,13 @@ class SliderTile extends StatelessWidget {
selected: true,
scaleKind: a.actionScale);
return SlidableAction(
onPressed: disabled ? null : a.onPressed,
backgroundColor: scaleActionTheme.backgroundColor,
foregroundColor: scaleActionTheme.textColor,
icon: subtitle.isEmpty ? a.icon : null,
label: a.label,
padding: const EdgeInsets.all(2));
onPressed: disabled ? null : a.onPressed,
backgroundColor: scaleActionTheme.backgroundColor,
foregroundColor: scaleActionTheme.textColor,
icon: subtitle.isEmpty ? a.icon : null,
label: a.label,
padding: const EdgeInsets.all(2).scaled(context),
);
}).toList()),
startActionPane: startActions.isEmpty
? null
@ -109,17 +109,18 @@ class SliderTile extends StatelessWidget {
scaleKind: a.actionScale);
return SlidableAction(
onPressed: disabled ? null : a.onPressed,
backgroundColor: scaleActionTheme.backgroundColor,
foregroundColor: scaleActionTheme.textColor,
icon: subtitle.isEmpty ? a.icon : null,
label: a.label,
padding: const EdgeInsets.all(2));
onPressed: disabled ? null : a.onPressed,
backgroundColor: scaleActionTheme.backgroundColor,
foregroundColor: scaleActionTheme.textColor,
icon: subtitle.isEmpty ? a.icon : null,
label: a.label,
padding: const EdgeInsets.all(2).scaled(context),
);
}).toList()),
child: Padding(
padding: scaleTheme.config.useVisualIndicators
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(0, 2, 0, 2),
: const EdgeInsets.fromLTRB(0, 2, 0, 2).scaled(context),
child: GestureDetector(
onDoubleTap: onDoubleTap,
child: ListTile(
@ -131,7 +132,8 @@ class SliderTile extends StatelessWidget {
softWrap: false,
),
subtitle: subtitle.isNotEmpty ? Text(subtitle) : null,
minTileHeight: 52,
contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 4)
.scaled(context),
iconColor: scaleTileTheme.textColor,
textColor: scaleTileTheme.textColor,
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_pin.dart';
export 'option_box.dart';
export 'pop_control.dart';
export 'preferences/preferences.dart';
export 'recovery_key_widget.dart';
export 'responsive.dart';
export 'scanner_error_widget.dart';
export 'slider_tile.dart';
export 'styled_alert.dart';
export 'styled_dialog.dart';
export 'styled_scaffold.dart';
export 'wallpaper_preferences.dart';
export 'styled_widgets/styled_button_box.dart';
export 'styled_widgets/styled_widgets.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(
backgroundColor: scale.primaryScale.border,
appBar: DefaultAppBar(
context: context,
title: Text(translate('developer.title')),
leading: IconButton(
iconSize: 24.scaled(context),
icon: Icon(Icons.arrow_back, color: scale.primaryScale.borderText),
onPressed: () => GoRouterHelper(context).pop(),
),
actions: [
IconButton(
iconSize: 24.scaled(context),
icon: const Icon(Icons.copy),
color: scale.primaryScale.borderText,
disabledColor: scale.primaryScale.borderText.withAlpha(0x3F),
@ -238,6 +241,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
await copySelection(context);
}),
IconButton(
iconSize: 24.scaled(context),
icon: const Icon(Icons.copy_all),
color: scale.primaryScale.borderText,
disabledColor: scale.primaryScale.borderText.withAlpha(0x3F),
@ -245,6 +249,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
await copyAll(context);
}),
IconButton(
iconSize: 24.scaled(context),
icon: const Icon(Icons.clear_all),
color: scale.primaryScale.borderText,
disabledColor: scale.primaryScale.borderText.withAlpha(0x3F),
@ -259,7 +264,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
}
}),
SizedBox.fromSize(
size: const Size(140, 48),
size: Size(140.scaled(context), 48),
child: CustomDropdown<LogLevelDropdownItem>(
items: _logLevelDropdownItems,
initialItem: _logLevelDropdownItems
@ -300,6 +305,7 @@ class _DeveloperPageState extends State<DeveloperPage> {
Image.asset('assets/images/ellet.png'),
TerminalView(globalDebugTerminal,
textStyle: kDefaultTerminalStyle,
textScaler: TextScaler.noScaling,
controller: _terminalController,
keyboardType: TextInputType.none,
backgroundOpacity: _showEllet ? 0.75 : 1.0,

View file

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