Source Code added

This commit is contained in:
Fr4nz D13trich 2026-02-02 15:06:40 +01:00
parent 800376eafd
commit 9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions

View 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(),
);
}

View 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;
}

View 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,
),
);
}
}

View 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,
);
}
}

View 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;
}
}

View 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),
);
}
}