mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 08:49:01 +03:00
refactor(mobile): form & form field (#25042)
* refactor: form & form field * chore: remove unused components --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1,3 +1,10 @@
|
||||
export 'src/buttons/close_button.dart';
|
||||
export 'src/buttons/icon_button.dart';
|
||||
export 'src/components/close_button.dart';
|
||||
export 'src/components/form.dart';
|
||||
export 'src/components/icon_button.dart';
|
||||
export 'src/components/password_input.dart';
|
||||
export 'src/components/text_button.dart';
|
||||
export 'src/components/text_input.dart';
|
||||
export 'src/constants.dart';
|
||||
export 'src/theme.dart';
|
||||
export 'src/translation.dart';
|
||||
export 'src/types.dart';
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/buttons/icon_button.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
import 'icon_button.dart';
|
||||
|
||||
class ImmichCloseButton extends StatelessWidget {
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichCloseButton({
|
||||
super.key,
|
||||
this.onTap,
|
||||
this.onPressed,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.ghost,
|
||||
});
|
||||
@@ -20,6 +21,6 @@ class ImmichCloseButton extends StatelessWidget {
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
onTap: onTap ?? () => Navigator.of(context).pop(),
|
||||
onPressed: onPressed ?? () => Navigator.of(context).pop(),
|
||||
);
|
||||
}
|
||||
98
mobile/packages/ui/lib/src/components/form.dart
Normal file
98
mobile/packages/ui/lib/src/components/form.dart
Normal file
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichForm extends StatefulWidget {
|
||||
final String? submitText;
|
||||
final IconData? submitIcon;
|
||||
final FutureOr<void> Function()? onSubmit;
|
||||
final Widget child;
|
||||
|
||||
const ImmichForm({
|
||||
super.key,
|
||||
this.submitText,
|
||||
this.submitIcon,
|
||||
required this.onSubmit,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ImmichForm> createState() => ImmichFormState();
|
||||
|
||||
static ImmichFormState of(BuildContext context) {
|
||||
final scope = context.dependOnInheritedWidgetOfExactType<_ImmichFormScope>();
|
||||
if (scope == null) {
|
||||
throw FlutterError(
|
||||
'ImmichForm.of() called with a context that does not contain an ImmichForm.\n'
|
||||
'No ImmichForm ancestor could be found starting from the context that was passed to '
|
||||
'ImmichForm.of(). This usually happens when the context provided is '
|
||||
'from a widget above the ImmichForm.\n'
|
||||
'The context used was:\n'
|
||||
'$context',
|
||||
);
|
||||
}
|
||||
return scope._formState;
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichFormState extends State<ImmichForm> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
bool _isLoading = false;
|
||||
|
||||
FutureOr<void> submit() async {
|
||||
final isValid = _formKey.currentState?.validate() ?? false;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await widget.onSubmit?.call();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final submitText = widget.submitText ?? context.translations.submit;
|
||||
return _ImmichFormScope(
|
||||
formState: this,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
spacing: ImmichSpacing.md,
|
||||
children: [
|
||||
widget.child,
|
||||
ImmichTextButton(
|
||||
labelText: submitText,
|
||||
icon: widget.submitIcon,
|
||||
variant: ImmichVariant.filled,
|
||||
loading: _isLoading,
|
||||
onPressed: submit,
|
||||
disabled: widget.onSubmit == null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImmichFormScope extends InheritedWidget {
|
||||
const _ImmichFormScope({required super.child, required ImmichFormState formState}) : _formState = formState;
|
||||
|
||||
final ImmichFormState _formState;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_ImmichFormScope oldWidget) => oldWidget._formState != _formState;
|
||||
}
|
||||
@@ -3,42 +3,48 @@ import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool disabled;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.onTap,
|
||||
required this.onPressed,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final background = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.onPrimary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.onSecondary,
|
||||
ImmichColor.primary => colorScheme.onPrimary,
|
||||
ImmichColor.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => Theme.of(context).colorScheme.primary,
|
||||
ImmichColor.secondary => Theme.of(context).colorScheme.secondary,
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
final effectiveOnPressed = disabled ? null : onPressed;
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
onPressed: onTap,
|
||||
onPressed: effectiveOnPressed,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: background,
|
||||
foregroundColor: foreground,
|
||||
58
mobile/packages/ui/lib/src/components/password_input.dart
Normal file
58
mobile/packages/ui/lib/src/components/password_input.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/components/text_input.dart';
|
||||
import 'package:immich_ui/src/internal.dart';
|
||||
|
||||
class ImmichPasswordInput extends StatefulWidget {
|
||||
final String? label;
|
||||
final String? hintText;
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(BuildContext, String)? onSubmit;
|
||||
final TextInputAction? keyboardAction;
|
||||
|
||||
const ImmichPasswordInput({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
this.label,
|
||||
this.hintText,
|
||||
this.validator,
|
||||
this.onSubmit,
|
||||
this.keyboardAction,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _ImmichPasswordInputState();
|
||||
}
|
||||
|
||||
class _ImmichPasswordInputState extends State<ImmichPasswordInput> {
|
||||
bool _visible = false;
|
||||
|
||||
void _toggleVisibility() {
|
||||
setState(() {
|
||||
_visible = !_visible;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichTextInput(
|
||||
key: widget.key,
|
||||
label: widget.label ?? context.translations.password,
|
||||
hintText: widget.hintText,
|
||||
controller: widget.controller,
|
||||
focusNode: widget.focusNode,
|
||||
validator: widget.validator,
|
||||
onSubmit: widget.onSubmit,
|
||||
keyboardAction: widget.keyboardAction,
|
||||
obscureText: !_visible,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: _toggleVisibility,
|
||||
icon: Icon(_visible ? Icons.visibility_off_rounded : Icons.visibility_rounded),
|
||||
),
|
||||
autofillHints: [AutofillHints.password],
|
||||
keyboardType: TextInputType.text,
|
||||
);
|
||||
}
|
||||
}
|
||||
87
mobile/packages/ui/lib/src/components/text_button.dart
Normal file
87
mobile/packages/ui/lib/src/components/text_button.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichTextButton extends StatelessWidget {
|
||||
final String labelText;
|
||||
final IconData? icon;
|
||||
final FutureOr<void> Function() onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool expanded;
|
||||
final bool loading;
|
||||
final bool disabled;
|
||||
|
||||
const ImmichTextButton({
|
||||
super.key,
|
||||
required this.labelText,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
this.variant = ImmichVariant.filled,
|
||||
this.color = ImmichColor.primary,
|
||||
this.expanded = true,
|
||||
this.loading = false,
|
||||
this.disabled = false,
|
||||
});
|
||||
|
||||
Widget _buildButton(ImmichVariant variant) {
|
||||
final Widget? effectiveIcon = loading
|
||||
? const SizedBox.square(
|
||||
dimension: ImmichIconSize.md,
|
||||
child: CircularProgressIndicator(strokeWidth: ImmichBorderWidth.lg),
|
||||
)
|
||||
: icon != null
|
||||
? Icon(icon, fontWeight: FontWeight.w600)
|
||||
: null;
|
||||
final hasIcon = effectiveIcon != null;
|
||||
|
||||
final label = Text(labelText, style: const TextStyle(fontSize: ImmichTextSize.body, fontWeight: FontWeight.bold));
|
||||
final style = ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: ImmichSpacing.md));
|
||||
|
||||
final effectiveOnPressed = disabled || loading ? null : onPressed;
|
||||
|
||||
switch (variant) {
|
||||
case ImmichVariant.filled:
|
||||
if (hasIcon) {
|
||||
return ElevatedButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
case ImmichVariant.ghost:
|
||||
if (hasIcon) {
|
||||
return TextButton.icon(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
icon: effectiveIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
style: style,
|
||||
onPressed: effectiveOnPressed,
|
||||
child: label,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final button = _buildButton(variant);
|
||||
if (expanded) {
|
||||
return SizedBox(width: double.infinity, child: button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
}
|
||||
88
mobile/packages/ui/lib/src/components/text_input.dart
Normal file
88
mobile/packages/ui/lib/src/components/text_input.dart
Normal file
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImmichTextInput extends StatefulWidget {
|
||||
final String label;
|
||||
final String? hintText;
|
||||
final TextEditingController? controller;
|
||||
final FocusNode? focusNode;
|
||||
final String? Function(String?)? validator;
|
||||
final void Function(BuildContext, String)? onSubmit;
|
||||
final TextInputType keyboardType;
|
||||
final TextInputAction? keyboardAction;
|
||||
final List<String>? autofillHints;
|
||||
final Widget? suffixIcon;
|
||||
final bool obscureText;
|
||||
|
||||
const ImmichTextInput({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.focusNode,
|
||||
required this.label,
|
||||
this.hintText,
|
||||
this.validator,
|
||||
this.onSubmit,
|
||||
this.keyboardType = TextInputType.text,
|
||||
this.keyboardAction,
|
||||
this.autofillHints,
|
||||
this.suffixIcon,
|
||||
this.obscureText = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State createState() => _ImmichTextInputState();
|
||||
}
|
||||
|
||||
class _ImmichTextInputState extends State<ImmichTextInput> {
|
||||
late final FocusNode _focusNode;
|
||||
String? _error;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.focusNode == null) {
|
||||
_focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _validateInput(String? value) {
|
||||
setState(() {
|
||||
_error = widget.validator?.call(value);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get _hasError => _error != null && _error!.isNotEmpty;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeData = Theme.of(context);
|
||||
|
||||
return TextFormField(
|
||||
controller: widget.controller,
|
||||
focusNode: _focusNode,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.hintText,
|
||||
labelText: widget.label,
|
||||
labelStyle: themeData.inputDecorationTheme.labelStyle?.copyWith(
|
||||
color: _hasError ? themeData.colorScheme.error : null,
|
||||
),
|
||||
errorText: _error,
|
||||
suffixIcon: widget.suffixIcon,
|
||||
),
|
||||
obscureText: widget.obscureText,
|
||||
validator: _validateInput,
|
||||
keyboardType: widget.keyboardType,
|
||||
textInputAction: widget.keyboardAction,
|
||||
autofillHints: widget.autofillHints,
|
||||
onTap: () => setState(() => _error = null),
|
||||
onTapOutside: (_) => _focusNode.unfocus(),
|
||||
onFieldSubmitted: (value) => widget.onSubmit?.call(context, value),
|
||||
);
|
||||
}
|
||||
}
|
||||
199
mobile/packages/ui/lib/src/constants.dart
Normal file
199
mobile/packages/ui/lib/src/constants.dart
Normal file
@@ -0,0 +1,199 @@
|
||||
/// Spacing constants for gaps between widgets
|
||||
abstract class ImmichSpacing {
|
||||
const ImmichSpacing._();
|
||||
|
||||
/// Extra small spacing: 4.0
|
||||
static const double xs = 4.0;
|
||||
|
||||
/// Small spacing: 8.0
|
||||
static const double sm = 8.0;
|
||||
|
||||
/// Medium spacing (default): 12.0
|
||||
static const double md = 12.0;
|
||||
|
||||
/// Large spacing: 16.0
|
||||
static const double lg = 16.0;
|
||||
|
||||
/// Extra large spacing: 24.0
|
||||
static const double xl = 24.0;
|
||||
|
||||
/// Extra extra large spacing: 32.0
|
||||
static const double xxl = 32.0;
|
||||
|
||||
/// Extra extra extra large spacing: 48.0
|
||||
static const double xxxl = 48.0;
|
||||
}
|
||||
|
||||
/// Border radius constants for consistent rounded corners
|
||||
abstract class ImmichRadius {
|
||||
const ImmichRadius._();
|
||||
|
||||
/// No radius: 0.0
|
||||
static const double none = 0.0;
|
||||
|
||||
/// Extra small radius: 4.0
|
||||
static const double xs = 4.0;
|
||||
|
||||
/// Small radius: 8.0
|
||||
static const double sm = 8.0;
|
||||
|
||||
/// Medium radius (default): 12.0
|
||||
static const double md = 12.0;
|
||||
|
||||
/// Large radius: 16.0
|
||||
static const double lg = 16.0;
|
||||
|
||||
/// Extra large radius: 20.0
|
||||
static const double xl = 20.0;
|
||||
|
||||
/// Extra extra large radius: 24.0
|
||||
static const double xxl = 24.0;
|
||||
|
||||
/// Full circular radius: infinity
|
||||
static const double full = double.infinity;
|
||||
}
|
||||
|
||||
/// Icon size constants for consistent icon sizing
|
||||
abstract class ImmichIconSize {
|
||||
const ImmichIconSize._();
|
||||
|
||||
/// Extra small icon: 16.0
|
||||
static const double xs = 16.0;
|
||||
|
||||
/// Small icon: 20.0
|
||||
static const double sm = 20.0;
|
||||
|
||||
/// Medium icon (default): 24.0
|
||||
static const double md = 24.0;
|
||||
|
||||
/// Large icon: 32.0
|
||||
static const double lg = 32.0;
|
||||
|
||||
/// Extra large icon: 40.0
|
||||
static const double xl = 40.0;
|
||||
|
||||
/// Extra extra large icon: 48.0
|
||||
static const double xxl = 48.0;
|
||||
}
|
||||
|
||||
/// Animation duration constants for consistent timing
|
||||
abstract class ImmichDuration {
|
||||
const ImmichDuration._();
|
||||
|
||||
/// Extra fast: 100ms
|
||||
static const Duration extraFast = Duration(milliseconds: 100);
|
||||
|
||||
/// Fast: 150ms
|
||||
static const Duration fast = Duration(milliseconds: 150);
|
||||
|
||||
/// Normal: 200ms
|
||||
static const Duration normal = Duration(milliseconds: 200);
|
||||
|
||||
/// Moderate: 300ms
|
||||
static const Duration moderate = Duration(milliseconds: 300);
|
||||
|
||||
/// Slow: 500ms
|
||||
static const Duration slow = Duration(milliseconds: 500);
|
||||
|
||||
/// Extra slow: 700ms
|
||||
static const Duration extraSlow = Duration(milliseconds: 700);
|
||||
}
|
||||
|
||||
/// Elevation constants for consistent shadows and depth
|
||||
abstract class ImmichElevation {
|
||||
const ImmichElevation._();
|
||||
|
||||
/// No elevation: 0.0
|
||||
static const double none = 0.0;
|
||||
|
||||
/// Extra small elevation: 1.0
|
||||
static const double xs = 1.0;
|
||||
|
||||
/// Small elevation: 2.0
|
||||
static const double sm = 2.0;
|
||||
|
||||
/// Medium elevation: 4.0
|
||||
static const double md = 4.0;
|
||||
|
||||
/// Large elevation: 8.0
|
||||
static const double lg = 8.0;
|
||||
|
||||
/// Extra large elevation: 12.0
|
||||
static const double xl = 12.0;
|
||||
|
||||
/// Extra extra large elevation: 16.0
|
||||
static const double xxl = 16.0;
|
||||
}
|
||||
|
||||
/// Border width constants (similar to Tailwind's border-* scale)
|
||||
abstract class ImmichBorderWidth {
|
||||
const ImmichBorderWidth._();
|
||||
|
||||
/// No border: 0.0
|
||||
static const double none = 0.0;
|
||||
|
||||
/// Hairline border: 0.5
|
||||
static const double hairline = 0.5;
|
||||
|
||||
/// Default border: 1.0 (border)
|
||||
static const double base = 1.0;
|
||||
|
||||
/// Medium border: 2.0 (border-2)
|
||||
static const double md = 2.0;
|
||||
|
||||
/// Large border: 3.0 (border-4)
|
||||
static const double lg = 3.0;
|
||||
|
||||
/// Extra large border: 4.0
|
||||
static const double xl = 4.0;
|
||||
}
|
||||
|
||||
/// Text size constants with semantic HTML-like naming
|
||||
/// These follow a type scale for harmonious text hierarchy
|
||||
abstract class ImmichTextSize {
|
||||
const ImmichTextSize._();
|
||||
|
||||
/// Caption text: 10.0
|
||||
/// Use for: Tiny labels, legal text, metadata, timestamps
|
||||
static const double caption = 10.0;
|
||||
|
||||
/// Label text: 12.0
|
||||
/// Use for: Form labels, secondary text, helper text
|
||||
static const double label = 12.0;
|
||||
|
||||
/// Body text: 14.0 (default)
|
||||
/// Use for: Main body text, paragraphs, default UI text
|
||||
static const double body = 14.0;
|
||||
|
||||
/// Body emphasized: 16.0
|
||||
/// Use for: Emphasized body text, button labels, tabs
|
||||
static const double bodyLarge = 16.0;
|
||||
|
||||
/// Heading 6: 18.0 (smallest heading)
|
||||
/// Use for: Subtitles, card titles, section headers
|
||||
static const double h6 = 18.0;
|
||||
|
||||
/// Heading 5: 20.0
|
||||
/// Use for: Small headings, prominent labels
|
||||
static const double h5 = 20.0;
|
||||
|
||||
/// Heading 4: 24.0
|
||||
/// Use for: Page titles, dialog titles
|
||||
static const double h4 = 24.0;
|
||||
|
||||
/// Heading 3: 30.0
|
||||
/// Use for: Section headings, large headings
|
||||
static const double h3 = 30.0;
|
||||
|
||||
/// Heading 2: 36.0
|
||||
/// Use for: Major section headings
|
||||
static const double h2 = 36.0;
|
||||
|
||||
/// Heading 1: 48.0 (largest heading)
|
||||
/// Use for: Page hero headings, main titles
|
||||
static const double h1 = 48.0;
|
||||
|
||||
/// Display text: 60.0
|
||||
/// Use for: Hero numbers, splash screens, extra large display
|
||||
static const double display = 60.0;
|
||||
}
|
||||
6
mobile/packages/ui/lib/src/internal.dart
Normal file
6
mobile/packages/ui/lib/src/internal.dart
Normal file
@@ -0,0 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/translation.dart';
|
||||
|
||||
extension TranslationHelper on BuildContext {
|
||||
ImmichTranslations get translations => ImmichTranslationProvider.of(this);
|
||||
}
|
||||
42
mobile/packages/ui/lib/src/theme.dart
Normal file
42
mobile/packages/ui/lib/src/theme.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/constants.dart';
|
||||
|
||||
class ImmichThemeProvider extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final Widget child;
|
||||
|
||||
const ImmichThemeProvider({super.key, required this.colorScheme, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: colorScheme,
|
||||
brightness: colorScheme.brightness,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.primary),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.primary),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: colorScheme.error),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(ImmichRadius.md)),
|
||||
),
|
||||
labelStyle: TextStyle(color: colorScheme.primary, fontWeight: FontWeight.w600),
|
||||
hintStyle: const TextStyle(fontSize: ImmichTextSize.body),
|
||||
errorStyle: TextStyle(color: colorScheme.error, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
31
mobile/packages/ui/lib/src/translation.dart
Normal file
31
mobile/packages/ui/lib/src/translation.dart
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImmichTranslations {
|
||||
late String submit;
|
||||
late String password;
|
||||
|
||||
ImmichTranslations({String? submit, String? password}) {
|
||||
this.submit = submit ?? 'Submit';
|
||||
this.password = password ?? 'Password';
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichTranslationProvider extends InheritedWidget {
|
||||
final ImmichTranslations? translations;
|
||||
|
||||
const ImmichTranslationProvider({
|
||||
super.key,
|
||||
this.translations,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static ImmichTranslations of(BuildContext context) {
|
||||
final provider = context.dependOnInheritedWidgetOfExactType<ImmichTranslationProvider>();
|
||||
return provider?.translations ?? ImmichTranslations();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant ImmichTranslationProvider oldWidget) {
|
||||
return oldWidget.translations != translations;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user