mirror of
https://github.com/immich-app/immich.git
synced 2026-02-22 03:00:24 -05:00
* 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>
238 lines
6.9 KiB
Dart
238 lines
6.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:showcase/constants.dart';
|
|
import 'package:syntax_highlight/syntax_highlight.dart';
|
|
|
|
late final Highlighter _codeHighlighter;
|
|
|
|
Future<void> initializeCodeHighlighter() async {
|
|
await Highlighter.initialize(['dart']);
|
|
final darkTheme = await HighlighterTheme.loadFromAssets([
|
|
'assets/themes/github_dark.json',
|
|
], const TextStyle(color: Color(0xFFe1e4e8)));
|
|
|
|
_codeHighlighter = Highlighter(language: 'dart', theme: darkTheme);
|
|
}
|
|
|
|
class ExampleCard extends StatefulWidget {
|
|
final String title;
|
|
final String? description;
|
|
final Widget preview;
|
|
final String? code;
|
|
|
|
const ExampleCard({
|
|
super.key,
|
|
required this.title,
|
|
this.description,
|
|
required this.preview,
|
|
this.code,
|
|
});
|
|
|
|
@override
|
|
State<ExampleCard> createState() => _ExampleCardState();
|
|
}
|
|
|
|
class _ExampleCardState extends State<ExampleCard> {
|
|
bool _showPreview = true;
|
|
String? code;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.code != null) {
|
|
rootBundle
|
|
.loadString('lib/pages/components/examples/${widget.code!}')
|
|
.then((value) {
|
|
setState(() {
|
|
code = value;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
elevation: 1,
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
widget.title,
|
|
style: Theme.of(context).textTheme.titleMedium
|
|
?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (widget.description != null)
|
|
Text(
|
|
widget.description!,
|
|
style: Theme.of(context).textTheme.bodyMedium
|
|
?.copyWith(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (code != null) ...[
|
|
const SizedBox(width: 16),
|
|
Row(
|
|
children: [
|
|
_ToggleButton(
|
|
icon: Icons.visibility_rounded,
|
|
label: 'Preview',
|
|
isSelected: _showPreview,
|
|
onTap: () => setState(() => _showPreview = true),
|
|
),
|
|
const SizedBox(width: 8),
|
|
_ToggleButton(
|
|
icon: Icons.code_rounded,
|
|
label: 'Code',
|
|
isSelected: !_showPreview,
|
|
onTap: () => setState(() => _showPreview = false),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
if (_showPreview)
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: SizedBox(width: double.infinity, child: widget.preview),
|
|
)
|
|
else
|
|
Container(
|
|
width: double.infinity,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF24292e),
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(
|
|
LayoutConstants.borderRadiusMedium,
|
|
),
|
|
bottomRight: Radius.circular(
|
|
LayoutConstants.borderRadiusMedium,
|
|
),
|
|
),
|
|
),
|
|
child: _CodeCard(code: code!),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ToggleButton extends StatelessWidget {
|
|
final IconData icon;
|
|
final String label;
|
|
final bool isSelected;
|
|
final VoidCallback onTap;
|
|
|
|
const _ToggleButton({
|
|
required this.icon,
|
|
required this.label,
|
|
required this.isSelected,
|
|
required this.onTap,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: isSelected
|
|
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.7)
|
|
: Theme.of(context).colorScheme.primary,
|
|
borderRadius: const BorderRadius.all(Radius.circular(24)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
icon,
|
|
size: 16,
|
|
color: Theme.of(context).colorScheme.onPrimary,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
label,
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onPrimary,
|
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CodeCard extends StatelessWidget {
|
|
final String code;
|
|
|
|
const _CodeCard({required this.code});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final lines = code.split('\n');
|
|
final lineNumberColor = Colors.white.withValues(alpha: 0.4);
|
|
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(left: 12, top: 8, bottom: 8),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: List.generate(
|
|
lines.length,
|
|
(index) => SizedBox(
|
|
height: 20,
|
|
child: Text(
|
|
'${index + 1}',
|
|
style: TextStyle(
|
|
fontFamily: 'GoogleSansCode',
|
|
fontSize: 13,
|
|
color: lineNumberColor,
|
|
height: 1.5,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
SelectableText.rich(
|
|
_codeHighlighter.highlight(code),
|
|
style: const TextStyle(
|
|
fontFamily: 'GoogleSansCode',
|
|
fontSize: 13,
|
|
height: 1.54,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|