Source Code added
This commit is contained in:
parent
800376eafd
commit
9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions
26
mobile/packages/ui/lib/src/components/close_button.dart
Normal file
26
mobile/packages/ui/lib/src/components/close_button.dart
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
import 'icon_button.dart';
|
||||
|
||||
class ImmichCloseButton extends StatelessWidget {
|
||||
final VoidCallback? onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
|
||||
const ImmichCloseButton({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
this.color = ImmichColor.primary,
|
||||
this.variant = ImmichVariant.ghost,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ImmichIconButton(
|
||||
key: key,
|
||||
icon: Icons.close,
|
||||
color: color,
|
||||
variant: variant,
|
||||
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;
|
||||
}
|
||||
54
mobile/packages/ui/lib/src/components/icon_button.dart
Normal file
54
mobile/packages/ui/lib/src/components/icon_button.dart
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/src/types.dart';
|
||||
|
||||
class ImmichIconButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final ImmichVariant variant;
|
||||
final ImmichColor color;
|
||||
final bool disabled;
|
||||
|
||||
const ImmichIconButton({
|
||||
super.key,
|
||||
required this.icon,
|
||||
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 => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
ImmichVariant.ghost => Colors.transparent,
|
||||
};
|
||||
|
||||
final foreground = switch (variant) {
|
||||
ImmichVariant.filled => switch (color) {
|
||||
ImmichColor.primary => colorScheme.onPrimary,
|
||||
ImmichColor.secondary => colorScheme.onSecondary,
|
||||
},
|
||||
ImmichVariant.ghost => switch (color) {
|
||||
ImmichColor.primary => colorScheme.primary,
|
||||
ImmichColor.secondary => colorScheme.secondary,
|
||||
},
|
||||
};
|
||||
|
||||
final effectiveOnPressed = disabled ? null : onPressed;
|
||||
|
||||
return IconButton(
|
||||
icon: Icon(icon),
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue