mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat(mobile): html text (#25739)
* feat: html text * feat: mobile ui showcase (#25827) * feat: mobile ui showcase * remove showcase from main app * update fonts * update code to be loaded from asset * fix ci --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> # Conflicts: # mobile/lib/widgets/common/immich_sliver_app_bar.dart --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class CloseButtonPage extends StatelessWidget {
|
||||
const CloseButtonPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.closeButton.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichCloseButton',
|
||||
subtitle: 'Pre-configured close button for dialogs and sheets.',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Default & Custom',
|
||||
preview: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichCloseButton(onPressed: () {}),
|
||||
ImmichCloseButton(
|
||||
variant: ImmichVariant.filled,
|
||||
onPressed: () {},
|
||||
),
|
||||
ImmichCloseButton(
|
||||
color: ImmichColor.secondary,
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class HtmlTextBoldText extends StatelessWidget {
|
||||
const HtmlTextBoldText({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichHtmlText(
|
||||
'This is <b>bold text</b> and <strong>strong text</strong>.',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class HtmlTextLinks extends StatelessWidget {
|
||||
const HtmlTextLinks({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichHtmlText(
|
||||
'Read the <docs-link>documentation</docs-link> or visit <github-link>GitHub</github-link>.',
|
||||
linkHandlers: {
|
||||
'docs-link': () {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Docs link clicked!')));
|
||||
},
|
||||
'github-link': () {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('GitHub link clicked!')));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
|
||||
class HtmlTextNestedTags extends StatelessWidget {
|
||||
const HtmlTextNestedTags({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ImmichHtmlText(
|
||||
'You can <b>combine <link>bold and links</link></b> together.',
|
||||
linkHandlers: {
|
||||
'link': () {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Nested link clicked!')));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class FormPage extends StatefulWidget {
|
||||
const FormPage({super.key});
|
||||
|
||||
@override
|
||||
State<FormPage> createState() => _FormPageState();
|
||||
}
|
||||
|
||||
class _FormPageState extends State<FormPage> {
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
String _result = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.form.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichForm',
|
||||
subtitle:
|
||||
'Form container with built-in validation and submit handling.',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Login Form',
|
||||
preview: Column(
|
||||
children: [
|
||||
ImmichForm(
|
||||
submitText: 'Login',
|
||||
submitIcon: Icons.login,
|
||||
onSubmit: () async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
setState(() {
|
||||
_result = 'Form submitted!';
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
spacing: 10,
|
||||
children: [
|
||||
ImmichTextInput(
|
||||
label: 'Email',
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) =>
|
||||
value?.isEmpty ?? true ? 'Required' : null,
|
||||
),
|
||||
ImmichPasswordInput(
|
||||
label: 'Password',
|
||||
controller: _passwordController,
|
||||
validator: (value) =>
|
||||
value?.isEmpty ?? true ? 'Required' : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_result.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(_result, style: const TextStyle(color: Colors.green)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:showcase/pages/components/examples/html_text_bold_text.dart';
|
||||
import 'package:showcase/pages/components/examples/html_text_links.dart';
|
||||
import 'package:showcase/pages/components/examples/html_text_nested_tags.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class HtmlTextPage extends StatelessWidget {
|
||||
const HtmlTextPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.htmlText.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichHtmlText',
|
||||
subtitle: 'Render text with HTML formatting (bold, links).',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Bold Text',
|
||||
preview: const HtmlTextBoldText(),
|
||||
code: 'html_text_bold_text.dart',
|
||||
),
|
||||
ExampleCard(
|
||||
title: 'Links',
|
||||
preview: const HtmlTextLinks(),
|
||||
code: 'html_text_links.dart',
|
||||
),
|
||||
ExampleCard(
|
||||
title: 'Nested Tags',
|
||||
preview: const HtmlTextNestedTags(),
|
||||
code: 'html_text_nested_tags.dart',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class IconButtonPage extends StatelessWidget {
|
||||
const IconButtonPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.iconButton.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichIconButton',
|
||||
subtitle: 'Icon-only button with customizable styling.',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Variants & Colors',
|
||||
preview: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichIconButton(
|
||||
icon: Icons.add,
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.filled,
|
||||
),
|
||||
ImmichIconButton(
|
||||
icon: Icons.edit,
|
||||
onPressed: () {},
|
||||
variant: ImmichVariant.ghost,
|
||||
),
|
||||
ImmichIconButton(
|
||||
icon: Icons.delete,
|
||||
onPressed: () {},
|
||||
color: ImmichColor.secondary,
|
||||
),
|
||||
ImmichIconButton(
|
||||
icon: Icons.settings,
|
||||
onPressed: () {},
|
||||
disabled: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class PasswordInputPage extends StatelessWidget {
|
||||
const PasswordInputPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.passwordInput.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichPasswordInput',
|
||||
subtitle: 'Password field with visibility toggle.',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Password Input',
|
||||
preview: ImmichPasswordInput(
|
||||
label: 'Password',
|
||||
hintText: 'Enter your password',
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Password is required';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password must be at least 8 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class TextButtonPage extends StatefulWidget {
|
||||
const TextButtonPage({super.key});
|
||||
|
||||
@override
|
||||
State<TextButtonPage> createState() => _TextButtonPageState();
|
||||
}
|
||||
|
||||
class _TextButtonPageState extends State<TextButtonPage> {
|
||||
bool _isLoading = false;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.textButton.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichTextButton',
|
||||
subtitle:
|
||||
'A versatile button component with multiple variants and color options.',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Variants',
|
||||
description:
|
||||
'Filled and ghost variants for different visual hierarchy',
|
||||
preview: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Filled',
|
||||
variant: ImmichVariant.filled,
|
||||
expanded: false,
|
||||
),
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Ghost',
|
||||
variant: ImmichVariant.ghost,
|
||||
expanded: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ExampleCard(
|
||||
title: 'Colors',
|
||||
description: 'Primary and secondary color options',
|
||||
preview: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Primary',
|
||||
color: ImmichColor.primary,
|
||||
expanded: false,
|
||||
),
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Secondary',
|
||||
color: ImmichColor.secondary,
|
||||
expanded: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ExampleCard(
|
||||
title: 'With Icons',
|
||||
description: 'Add leading icons',
|
||||
preview: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'With Icon',
|
||||
icon: Icons.add,
|
||||
expanded: false,
|
||||
),
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Download',
|
||||
icon: Icons.download,
|
||||
variant: ImmichVariant.ghost,
|
||||
expanded: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ExampleCard(
|
||||
title: 'Loading State',
|
||||
description: 'Shows loading indicator during async operations',
|
||||
preview: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ImmichTextButton(
|
||||
onPressed: () async {
|
||||
setState(() => _isLoading = true);
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
},
|
||||
labelText: _isLoading ? 'Loading...' : 'Click Me',
|
||||
loading: _isLoading,
|
||||
expanded: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ExampleCard(
|
||||
title: 'Disabled State',
|
||||
description: 'Buttons can be disabled',
|
||||
preview: Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Disabled',
|
||||
disabled: true,
|
||||
expanded: false,
|
||||
),
|
||||
ImmichTextButton(
|
||||
onPressed: () {},
|
||||
labelText: 'Disabled Ghost',
|
||||
variant: ImmichVariant.ghost,
|
||||
disabled: true,
|
||||
expanded: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_ui/immich_ui.dart';
|
||||
import 'package:showcase/routes.dart';
|
||||
import 'package:showcase/widgets/component_examples.dart';
|
||||
import 'package:showcase/widgets/example_card.dart';
|
||||
import 'package:showcase/widgets/page_title.dart';
|
||||
|
||||
class TextInputPage extends StatefulWidget {
|
||||
const TextInputPage({super.key});
|
||||
|
||||
@override
|
||||
State<TextInputPage> createState() => _TextInputPageState();
|
||||
}
|
||||
|
||||
class _TextInputPageState extends State<TextInputPage> {
|
||||
final _controller1 = TextEditingController();
|
||||
final _controller2 = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PageTitle(
|
||||
title: AppRoute.textInput.name,
|
||||
child: ComponentExamples(
|
||||
title: 'ImmichTextInput',
|
||||
subtitle: 'Text field with validation support.',
|
||||
examples: [
|
||||
ExampleCard(
|
||||
title: 'Basic Usage',
|
||||
preview: Column(
|
||||
children: [
|
||||
ImmichTextInput(
|
||||
label: 'Email',
|
||||
hintText: 'Enter your email',
|
||||
controller: _controller1,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ImmichTextInput(
|
||||
label: 'Username',
|
||||
controller: _controller2,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Username is required';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Username must be at least 3 characters';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller1.dispose();
|
||||
_controller2.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user